Plan: Milestone 12 - Agents and a Native Terminal (from Prompt20).
Steps 1–7 done (2026-09-21) — 5 new browser suites / 36 in the batch, 104 native tests, clippy clean
Claude Code is a provider: Settings → Language model → Claude Code (subscription, no API key) spawns the
claudeCLI headless in the open folder for every turn (stream-json,--resumefor continuity,permission_modeplanby default), its tool activity shows as▸ Read src/main.rslines, and a mock CLI covers it in tests. Agent sessions on the server (agent.on_server): a turn runs on the server to the end whether or not a window is open; the transcript is on disk under.moonkale/agent-sessions/; any client that connects later — the phone included — lists the sessions, reads the transcript and answers a pending approval. Connect to Server… on desktop and mobile (URL + token through the client relay). Terminal (Rust): a second terminal extension with no JavaScript of ours —vt100grid, Dioxus rows, keys in Rust, colours, scrollback, Ctrl+click links — opt-in; with both enabled New Terminal asks which (orterminal.implementationdecides). Extensions has its own panel and activity-bar entry.
Steps as executed
| # | step | outcome | notes |
|---|---|---|---|
| 1 | ui/extensions_panel.rs: ExtensionsExtension (activity puzzle, order 910), the list shared with Settings → Extensions (ExtensionsList) | ✅ extensions.mjs: rail entry, panel lists tiers, toggling Flow persists, view.panel.extensions in the palette | ui.md |
| 2 | moonkale-llm feature claude-code: claude_code.rs (spawn, stream-json → events, per-folder (session id, turns) → --resume, 10-min turn timeout, readable errors), Request.cwd, LlmSettings.options, ProviderKind::ClaudeCode; Settings UI (command, permissions, allowed tools); the server jails cwd; tests/mock-claude.sh speaks the protocol | ✅ 2 unit tests (mapping the probed shapes; resume/restart), 3 mock-CLI tests, claude-code.mjs on the server (▸ line, reply, cwd, permission mode, resume) | llm.md; shapes probed against claude 2.1.278 with one real call |
| 3 | api::agent_sessions: records in memory + <folder>/.moonkale/agent-sessions/<id>.jsonl, a turn per thread (Agent::send with a server tool host over the MCP read-only tools; approvals wait for a client, allow_writes auto-yes, 10-min decline), server functions agent_list/send/events/approve; moonkale_llm::sessions wire types; ext-api::AgentSessions + agent.on_server; agent/server_panel.rs (send, poll, sessions list, approvals) | ✅ agent-server.mjs: turn runs and is polled in, tool call, page closed → a new page lists the session and shows the transcript, mutating call waits for an approval, Deny reported | api.md, editor-agent.md |
| 4 | Connect to Server…: ServerClient in WorkspaceConfig, Workspace::{connect_server, disconnect_server}, server_link, ui/server_dialog.rs, File menu + palette + status item; desktop and mobile (the phone gets api + the relay, server-side shells and git, agent sessions) | ✅ ext-api/tests/remote_flow.rs::connect_server_… (VirtualDom harness: connect, root opened and recorded, recents untouched, disconnect closes); the phone build compiles, not installed on the device this time. Installed and verified 2026-09-22: the dialog’s connect never ran (P-116, the P-108 pattern again); fixed, ui/tests/server_dialog.rs covers the dialog itself, and the phone then opened the LAN server’s folder | desktop.md, mobile/README.md |
| 5 | editors/terminal-native: keys.rs (DOM key → bytes), panel.rs (vt100::Parser per session, rows of styled runs, cursor, 16-colour classes + 256/RGB inline, cell size from a 10-M probe via get_client_rect, onresize, wheel scrollback, Ctrl+click on a row → links::find → open) | ✅ terminal-native.mjs: Ctrl+opens it, prompt renders,echooutput,\e[31m→.mk-tn-fg1, bold, Ctrl+click opens src/main.rs`; 1 unit test for the key table | editor-terminal-native.md |
| 6 | Command::NewTerminalIn(&str), terminal.implementation (ask | xterm | native; Settings → Terminal), the frame resolves NewTerminal, ui/terminal_chooser.rs with remember; both panels answer NewTerminalIn | ✅ last step of terminal-native.mjs (chooser → Rust); terminal.mjs unchanged and passing | P-107, P-108 below |
| 7 | verify, log, vault, catalogue | ✅ full batch 36/36; this note; Extension Catalogue (two new rows, the provider) |
What the user sees
- Settings → Language model → Provider: Claude Code (subscription, no API key). Command (empty =
claudeonPATH), Permissions (plan— read and propose only;default;acceptEdits;bypassPermissions), Allowed tools. The Agent panel header readsclaude-code · subscription. A turn shows▸ Read …,▸ Edit …lines as the CLI works, then its answer; the next message resumes the same CLI session. On the web the CLI runs on the server, in the server’s folder. - Run agent turns on the server (Settings → Language model): the Agent panel gets a session picker (
●marks a running one) and New; a turn keeps running after the window closes; reopening (or connecting from the phone) shows where it is; a tool that needs approval waits for anyone connected to answer. Off by default on desktop and web (the local agent stays as it was); the phone always uses server sessions when connected. - File → Connect to Server… (desktop, phone): URL + token; the server’s folder opens;
⇅ urlin the status bar; Disconnect Server returns to local sources. - Extensions in the activity bar (below Settings): the same list as Settings → Extensions, on its own page, with the catalogue link.
- Terminal (Rust): enable it in Extensions; View → New Terminal / Ctrl+
then asks *xterm.js* or *Rust* (tick *remember* or set Settings → Terminal → Implementation). The Rust panel: prompt, colours, bold/italic/underline/inverse, scrollback with the wheel, Ctrl+click onpath:line` opens the file, focus ring when it has the keyboard.
Deviations from the plan
- Server sessions are opt-in (
agent.on_server, default off), not automatic whenever the sources are a server’s: the web client’s local agent has features the server host does not (editor writes, wasm tools, Activity log, Save as page), and four browser suites rely on them. The phone, which has no local provider, always uses server sessions. - The server tool host is read-only (the MCP tools over the registry) plus approval-gated calls that then fail with “not available in a server session” for editor/wasm tools — approvals are exercised, edits are not. Claude Code as the provider edits through its own tools in the server’s folder, which is the case the prompt asked for.
- Claude Code’s permission prompts are not routed through Moonkale’s gate (
--permission-prompt-tool, Level 3 of Claude Code Extension as written):permission_mode+allowed_toolsdecide; the CLI runs its own tools under its own rules. The IDE bridge (Level 2) is untouched. claude-codecontinuity is--resumewhen the chat grew by one user message; a cleared or restarted chat starts a new CLI session with the earlier turns rendered into the first prompt.- The native terminal has no mouse selection, no paste, no search, no IME yet; Ctrl+Shift combos are left to the browser (copy/paste); dead keys are ignored. It is opt-in and the chooser names xterm.js first.
- The Android build was compiled for the host only in this session; on 2026-09-22 the release APK went onto the phone and Connect to Server… turned out to be dead there and on desktop (P-116) — one
spawnthat should have beenspawn_forever. Theremote_flow.rstest exercisedconnect_serverdirectly and never the dialog; the new harness test drives the dialog’ssubmitevent. - One real call to
claudewas made to capture the stream-json shapes (a two-word prompt,--max-turns 1); everything else runs against the mock.
Problems hit (→ Problem Log)
- P-107 two
dispatches in one tick: the commands signal holds the latest command only, so show the panel + new terminal lost the first —open_terminal_inshows the panel, yields two renders (YieldNow), then dispatches. - P-108 a task spawned from a dialog’s click handler is owned by the dialog’s scope and cancelled when the dialog closes —
spawn_foreverfor work that outlives its dialog. - P-109
Signal<Vec<Rc<RefCell<…>>>>and#[component]props: the component macro derivesPartialEqon every prop;Rc<RefCell<NativeSession>>needed a by-idPartialEq,AgentSessions(fn pointers) a constant one. - P-110 text replacements after
cargo fmt: two edits silently missed their target because rustfmt had reflowed the lines in between — theon_servergate and the terminal routing both shipped once as no-ops before the browser suites caught them.
Numbers
- New code:
terminal-native520 lines,claude_code.rs500,agent_sessions.rs470,server_panel.rs330,extensions_panel.rs190, dialogs 150. - Tests: 104 native (workspace, +12), 36 browser suites (+5:
extensions,claude-code,agent-server,terminal-native, and theremote_flowharness gained the server-connect case). - The mock
claudeanswers in 10 ms; the real one took 2.6 s of API time for “ok” (27 592 cache-creation tokens: the CLI’s system prompt).