This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
bb run # start eca-cli TUI
bb run --trust # start with auto-approved tool calls
bb run --eca /path/to/eca # specify ECA binary explicitly
bb run --workspace /path # set workspace root (default: cwd)
bb run --model <model> # specify model
bb run --agent <agent> # specify agent
bb nrepl # start nREPL for development
bb upgrade-eca # download and install pinned ECA binaryIn-app commands (type in input, press Enter):
/model open model picker (Ctrl+L also works)
/agent open agent picker
/new start fresh chat (deletes current chat, clears UI)
/sessions browse and resume previous chats
Requires Babashka 1.12.215+. No separate build step — bb run compiles and runs directly.
ECA server logs go to ~/.cache/eca/eca-cli.log. Tail this when debugging.
eca-cli is a Babashka TUI client for the ECA (Editor Code Assistant) server. It speaks the ECA JSON-RPC protocol over stdin/stdout (same as editor plugins). The LLM pulls context via ECA's built-in tools — the user just sends messages.
ECA process stdout
→ reader thread (server.clj)
→ LinkedBlockingQueue
→ drain-queue-cmd (state.clj) — polls every 50ms
→ :eca-tick message
→ update-state (state.clj)
→ charm.clj render loop
→ view (view.clj)
Responses to requests (e.g. chat/prompt → {chatId, model}) are routed through the queue via a callback that calls .put queue — so all server messages flow through the same path.
program/run drives the TUI with three fns:
state/make-init— returns a fn that spawns ECA, starts the reader thread, returns[initial-state init-cmd]state/update-state— pure dispatch: returns[new-state cmd-or-nil]view/view— pure render: returns a string from state
The drain loop is self-sustaining: :eca-tick handler always returns a new drain-queue-cmd, keeping the queue polled indefinitely. If the cmd fn returns nil, charm.clj drops it — so the tick message is always {:type :eca-tick :msgs [...]} even when the queue is empty.
:connecting → :ready ↔ :chatting ↔ :approving
:connecting— waiting forinitializeresponse:ready— input focused, awaiting user message:chatting— LLM active, streaming; input blurred:approving— tool call waiting ony/n/Y
toolCallPrepare (many, streaming args) → toolCallRun (approval decision point) → toolCallRunning → toolCalled or toolCallRejected
manualApproval: true on toolCallRun triggers :approving mode unless :trust is set or the tool name is in :session-trusted-tools (populated by Y).
No viewport component — view.clj uses manual line-slice: :chat-lines is a flat vec of rendered strings kept in state, rebuilt via view/rebuild-chat-lines after every content change. render-chat slices the last N visible lines adjusted by :scroll-offset.
src/eca_cli/server.clj— process spawn, Content-Length JSON-RPC framing, reader thread, queue drainsrc/eca_cli/protocol.clj— message constructors, request ID tracking, response correlationsrc/eca_cli/state.clj— Elm state machine, ECA content handlers, all key bindingssrc/eca_cli/view.clj— pure rendering: chat lines, tool icons, approval prompt, status barsrc/eca_cli/sessions.clj— EDN persistence of workspace → chat-id mapsrc/eca_cli/upgrade.clj— ECA binary download and version check
--ecaflag~/.cache/eca/eca-cli/eca(eca-cli managed, installed viabb upgrade-eca)which eca(PATH)~/.cache/nvim/eca/eca(nvim plugin, Linux)~/Library/Caches/nvim/eca/eca(nvim plugin, macOS)~/.emacs.d/eca/eca(emacs plugin)
Chat-ids are persisted to ~/.cache/eca/eca-cli-sessions.edn keyed by workspace path. Each startup begins a fresh session — no auto-resume. Use /sessions to explicitly resume a previous chat. /new deletes the current chat and removes its entry from the sessions file.
ECA protocol spec: ../eca/docs/protocol.md