I got Pi working inside Tidewave.

Not the terminal version where Pi talks to Tidewave’s MCP server — the version where Pi runs as a Tidewave External Agent, inside Tidewave’s own UI, with access to the shared browser window.

It works. It’s been working well since the fixes.

What Pi is

Pi is a minimal terminal coding agent by Mario Zechner. It’s intentionally stripped down — no built-in MCP, no sub-agents, no plan mode, no permission popups. The philosophy is that all of those are extension concerns, not core features. You build or install the parts you need.

Pi runs in four modes: interactive terminal, print/JSON, RPC (stdin/stdout JSON protocol), and SDK. Most integrations go through RPC — Pi spawns as a subprocess, you send commands on stdin, events stream back on stdout.

The MCP omission is deliberate. Mario wrote about this — his position is that MCP adds complexity without enough payoff for many workflows. If you want MCP, you add it through an extension or an adapter.

That design choice is what makes this whole story happen.

What Tidewave is

Tidewave is a development tool by José Valim that runs inside your web app and gives AI agents access to a shared browser context. It supports External Agents — agents that live inside Tidewave’s UI, see the same page you see, and can navigate and inspect the app in context.

Tidewave speaks ACP to its external agents and passes them MCP servers for tools access. It expects the agent to handle both.

The thing I actually wanted

What I wanted was simple to describe and took some patching to get right.

I wanted to use Pi inside Tidewave’s browser window.

I didn’t want Pi in one terminal and Tidewave in another. I didn’t want Pi talking to the app over some side channel while I looked at the browser separately. I didn’t want something that was technically integrated but still felt like two separate tools.

I wanted an agent inside the UI, looking at the same page I’m looking at, able to navigate the app and inspect things in context.

Pi doesn’t support MCP. Tidewave expects agents that speak ACP and accept MCP servers. That’s the gap.

The tool that mattered

The adapter that mattered was pi-acp.

Not Pi itself. Not pi-executor. Not pi-mcp-adapter.

pi-acp is a separate adapter that makes Pi speak ACP well enough for hosts that expect an ACP agent process. It was built primarily for Zed, and the README is upfront that other ACP clients “may have varying support.” Tidewave is not a tested target.

At the time I was testing this, the installed version was:

pi-acp 0.0.25

The repository is separate from Pi itself:

https://github.com/svkozak/pi-acp

I did not fork it. I patched the installed compiled file in place, which is ugly but fast.

The patch lived here:

/opt/homebrew/lib/node_modules/pi-acp/dist/index.js

I wouldn’t recommend that as a long-term strategy, but it was enough to learn what the real problem was.

My first wrong assumption

At first I thought the issue was just capability advertisement.

Tidewave connected to Pi, but complained that the agent didn’t support MCP servers over Streamable HTTP. The pi-acp README actually documents this — MCP servers are “accepted but not wired to pi.” It’s not a bug, it’s a known scope boundary. But Tidewave needs an agent that can receive MCP servers, so I had to patch past it.

So I patched pi-acp to advertise what Tidewave seemed to expect.

In practice that meant changing its ACP initialize response so it claimed support for:

{
  "agentCapabilities": {
    "loadSession": true,
    "mcpCapabilities": { "http": true, "sse": true },
    "promptCapabilities": { "embeddedContext": true }
  }
}

The loadSession: true turned out to matter more than I realized at the time. It tells Tidewave that the agent can restore a session from a previous state, which is exactly the capability the later fix depends on.

I also added logic so that when Tidewave passed MCP servers into the ACP session, pi-acp would write them into the project’s .pi/mcp.json file (prefixed with acp_ to avoid collisions). That let Pi see the Tidewave-provided MCP servers as part of the session.

The shape of that patch was roughly this:

syncProjectMcpConfig(params.cwd, params.mcpServers)

That helped. It got me past the first blocker. But it did not solve the problem.

The first message trap

After those capability patches, Tidewave could connect.

Then something slightly confusing: the first message sometimes worked, but later turns would fail with:

