Goal
Add two workspace tools to the AgentNet UI: a terminal and a retro-style file explorer. Both must work on every surface that renders the webview (localhost browser + Android), with vscode/cli out of scope for v1.
Why this is cheaper than it looks
The hard parts are already solved by the existing architecture:
- Shared UI.
surfaces/webview is the single React UI rendered everywhere. Build a screen once → it appears on desktop and Android.
- Shared wire. One transport (
POST /rpc for commands, GET /events SSE for streams), defined in webview/src/transport/protocol.ts and served by surfaces/localhost/src/index.ts.
- Real runtime already on device. Android runs the same localhost node server inside a proot Ubuntu guest (
surfaces/android ships proot-arm64; ServerService.kt keeps "the node server" alive). So a real shell + a real glibc filesystem already exist on the phone. We are not porting a shell — it is already there.
Net: implement RPC handlers in localhost/index.ts + screens in webview → Android inherits both for free (it reuses the localhost surface verbatim). vscode would need its own handlers later (or just reuse its native terminal/explorer).
UI placement (decided)
The bottom nav is a fixed 4-domain pager — Chat · Skills · Agent · Market — with hard-coded tab-width math (--an-tab-w) and LAST = 3 in App.tsx. Adding terminal/files as 5th/6th tabs breaks that math and crowds the mobile bar, and they are not top-level domains.
Terminal and Files are tools of the active session's workspace (the cwd the agent is operating in). They belong attached to Chat, which already owns activeSessionId.
Placement: two icon buttons in the Chat header (chat/ChatScreen.tsx, the right-side <div className="flex items-center gap-1 shrink-0"> group, before StatusBadge). Tapping opens a full-screen overlay scoped to the active session's cwd — back-swipe / X to close. Same overlay reads as a side panel on a wide desktop browser. No change to the pager or tab bar.
Chat header: [☰ chats] Title / addr … [▮ term] [▤ files] [status]
└ open full-screen overlay (active session cwd)
Phase 1 — Retro file explorer (EASY, ~1 day, low risk)
protocol.ts — new messages:
- client:
{ type: "fs.list"; path: string }, { type: "fs.read"; path: string }
- server:
{ type: "fs.listed"; path; entries: { name; kind: "dir"|"file"; size }[] }, { type: "fs.file"; path; content; truncated }
localhost/index.ts — handler: fs.readdir(path, { withFileTypes }) + stat; readFile capped (~256 KB). Normalize + scope every path to the runtime workspace root; reject .. escapes.
webview/src/files/FilesOverlay.tsx — lazy tree (load a dir's children only on expand — proot fs is slow, no recursive walk). Retro look = pure CSS (monospace, box-drawing chars, green-on-black). Read-only in v1.
ChatScreen.tsx — Files icon button → open overlay.
Phase 2 — Terminal (MEDIUM, ~2–3 days)
protocol.ts — new messages:
- client:
{type:"term.open"; cols; rows}, {type:"term.input"; id; data}, {type:"term.resize"; id; cols; rows}, {type:"term.close"; id}
- server:
{type:"term.data"; id; data}, {type:"term.exit"; id; code} (data streams over the existing SSE channel; input arrives via POST)
localhost/index.ts — spawn a shell:
- v1 (lazy):
child_process.spawn(shell, { stdio: pipe }) — pipe stdin/stdout. Works for line commands; no full-screen TUIs (vim/htop). // ponytail: pipe shell, swap to node-pty if a real PTY is needed.
- v2:
node-pty for a true PTY. Risk: native arm64 build inside proot (the "mass file churn" tax noted in guest/AGENTS.md). Ship v1 first.
webview/src/term/TermOverlay.tsx — @xterm/xterm + fit addon (one new dep). Wire to term.* messages.
ChatScreen.tsx — Terminal icon button → open overlay.
Risks / decisions
- node-pty on arm64/proot → ship the pipe-shell terminal first; add real PTY only if TUIs are needed.
- proot fs is slow → explorer lazy-loads per directory, never recurses the whole tree.
- Security → both handlers run inside the existing loopback-only server ("nothing leaves the device"). File explorer path-scopes to the workspace root. Terminal = full shell, but that is the same trust boundary the agent already operates under in proot; keep it loopback-only and behind the existing wallet-gated session.
- Scope → localhost + Android only for v1. vscode/cli later (likely reuse native terminal/explorer instead of these overlays).
Acceptance
Goal
Add two workspace tools to the AgentNet UI: a terminal and a retro-style file explorer. Both must work on every surface that renders the webview (localhost browser + Android), with vscode/cli out of scope for v1.
Why this is cheaper than it looks
The hard parts are already solved by the existing architecture:
surfaces/webviewis the single React UI rendered everywhere. Build a screen once → it appears on desktop and Android.POST /rpcfor commands,GET /eventsSSE for streams), defined inwebview/src/transport/protocol.tsand served bysurfaces/localhost/src/index.ts.surfaces/androidshipsproot-arm64;ServerService.ktkeeps "the node server" alive). So a real shell + a real glibc filesystem already exist on the phone. We are not porting a shell — it is already there.Net: implement RPC handlers in
localhost/index.ts+ screens inwebview→ Android inherits both for free (it reuses the localhost surface verbatim). vscode would need its own handlers later (or just reuse its native terminal/explorer).UI placement (decided)
The bottom nav is a fixed 4-domain pager — Chat · Skills · Agent · Market — with hard-coded tab-width math (
--an-tab-w) andLAST = 3inApp.tsx. Adding terminal/files as 5th/6th tabs breaks that math and crowds the mobile bar, and they are not top-level domains.Terminal and Files are tools of the active session's workspace (the cwd the agent is operating in). They belong attached to Chat, which already owns
activeSessionId.Placement: two icon buttons in the Chat header (
chat/ChatScreen.tsx, the right-side<div className="flex items-center gap-1 shrink-0">group, beforeStatusBadge). Tapping opens a full-screen overlay scoped to the active session's cwd — back-swipe / X to close. Same overlay reads as a side panel on a wide desktop browser. No change to the pager or tab bar.Phase 1 — Retro file explorer (EASY, ~1 day, low risk)
protocol.ts— new messages:{ type: "fs.list"; path: string },{ type: "fs.read"; path: string }{ type: "fs.listed"; path; entries: { name; kind: "dir"|"file"; size }[] },{ type: "fs.file"; path; content; truncated }localhost/index.ts— handler:fs.readdir(path, { withFileTypes })+stat;readFilecapped (~256 KB). Normalize + scope every path to the runtime workspace root; reject..escapes.webview/src/files/FilesOverlay.tsx— lazy tree (load a dir's children only on expand — proot fs is slow, no recursive walk). Retro look = pure CSS (monospace, box-drawing chars, green-on-black). Read-only in v1.ChatScreen.tsx— Files icon button → open overlay.Phase 2 — Terminal (MEDIUM, ~2–3 days)
protocol.ts— new messages:{type:"term.open"; cols; rows},{type:"term.input"; id; data},{type:"term.resize"; id; cols; rows},{type:"term.close"; id}{type:"term.data"; id; data},{type:"term.exit"; id; code}(data streams over the existing SSE channel; input arrives via POST)localhost/index.ts— spawn a shell:child_process.spawn(shell, { stdio: pipe })— pipe stdin/stdout. Works for line commands; no full-screen TUIs (vim/htop).// ponytail: pipe shell, swap to node-pty if a real PTY is needed.node-ptyfor a true PTY. Risk: native arm64 build inside proot (the "mass file churn" tax noted inguest/AGENTS.md). Ship v1 first.webview/src/term/TermOverlay.tsx—@xterm/xterm+ fit addon (one new dep). Wire to term.* messages.ChatScreen.tsx— Terminal icon button → open overlay.Risks / decisions
Acceptance