A phased plan to make eca-cli a minimal, reliable, pi-like TUI client for ECA server.
Read assessment.md for the philosophical grounding behind this roadmap.
| Phase | Name | Focus | Status | Detail |
|---|---|---|---|---|
| 1a | Reliable Core | Everything works, nothing leaks | ✅ Complete | detail |
| 1b | Login Hardening | End-to-end auth with real credentials | ✅ Complete | detail |
| 2 | Model & Agent Identity | Know what you're running, change it | ✅ Complete | detail |
| 3 | Session Continuity | Quit and resume, start fresh | ✅ Complete | detail |
| 4 | Command System | Slash commands as the extensibility seam | ✅ Complete | detail |
| 5 | Rich Display | Expandable tool blocks, thinking, sub-agent nesting | ✅ Complete | detail |
| 6 | Tool-Call Diff Display | Render file-edit tool calls as unified diffs | ✅ Complete | #25 |
| 7 | MCP Integration | Status indicator, details panel, server update notifications | ✅ Complete | — |
| 8 | Markdown Rendering | md→ANSI; url/OSC-8; wide-table horizontal-scroll |
⬜ Unbuilt | — |
| 9 | Message Steering | Influence a running prompt | ⬜ Unbuilt | — |
| 10 | Server-Driven Interaction | chat/askQuestion, chat/queryCommands autocomplete, log viewer |
⬜ Unbuilt | — |
| 11 | Power Features | @-refs ✅ · jobs ✅ · rollback + fork remain |
🟡 Partial | — |
| 12 | Context Expansion | Full chat/queryContext picker (dirs/URLs/cursor/repoMap/MCP-resources); image-attach |
🆕 Planned | — |
| 13 | Chat Management | Rename + per-chat trust, server-side clear, chat/statusChanged, history pagination |
🆕 Planned | — |
| 14 | Chat Navigation | Flags/bookmarks, timeline jump, export transcript | 🆕 Planned | — |
| 15 | Task Panel | TaskDetails — checklist, N/M progress, priority, blockedBy graph |
🆕 Planned | — |
| 16 | Granular Approval | Per-shell-command approve & remember (save:'session', approvalKey) |
🆕 Planned | — |
| 17 | MCP Write Management | MCP CRUD + start/stop/enable/disable/logout, OAuth (L) | 🆕 Planned | — |
| 18 | Providers & Config | Providers panel (auth, logout, cost badges); in-app global-config editor | 🆕 Planned | — |
| 19 | Variant Picker | Interactive model/agent variant selector | 🆕 Planned | — |
| 20 | Context Breakdown | usage.contextBreakdown segmented bar + autoCompactPercentage threshold |
🆕 Planned | — |
| 21 | Multi-Session & Remote | Multi-workspace/concurrent servers; remote ECA host connect (L) | 🆕 Planned | — |
Status legend: ✅ Complete · 🟡 Partial · 📋 Spec'd (issue open) · ⬜ Unbuilt · 🆕 Planned (numbered, not yet detailed).
Phase numbering is frozen identity — shipped phases are marked done in place, never renumbered; all new work is appended as 12+. This is a stable-identity roadmap, not a reordered one. The frozen reconciliation (drift corrections, scope cuts, phase 12–21 assignment) was decided in #24 — a closed decision record. This file is the live ordered plan; #24 is not edited.
Separate axis from the frozen numbering above. Principle: ECA-checklist-required first → broad-value → L-sized speculative phases to the tail, with one named value-exception. Re-weigh here (low churn) as priorities shift — do not touch #24.
- Next:
6 Diff→8 Markdown(value-exception — highest daily-visibility wart, promoted above required items) →12 Context Expansion - Near:
9 Steering→10 Server-Driven→11 Rollback/Fork - Later:
15 Task Panel·16 Granular Approval·20 Context Breakdown·19 Variant·13 Chat Mgmt·14 Chat Nav·18 Providers & Config - Deferred (L):
17 MCP Write Management·21 Multi-Session & Remote
Out of scope for a standalone TUI client (need a host buffer/cursor/LSP feed eca-cli lacks — see the standalone TUI client vs editor client distinction in CONTEXT.md): rewrite/prompt (in-buffer rewrite), completion/inline (ghost text), editor/getDiagnostics (client-supplied diagnostics), inline image render (kitty/iterm/sixel — GUI-bound). Repeat/resend prompt is covered by ↑/↓ input-history recall.
Tracked items that aren't tied to a phase but should be addressed at some point.
Production-ready distribution via bbin
When eca-cli is ready for end-users (not just developers cloning the repo), it should be installable via bbin install so the standard install path is one command, not "clone, install Babashka, run bb upgrade-eca, run bb run". Action items:
- Audit
bb.ednfor bbin compatibility. The:tasksmap needs at least a default task with stable CLI args sobbin installcan wireeca-clias a script-on-PATH. Reference: bbin docs on script installation. - Tighten config + cache paths to follow XDG conventions. Currently
~/.cache/eca/eca-cli-chats.edn,~/.cache/eca/eca-cli.log, and~/.cache/eca/eca-cli/eca(the upgrade-eca-managed binary) are hardcoded under~/.cache/eca/. For bbin distribution we should:- Respect
XDG_CACHE_HOME(cache files) andXDG_CONFIG_HOME(any future config) when set, falling back to~/.cacheand~/.config. - Centralise path resolution in one helper (probably
sessions.cljor a newpaths.clj) so future additions don't drift.
- Respect
- Tighten logging. ECA server stderr currently goes to
~/.cache/eca/eca-cli.log. For installed users we should:- Document the log path in
--helpoutput and in startup banners ("Logs: $XDG_CACHE_HOME/eca-cli/server.log"). - Add log rotation or size-cap (currently the log grows unbounded).
- Ensure the log directory is created on first run rather than failing silently.
- Document the log path in
- Document install via bbin in README — assuming successful audit, the canonical install becomes
bbin install io.github.editor-code-assistant/eca-cli.
Trigger: post Phase 7 (MCP) — now reached (Phase 7 shipped); the feature surface is stable enough to start treating eca-cli as something end-users would install, and the Phase 10 in-app log viewer is also a good moment to revisit log paths.
Goal: Every part of the fundamental chat loop works correctly and visibly. ECA's full initialization lifecycle is handled. No credentials required.
See roadmap/phase-1a-reliable-core.md for the full implementation plan, tests, and stopping criteria.
Goal: The login flow works end-to-end against real providers. Requires working credentials.
See roadmap/phase-1b-login-hardening.md for the full implementation plan, tests, and stopping criteria.
Goal: The user knows exactly what model and agent they're running, and can change either without restarting.
Model list from config/updated.
Store the available models and agents received from ECA. These are the options for switching.
Model picker (Ctrl+L / /model).
A simple inline selector showing available models. Selecting one sends chat/selectedModelChanged to ECA and updates the status bar. This mirrors pi's Ctrl+L.
Agent picker (/agent).
Same pattern for agents. Selecting one updates state and the status bar.
Status bar model/agent display.
The status bar should clearly show model • agent (or just model if no agent) so the user always knows their current context.
chat/selectedModelChanged and chat/selectedAgentChanged notifications.
These are client notifications (no response expected) that inform ECA of the user's selection. The next chat/prompt will use the selected model/agent.
config/updatedis handled and model/agent lists are stored in state- Ctrl+L opens a model selector; selecting a model updates state and notifies ECA
/agentopens an agent selector- Status bar shows current model and agent at all times
- Model change takes effect on the next sent message
Goal: Smooth everyday interaction — not a planned phase, delivered incrementally.
alt-screen true— fullscreen mode; native mouse selection (Shift+drag) works out of the box- Mouse wheel scroll — DECSET 1000 mouse reporting lets the app own scroll; 3 lines per tick
- Input history — Up/Down in
:readyrecalls previously sent messages - PgUp/PgDn — full-page scroll through chat history
- Integration tests for all of the above (Phase 3 tests in
integration_test.clj)
Goal: Quit and come back. Start fresh. Know which chat you're in.
See roadmap/phase-3-session-continuity.md for the full implementation plan, tests, and stopping criteria.
Own ECA binary (bb upgrade-eca).
Download and manage a pinned ECA binary at ~/.cache/eca/eca-cli/eca, independent of editor plugins. Discovery order updated to prefer this over nvim/emacs locations. Startup version check warns if the running binary doesn't match the pinned version.
Chat-id persistence.
After first exchange, write chat-id to ~/.cache/eca/eca-cli-chats.edn keyed by workspace path. On restart, read it and resume silently — no prompt, no flag.
chat/opened and chat/cleared handlers.
chat/opened is the canonical notification when a chat is created or replayed — store chat-id and title. chat/cleared signals the server wants us to wipe local items (used before replay and after /new).
/new command.
Clears the chat-id, deletes the chat on the server, wipes local items, removes from disk. Next message starts a fresh session.
/chats command.
Calls chat/list, shows a picker of available chats. Selecting one sends chat/open — server replays the chat via chat/cleared → chat/opened → chat/contentReceived.
Chat title in status bar.
Show the current chat title (truncated) from chat/opened.
- Chat-id persisted; restart in same workspace resumes automatically
chat/openedstores chat-id and title;chat/clearedwipes local items/newstarts a fresh chat (old messages gone, new chat-id on next send)/chatsshows selectable list of previous chats and opens chosen one- Status bar shows current chat title when available
Goal: A slash command system that is the primary extensibility seam for eca-cli.
Command registry.
A simple map of command name → handler function. Commands are triggered by typing /name in the input and pressing Enter. The registry is populated at startup and is the single place to add new commands.
Command autocomplete.
When the user types /, show a list of available commands (filtered as they type). This replaces the raw text input with a command picker temporarily, returning to normal input on selection or Escape.
Built-in commands.
| Command | Action |
|---|---|
/model |
Open model picker (Phase 2) |
/agent |
Open agent picker (Phase 2) |
/new |
New chat (Phase 3) |
/chats |
Chat browser (Phase 3) |
/clear |
Clear chat display (local only, no ECA call) |
/help |
Show available commands |
/quit |
Exit cleanly |
/login command.
Manually trigger the login flow for a provider (calls providers/list then login). Useful when tokens expire mid-session.
- Typing
/shows a filtered list of available commands - All built-in commands from the table above are wired and functional
/helplists all registered commands with descriptions- Adding a new command requires only registering it in the command map — no other changes
Goal: Replace the flat text-only chat display with structured, interactive blocks — collapsible tool calls, thinking blocks, and nested sub-agent content — matching the UX fidelity of nvim's ECA buffer.
See roadmap/phase-5-rich-display.md for the full implementation plan, tests, and stopping criteria.
Extended item model. Tool-call items gain :args-text, :out-text, :expanded?, and :sub-items. A new :thinking item type stores model reasoning blocks.
Collapsible rendering. render-item-lines dispatches collapsed (1-line) vs. expanded (args + output block) per item type. Collapsed by default. eca__spawn_agent shows ▸ N steps when it has nested sub-agent content.
Thinking blocks. ECA's thinking content type is captured and displayed as ▸ Thought (collapsed) / ▾ Thought + text (expanded). Currently ignored.
Sub-agent nesting. Replaces the interim parentChatId suppression. Sub-agent contentReceived is routed to the parent eca__spawn_agent tool call's :sub-items via the subagentChatId protocol link, and rendered indented under it when expanded.
Tab focus + Enter toggle. Tab/Shift+Tab navigates between focusable items (tool-call, thinking). Enter/Space toggles :expanded?. Escape clears focus. Scroll adjusts to keep the focused item visible.
- Collapsed tool-call renders 1 line; expanded shows args + output block
- Thinking content creates a
:thinkingitem; expands to show model reasoning - Sub-agent content appears nested under
eca__spawn_agent, not suppressed or in main flow - Tab focuses next tool-call/thinking item; Enter toggles; Escape clears focus
- Scroll adjusts to keep focused item visible
Goal: When a tool call modifies a file, the expanded view should render a unified diff so the user can see exactly what changed before approving or after the call completes.
Full spec: #25. The summary below is corrected against it — the ticket is authoritative.
The server computes the diff — eca-cli colourises, does not compute. ECA ships a pre-computed unified diff in the tool-call fileChange detail ({type, path, diff, linesAdded, linesRemoved}), populated at toolCallRun (approval) through toolCalled. eca-cli must not extract before/after, run an LCS, or match tool names. Detection is by payload presence (details.type == "fileChange"), not tool name — automatically covering edit_file/write_file/apply_patch/future tools.
Diff colouriser leaf. A pure render-diff in blocks.clj colours the server's diff string by leading char: - red, + green, @@ dim, context grey. Reads on light + dark terminals. >500 rendered lines truncate with [truncated]. Collapsed one-liner shows +N −M.
Two mount points, one renderer. Pre-work F01 unifies the approval surface onto render-item-lines (force-expanded) + the [y/n/Y] line; F02 classifies the fileChange detail into an :edit key at ingest. The diff then appears in both the approval prompt and the completed block for free. Non-edit tool calls fall back to the existing arguments/output renderer.
- File-edit tool calls render as a unified diff in the expanded block
- Diff colours work in light and dark terminals
- Approval mode shows the diff before the y/n/Y prompt
- Non-edit tool calls fall back to the existing output renderer
- Large diffs (>500 lines) truncate with a "[truncated]" footer
Goal: First-class MCP server support — users see which MCPs are running, can drill into details when one fails, and react to server status changes without restarting.
Status indicator.
The status bar shows MCP health at a glance: MCPs: 3/4 ✓ or MCPs: 2/4 ⚠ if one has failed. Hidden when no MCPs are configured.
/mcp panel.
A new picker-style overlay listing each configured MCP server: name, status (running / failed / pending / requires-auth), exposed tools count, last error message if failed. Selecting an entry expands its details. Escape closes the panel.
tool/serverUpdated notification handler.
ECA emits status changes for MCP servers. The handler updates the in-memory MCP map, refreshes the status bar, and updates the panel if open.
mcp.clj namespace.
New ns for MCP state, notification handler, and /mcp command handler. Pattern matches login.clj / picker.clj.
- Status bar shows MCP count and aggregate health when MCPs are configured
/mcpopens a panel listing servers with status, tool count, and any errortool/serverUpdatednotifications update the panel and status bar live- Failed MCPs surface their error message in the panel
requires-authMCPs surface a hint to run the relevant login flow- Panel keybindings: arrow nav, Enter to expand, Escape to close
Goal: Render assistant and user text through a markdown→ANSI converter so that bold, italic, code spans, fenced code blocks, headers, lists, and tables display as formatted output rather than raw syntax.
Markdown→ANSI converter.
A lightweight pass over text items before they are split into lines. Outputs ANSI escape sequences for bold (\e[1m), italic (\e[3m), dim (code spans), and resets. Fenced code blocks get a visual border and a language label. Headers are bold. Lists indent correctly. Tables render with column alignment.
Library evaluation.
Babashka can load Java libraries via :mvn/version. Candidates: commonmark-java (CommonMark spec-compliant, extensible) and flexmark-java (fast, configurable). Evaluate for binary size, Babashka compatibility, and ANSI output support. If neither fits, a purpose-built single-pass tokenizer covering the 80% case (bold, italic, code, headers, lists) is the fallback.
Scope.
Applies to :assistant-text and :user items. Tool output (:out-text in tool-call items) and thinking text are rendered plain — they are code/prose from tools, not model-authored markdown.
url content type.
With markdown rendering in place, url items ({:type "url" :title "..." :url "..."}) are a natural fit here: render as title (url) inline, or as an OSC 8 hyperlink where the terminal supports it.
- Assistant text with
**bold**renders with ANSI bold, not literal asterisks - Fenced code blocks display with a border and language label
- Lists indent correctly; headers are visually distinct
urlcontent items render as linked text in chat- Plain-text fallback if the markdown library is unavailable at runtime
Goal: Send messages to influence a running prompt without stopping it — the eca-cli equivalent of pi's message queue.
Message queue.
While in :chatting mode, pressing Enter queues the typed message rather than discarding it. The queue is stored in state. A visual indicator (e.g. [1 queued] in the status bar) shows pending messages.
Steering (chat/promptSteer).
The queued message is sent as a steer at the next tool-call boundary via chat/promptSteer. ECA injects it into the running LLM turn. If the prompt finishes before the steer is consumed, the message is sent as a regular chat/prompt instead.
Escape behaviour update.
Escape in :chatting mode with no chat-id: return to :ready (current behaviour). Escape with a chat-id: stop the prompt via chat/promptStop and restore queued messages to the input, as pi does.
Follow-up mode.
Alt+Enter queues a follow-up message, delivered only after the agent fully completes (i.e., sent as chat/prompt once progress: finished arrives).
- Typing while the agent is working queues the message with a visual count indicator
- Queued messages are delivered as steers at the next tool boundary
- Escape with a running prompt stops it and restores queued messages to the input
- Alt+Enter queues a follow-up delivered after completion
- No message is ever silently lost
Goal: Handle server-initiated dialogue and server-side metadata that wasn't covered earlier — chat/askQuestion, server-supplied slash commands via chat/queryCommands, and an in-app log viewer for users who don't tail files directly.
chat/askQuestion handler.
ECA server can send a chat/askQuestion request with a question, optional predefined options, and an allowFreeform flag. eca-cli must:
- Pause the chat display and show the question prominently
- If options are provided, show a numbered/lettered selector
- If
allowFreeformis true, also allow typing a custom answer - Send the response back via the JSON-RPC response (it's a request, not a notification)
- Resume the chat display
:asking mode.
A new state mode, similar to :approving, that takes over the input area with the question UI. The text input handles freeform answers; digit/letter keys select predefined options; Escape sends a null/cancelled response.
Question display in chat.
The question and the user's answer should appear in the chat history as a distinct content type (e.g., :question item), so the exchange is readable in context.
chat/queryCommands server-side autocomplete.
Phase 4 built a local registry. ECA also exposes server-side commands via chat/queryCommands (e.g. agent-defined slash commands). When the user types / and the input picker opens, query the server in addition to the local registry and merge results.
In-app log viewer (/logs).
A panel that reads ~/.cache/eca/eca-cli.log and shows the tail with auto-scroll, primarily for users / contributors who don't know to tail the file directly. Read-only. Keyboard scrolling, Escape to close. Useful when something goes wrong and the user can't be expected to know the path.
chat/askQuestionrequests are handled without hanging the reader thread- Options are presented and selectable by key
- Freeform input works when
allowFreeformis true - Escape sends a cancelled response (ECA handles gracefully)
- Question and answer appear in the chat history
/-autocomplete includes both local and server-side commands, deduplicated/logspanel shows recent log lines and scrolls
Goal: Surface the remaining ECA capabilities that reward power users — context injection, background jobs, and session surgery.
Status: 🟡 Partial. @-file context injection and the jobs panel have shipped to main; /rollback and /fork remain.
Context injection (@ file references). ✅ Shipped.
Typing @ in the input triggers a fuzzy file search (using chat/queryFiles) and inserts the selected file as a context in the chat/prompt call. This mirrors pi's @ shortcut exactly. The context appears as a reference in the user message display.
Background jobs panel. ✅ Shipped.
ECA tracks long-running background jobs (dev servers, watchers, etc.) via jobs/list and jobs/updated. eca-cli shows a compact jobs indicator in the status bar (e.g., [2 jobs]) that the user can expand into a panel listing job names, statuses, and elapsed times. Killing a job (jobs/kill) is possible from the panel.
Chat rollback (/rollback). ⬜ Remaining.
A command that shows the chat history and lets the user pick a message to roll back to. Sends chat/rollback to ECA. ECA sends chat/cleared followed by the kept messages, which eca-cli re-renders. This is the eca-cli equivalent of pi's /tree for branching.
Chat fork (/fork). ⬜ Remaining.
Fork the current chat at a selected message. Sends chat/fork to ECA, which creates a new chat. eca-cli receives chat/opened with the new chat-id and switches to it. The forked chat appears in /chats.
Numbered identities frozen in #24, not yet spec'd into detailed tickets. One-liners below; each gets a full "what to build" + stopping criteria when it's spec'd into an issue. See Execution order for scheduling.
Goal: Full chat/queryContext picker — directories, URLs, cursor, repoMap, MCP resources — beyond the file-only queryFiles shipped in Phase 11. Plus image-attach (ImageContext).
Goal: Rename + per-chat trust (chat/update); server-side clear (chat/clear); chat/statusChanged; history pagination (chat/history cursors, "load older").
Goal: Flags/bookmarks (chat/addFlag/removeFlag); timeline (jump to message/flag); export transcript.
Goal: TaskDetails — checklist, N/M progress, priority, blockedBy dependency graph.
Goal: Per-shell-command approve & remember (save:'session', approvalKey), beyond the current tool-name-only approval.
Goal (L): MCP CRUD + start/stop/enable/disable/logout, OAuth beyond connect, tool/serverRemoved. Phase 7 delivered read-only MCP (part 1).
Goal: Providers panel (auth status, providers/logout, model + cost badges); in-app global-config JSONC editor.
Goal: Interactive model/agent variant selector (baseline today: status-bar display only).
Goal: usage.contextBreakdown segmented bar + autoCompactPercentage threshold + legend (baseline today: bare context-%).
Goal (L): Multi-workspace / concurrent servers, cross-session list, workspace switcher; remote ECA host/port connect + discovery.
@in the input opens a file picker; selected file is attached to the next prompt- Status bar shows a job count when background jobs are active
- Jobs panel lists running jobs with elapsed time; kill works from the panel
/rollbacklets the user pick a message and rolls the chat back to that point/forkcreates a new chat from the current one and switches to it