Invalid params

So I added debug logging directly inside pi-acp, writing to ~/.pi/pi-acp/debug.log.

The real bug

The logs showed what was happening:

Tidewave was still sending the same ACP sessionId, but pi-acp had already lost the in-memory Pi session associated with it.

The mechanism was simple: pi-acp keeps live sessions in a Node process memory map. If the process restarts for any reason — a crash, a reconnect, anything — that map is empty. But Pi’s session data is still on disk. The adapter just wasn’t looking there.

From Tidewave’s point of view, everything was normal. Same session, next prompt.

From pi-acp’s point of view, the in-memory map had no entry, so it treated the next prompt like invalid input.

The log pattern that made it clear:

prompt.request
prompt.session_missing
prompt.session_restored
prompt.response

That was the real bug.

The problem was session recovery.

What fixed it

Pi persists sessions to disk. pi-acp also keeps a session map. So the fix was straightforward: if a prompt arrives for a sessionId that’s no longer in memory but the adapter knows which session file belongs to it, restore the session instead of throwing.

The actual stored state lived in two places:

~/.pi/pi-acp/session-map.json
~/.pi/agent/sessions/.../<session-id>.jsonl

And the restore logic was basically:

const stored = this.store.get(params.sessionId)
if (stored?.sessionFile && stored?.cwd) {
  const proc = await PiRpcProcess.spawn({
    cwd: stored.cwd,
    sessionPath: stored.sessionFile,
    piCommand: process.env.PI_ACP_PI_COMMAND,
  })
  session = this.sessions.getOrCreate(params.sessionId, { ... })
}

Once that was in place, later turns stopped failing.

What “working” means here

After the session-restore patch, I could do the thing I wanted.

Pi inside Tidewave could:

  • connect as an External Agent
  • access Tidewave-provided tools
  • connect to the Tidewave browser MCP server
  • inspect the shared browser state
  • navigate the app

I verified that by having Pi navigate the local app to the sign-in page and describe what it could see. It successfully moved the shared browser to:

http://localhost:3000/users/sign_in

Pi was operating inside Tidewave’s UI, using the shared browser context.

What I still don’t know

Since the fixes, this has been working well. The question is whether it stays that way.

The current state is:

  • the core flow works
  • the fix still depends on a local patch to a compiled file
  • I haven’t run it long enough to call it production-stable

Working, promising, too fresh to make big claims.

What I learned from this

The most interesting part was not Tidewave. And it wasn’t really Pi either.

It was the adapter.

The first blocker was capability advertisement — Tidewave needed the adapter to declare what it supported.

The second was that Tidewave and pi-acp had different assumptions about session durability.

Tidewave assumed a session ID remained meaningful across turns.

pi-acp, before the patch, behaved as if losing the in-memory session meant the session was effectively gone.

If the agent has persisted state on disk, the adapter should recover from that. Straightforward fix, not a Tidewave-specific hack.

If you want to try this yourself

Use Tidewave External Agent mode with pi-acp 0.0.25. I launched it through a thin wrapper (exec /opt/homebrew/bin/pi-acp).

The three patch points:

  1. ACP initialize — advertise loadSession, MCP HTTP/SSE, and embedded context support
  2. Session creation — sync Tidewave-provided MCP servers into .pi/mcp.json
  3. Prompt handling — if the session ID exists on disk but not in memory, restore it before throwing

The two files that matter most while debugging:

~/.pi/pi-acp/debug.log
~/.pi/pi-acp/session-map.json

If I keep using this and it stays solid, I’ll either publish the patch properly or upstream the parts that belong upstream. For now this is a field report, not an install guide.

What I would do if I had to do this again

I’d still start with the ugly local patch. It gets you to the truth faster than trying to make it clean first.

Next step is moving the working state from a patched dist/index.js to a proper source branch.

For now, this is the memory artifact. The patch that mattered was the one that restored persisted Pi sessions when the adapter had lost them in memory.