pumi

Agents

by pumi-dev

Coding agents in pumi's tabs: Claude Code, Codex, pi and more open from the + menu, in their own terminal or in a GUI. pumi sees when each one works or waits on you, and each one is told about the workspace around it.

agentschatacpclaude-codecodexpihookscontext

What it adds

Commands
toggle Agents: flip the focused tab between the terminal and the chat
handoff Agents: continue this agent in its GUI
back Agents: continue this chat tab's session in the agent's terminal
Pane types
term, session
Adds to
tab.toolbar
Settings
5
Key bindings
1
Uses
files, processes

README

Coding agents in pumi's tabs, in one plugin. A pane's + menu opens each agent installed here, in its own terminal or in a GUI; pumi sees when it works, waits on you or is done; and it is told about the sessions, panes and tabs around it.

pumi plugin add pumi-dev/plugins/agents

Settings → Agents has one switch per agent:

What opensAgents
Terminalthe agent's own screen; the tab's button flips it to a chat over it and backall
GUIa chat tab that runs the agent over its SDK, else ACPpi, Codex, Claude Code (SDK); Gemini, OpenCode, Copilot, Kiro (ACP)
Offnothing: it is left out of the + menu, its tabs are left alone and its hook taken outall

Open agents in under them sets every agent at once (those Off stay off): Terminal, GUI (SDK) or GUI (ACP). It also picks which a GUI tries first for agents that have both (pi, Codex, Claude Code); an agent with only one uses that one.

It takes over from Chat UI (pumi_dev/chat-ui) and the workspace context plugin (amresyd/agents): their options carry over the first time it starts, and Chat UI's open chat tabs each time it starts while Chat UI is off.

A chat over the terminal

Nothing restarts and nothing is screen-scraped. The chat reads the session file the agent writes and types what you send into its terminal, which keeps running underneath. Terminal gives it back exactly as it was.

  • Send while it works: prompts queue in the chat and go in order when the agent is ready. Stop sends Escape. Queued prompts reach Claude Code and pi at their turn's end through the stop hook, rather than typed once the agent is idle.
  • @ for files and tabs: lists the other tabs by title, then the agent's project (.gitignored files left out): its directory, or its session's when the agent runs in the home folder. Pick one to add it to the prompt. A file or a tab in the prompt shows as a chip, and Backspace takes it whole; the agent reads a tab named @tab:%3 with pumi tab read.
  • / for commands: the agent's own, plus Claude's .claude/commands and skills, Codex's prompts, and pi's prompt templates and skills. A command that opens a screen of the agent's own (/model, /resume) shows the terminal.
  • Pictures: drop one on the chat or the prompt to attach it to the next prompt. It stays in the line as a chip, in the prompt and in the sent message; hover it to see the picture.
  • Nothing typed is lost: the draft stays when the tab flips to the terminal and back, and is kept in the tab across a restart of the app. The prompts sent are kept too: ↑ brings them back, even after a session that never started.
  • Continue after a turn that was stopped, failed or cut off goes on without a message to type.
  • Scrolling: the chat follows the answer while you are at its foot and stays where you read when you scroll up; a button takes you back down.
  • Model picker beside Send, for Claude Code and pi.
  • Permission prompts as cards you answer in the chat (Claude's, with "Answer permissions in pumi" on).
  • Edit a message you sent: click it, change it, Enter. pi goes back to that exact message (through a small pi extension the plugin installs on first use, ~/.pi/agent/extensions/pumi-chat.ts); Claude Code and Codex open their own rewind.

The tab's toolbar has the button; Agents: flip the focused tab between the terminal and the chat is in the palette.

GUI tabs (SDK and ACP)

The plugin runs the agent itself, under a small host process per tab (src/session/host.ts), so the agent keeps running when the window closes and the tab finds it again. The host speaks ACP, or the agent's own protocol, and the chat draws what it says as it is written: text token by token, calls with their output, permission requests and an agent's questions as cards, its models in the picker. A tab with a session continues it after a restart. The chat shows at once, while the agent starts: a prompt sent then goes as soon as it is ready.

  • Agents: continue this agent in its GUI hands a terminal agent's session to a GUI tab, once its turn is over.
  • The toolbar's terminal button, or Agents: continue this chat tab's session in the agent's terminal, goes back to the TUI on the same session.
  • ACP adapters that aren't installed are fetched with npx at a pinned version.
  • Questions pi (ask_user_question) and Codex ask are answered in their card: a choice, several, or words of your own.
  • Skills anywhere in the prompt: / after a space lists the agent's skills (pi's own list, Codex's skills/list, Claude's .claude/skills from the project up and in the home folder). A picked skill is a chip; on send the host puts its SKILL.md before the prompt, as pi does for /skill:name.
  • Claude Code's GUI over the SDK runs your installed claude through Anthropic's Agent SDK, signed in as it is. Anthropic asks apps built on the Agent SDK to sign in with an API key; on a Pro or Max sign-in this use may be billed as extra usage. Its row says so while its GUI runs the SDK. The chat over its terminal runs the unmodified claude, which has no such question.

