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
- Buffer ropey + tree-sitter
- Language Rust throughout
- Agent ACP over stdio
- Everything else The tools you already have
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.
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.
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.
Newline-delimited JSON-RPC to claude-agent-acp. Auth belongs to the agent: Keel never sees a credential.
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.
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.
Keel