Keel

A code editor for the terminal, with Claude Code as a first-class citizen.

Rust throughout. No webview, no JavaScript at runtime, one process — and about 19 MB of memory.

Why this stack

The webview was 313 MB of the 618, so it went

Keel used to be a Tauri app, and small by desktop standards — 8 MB on disk against Electron's hundreds. Measured while running, it held about 618 MB across three processes, and almost none of it was Keel: WebKitGTK's own web and network processes were 313 MB, Mesa's shader compiler another 48. The editor's code was 5 MB of the total. There is no lighter way to host a webview on Linux — the weight is the engine, and the only way past it is not to have one. The backend stayed, the frontend went, and most of it got smaller.

Shell
ratatui, in cells

The same window the Tauri build drew — menu bar, activity rail, side bar, tabs per pane, status bar — region for region. No webview, and no JavaScript at runtime.

Buffer
ropey + tree-sitter

Eleven grammars compiled in. Only the visible window is highlighted, edits reparse the damaged subtree, and a highlight query reads the rope's chunks rather than a second copy of the text.

Language
Rust throughout

One crate, no IPC boundary, no command layer, no serialisation between halves. The UI calls the backend as ordinary Rust — which is why the port removed 108 command wrappers and 13 event names.

Agent
ACP over stdio

Newline-delimited JSON-RPC to claude-agent-acp. Auth belongs to the agent: Keel never sees a credential.

Everything else
The tools you already have

git rather than libgit2, ripgrep for project search, LSP and DAP over the same framing. Keel shells out to what is installed instead of reimplementing it.

The Claude Code integration

The agent is a tab, not a chat box bolted to the side

Rust spawns the adapter and speaks JSON-RPC over its stdio.

  • Conversations run side by side. Each is a tab with its own transcript, its own half-typed prompt and its own place in the scroll. Start something long, open a second, and work in it while the first runs — a reply arriving in one never waits on another.
  • A phase, not a spinner. ACP reports one bit: the turn is open or it is not. That cannot tell working from blocked, so the panel derives thinking, writing, running a named tool, waiting on you, or running sub-agents — and shows the elapsed time.
  • Waiting is a different shape. Steady amber rather than a pulse. A pulse means "in progress", and a question is the opposite of that.
  • Tool calls are cards. Every call has an id and gets updates against it, so a card is created once and edited in place — a diff for an edit, output as it arrives for a command, and the files it touched as links that open at the line.
  • Steer a turn instead of killing it. Where the agent supports it, Send becomes Steer and the message goes into the turn already running — redirecting work that went the wrong way without losing what it has done.
  • Keel never handles credentials. The adapter reuses the Claude Code CLI's own login. There is no API-key path, nothing to store, and no auto-approval on your behalf.
  • Sub-agents get their own transcripts, because asking for them at the handshake is what stops a Task being flattened into the parent with its prose stripped.
session/update— the stream the panel reads
initialize                 handshake; authMethods lists any sign-in
session/new {cwd}          returns a sessionId
session/list {cwd}         what the agent still keeps for this folder
session/load {sessionId}   replay one into a tab, when asked
session/prompt {sessionId} returns {stopReason} when the turn ends
session/update             notifications streamed during the turn
session/request_permission a REQUEST — the turn blocks
elicitation/create         a REQUEST — the agent asks you
_session/steering          speak into a turn already running
session/set_config_option  the reply is the whole new set

Every message names its session, so every event is
routed by the id it carries rather than landing in
whichever transcript is on screen.

Verified against @agentclientprotocol/claude-agent-acp
0.74.0 with a real prompt: streaming deltas,
stopReason: "end_turn"

A turn ends with a line saying how long it took, what it cost, and — when it was not the ordinary ending — why it stopped. Cancelled, refused and token-capped turns used to be indistinguishable from a finished one: the answer simply stopped.

What else is in there

An editor first, and the agent's neighbours second

Everything below is built, running and documented.

Conversation history

Search every Claude Code transcript on disk — every project, every session — by text, tool call, thinking or sub-agent. It opens onto a list of recent conversations rather than a blank box.

Walked per query, newest file first. Nothing is written down: an index over the transcripts would be the same mistake against a different directory.

Queued and scheduled tasks

Line up six prompts, press play, and go and do something else. Each one opens an ordinary conversation — same transcript, same permission cards, same find bar.

The queue never restores itself running: reopening a project is not consent to start spending.

Several accounts, all at once

CLAUDE_CONFIG_DIR relocates the CLI's whole config root, so two agents pointing at one binary with different config directories are two accounts.

Each keeps a process and its own conversations, so switching is instant: the turns you leave behind go on running, and the replies are waiting when you come back.

Nothing indexed up front

A directory is read when you expand it and not before, and the walk stays out of node_modules, .git and target. The watcher is told exactly the directories the tree has opened, never recursively.

That is also why there is no fuzzy file finder: fuzzy matching wants the whole list up front, which is the background walk this avoids. Project search goes through ripgrep, which walks without keeping the result.

Source control, over real git

Stage by file or by hunk, commit, fetch, pull and push from the pane, with the diff over the editor and blame in the gutter. A forced push is --force-with-lease, never a bare --force.

Changed lines come from the hunk bodies rather than the headers — git pads each hunk with context, so in a small file a header can cover the whole thing.

Language servers and a debugger

rust-analyzer, typescript-language-server and pyright if they are installed, with diagnostics in the gutter, go-to-definition and Format Document. The server hears every edit, once per frame.

Harper runs beside them for grammar — the prose in comments and Markdown, never the code — and each server's findings replace only its own.

Keybindings

The ones worth knowing

One registry, two views: the palette flattens it and the menu walks it, so the two cannot disagree about what exists — and the shortcut page is generated from it rather than written. A command that cannot run right now is still listed, dimmed, with the reason where its key would go. The full table is in the docs.

Ctrl+KThe command palette — every command, by name
F10The menu bar, over the same registry
F1Claude Code, as a tab
F3The conversation history
F5The task queue
F2 F4 F9Source Control, Run and Debug, Search
F7 Shift+F7Split beside, split below
Ctrl+E Ctrl+XCopy and cut — not Ctrl+C, which is the interrupt
Ctrl+DAdd a caret at the next occurrence
Shift+Alt+→Expand the selection to the enclosing syntax node
Ctrl+PMarkdown preview; with Shift, beside the source
F12Go to definition