Hooks

The plugin puts each agent's hook into the agent's own config by itself (Claude Code's ~/.claude/settings.json, Codex's ~/.codex/hooks.json, pi's extension ~/.pi/agent/extensions/pumi-context.ts, …) for every agent installed here that isn't Off, and keeps it up to date; it takes it out when the agent is set to Off, or all of them when Connect agents to pumi is off. An agent running then picks the change up when it starts again. The user's own hooks there are left as they are, and the first copy of a config is kept as .pumi-bak. A hook runs only in a terminal opened for its agent, which has PUMI_AGENT set:

pumi call pumi_dev.agents.open claude [cwd]   # an agent tab, as Settings says

An agent's terminal is the plugin's own block, pumi_dev.agents.term, on pumi's terminal engine: a terminal like any other, kept across a restart, drawn as the chat or as the terminal. A shell in a plain terminal that starts an agent is not an agent tab: its hook does nothing there.

The hook tells the plugin the agent's session, model and session file, and its state (the tab's @status); the tab shows the agent's mark (@icon) and, when the tab comes back after a restart, resumes the session (restore_command).

Workspace context

At its start and with each prompt after, an agent is told about the workspace it is in, unseen by you: a short guide to pumi (context/guide.md) and a map of the sessions, views, panes and tabs around it, then only what changed.

<pumi-context from="pumi_dev/agents">
<pumi>
You are in a tab of pumi, a terminal workspace. …
</pumi>
<pumi-panes>
$7 app (current)
  @3 main (current)
    :11 (focused)
      %129 π - Fix the build — ~/src/app — done (current)
    :12
      %130 zsh — ~/src/app/src
</pumi-panes>
</pumi-context>

The map's ids are pumi's: $1 a session, @2 a view, :4 a pane, %3 a tab. (current) marks where you are. A terminal agent hears it through its hook (Claude's additionalContext, pi's hidden message, OpenCode's system prompt); a GUI tab's host puts it ahead of the prompt. After a compaction the agent is told everything again.

For other plugins and agents

An agent tab is one with the plugin's trait, @pumi_dev.agents.agent (the agent's id; its fields .session, .model, .transcript, .accepts, .conversation), and its status is the tab's @status. Every agent tab serves its conversation as pumi_dev.agents.conversation.read(tab, since?), .send(tab, command) and the .changed event; pumi_dev.agents.queue(tab, text) is a prompt for it after its turn, which is how one agent asks another.

Keys

⇧⌘C flips the focused agent tab between its terminal and the chat; change it on the Keybindings page. The rest work wherever the keyboard is in the chat.

Enter, Shift+Entersend; a new line
Escclose the / or @ list, or the edit; else stop the agent while it works
↑ / ↓in an empty prompt, the prompts you sent before, and back
↑ ↓ Tab Enterin the / or @ list, move and pick
⌃1 … ⌃9answer the waiting permission with its first … ninth button

Options

Settings → Agents, one page.

Agentsone row per agent installed here: Off, Terminal or GUI, and whether it is connected
Open agents insets every agent to Terminal, GUI (SDK) or GUI (ACP); a GUI tries this one first (Terminal)
Connect agents to pumithe agents' hooks, put in and kept up to date by the plugin (on)
Answer permission prompts in pumiClaude Code's permission prompts come to pumi; unanswered in two minutes, Claude asks itself (off)
Tell agents about my workspacethe guide and the map (on)

A chat reads the last 4 MB of a session file, and the map has your other sessions' tabs too.

Limits

  • In a chat over the terminal, a reply shows as each message is written to the session file, not token by token, and the mode can't be changed from the chat, nor Codex's model: those pickers are the TUI's.
  • In GUI tabs, going back to an earlier message is the agent's own, in its terminal; and the agent's own list of sessions to pick from is ACP's only.
  • An agent without its hook is taken to be ready 15 s after a prompt, so a queued prompt may arrive while it still works.
  • A hook hands its input to pumi call as one argument: an input past the system's argument limit (128 KB on Linux; a permission to write a big file) fails, and the agent asks in its own prompt.
  • What an agent was told is kept in memory: after the plugin starts again, an agent's next prompt hears the whole map again.

Development

pumi plugin dev agents   # link it, reload on save
bun test                 # the parsers, the conversation model, mentions, commands, keys, the hosts, the hooks, the context

The tests run on synthetic records shaped like each agent's files; no real session is in them. src/conversation/ is the agent-agnostic conversation (the model, its changes, the chat's drawing), src/parse/ turns each agent's session file into it, and src/bridge.ts follows one terminal tab. src/catalog.ts lists the agents and how each opens; src/sessions.ts runs the ACP and SDK tabs, and src/session/ is their host, one engine per protocol. src/hooks/ is the agents' hooks (the table and their files, integrations.ts; what the hook function answers, hook.ts and run.ts), and src/context/ with src/tell.ts is what an agent is told about the workspace.