docs: update plan.md

Remove completed sections (shadcn migration, ACP panel integration, all
related out-of-scope items that landed in v1). Add the new task
'AcpPanel tool-call block UI (session/update tool_call type)' as the
next phase: forward tool_call events through the stream, store them
in history, render them as collapsible blocks above the agent's
text bubble.
This commit is contained in:
tao.chen
2026-07-06 17:19:28 +08:00
parent ec54ea4473
commit 79292c9f38
+62 -62
View File
@@ -2,80 +2,80 @@
Roadmap for the next phases of the codespace web IDE. Roadmap for the next phases of the codespace web IDE.
## Now: shadcn/ui dialog and dropdown migration ## Next: AcpPanel tool-call block UI
The frontend still uses `window.prompt`, `window.confirm`, and an absolute-positioned `<div>` for the file-tree context menu. Replace these with shadcn/ui components backed by Radix UI primitives. This gives us: When the agent invokes a tool (e.g. reads a file, runs a command, edits a buffer) it emits a `session/update` notification with `sessionUpdate: "tool_call"`. Currently the client silently drops these in `handleNotification` (debug log only), so the user has no visibility into what the agent is doing while the reply streams in. Render them as discrete blocks under the agent bubble so the user can see "the agent read foo.go, ran `git status`, is now editing bar.go…".
- Consistent visual style for all popups
- Better keyboard navigation and accessibility
- The right-click context menu in the file tree becomes a real `DropdownMenu` instead of an in-place div
### Concrete steps
1. Add Radix UI primitives: `@radix-ui/react-dialog`, `@radix-ui/react-alert-dialog`, `@radix-ui/react-dropdown-menu`.
2. Add shadcn-style wrappers under `web/src/components/ui/`:
- `dialog.tsx` (Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter)
- `alert-dialog.tsx` (AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel)
- `dropdown-menu.tsx` (DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator)
- `input.tsx` (used inside Dialogs for prompts)
3. Migrate the existing callers:
- `WorkspaceSelector.tsx`: replace `window.prompt` for new-workspace with a `Dialog` containing an `Input`. Replace `window.confirm` for delete with an `AlertDialog`.
- `FileExplorerPanel.tsx`: replace all `window.prompt` (new file, new folder, rename) with a generic `Dialog` + `Input` component parameterized by title and callback. Replace `window.confirm` (delete) with an `AlertDialog`. Replace the absolute-positioned context menu with `DropdownMenu`.
4. Do not change backend behavior.
## Next: opencode ACP panel and integration
`opencode acp` exposes the editor as an **Agent Client Protocol** server. The current `opencode` process is started in the background but the user has no way to interact with it. The next phase adds a first-class ACP client panel in the web IDE.
**Status: ✅ Completed** (commits: ACP backend `feat(backend): add opencode ACP ...`, ACP frontend `feat(web): add ACP chat panel ...`, bug fix `fix(acp): parse session/update content as ContentBlock, not string`).
### Why ### Why
The terminal currently runs `bash` (the shell subsystem). The `opencode` process is started via the toolbar and is meant to be controlled by an ACP client — the same protocol that editors like Zed use to talk to coding agents. Without an ACP panel, the opencode process is invisible: it has a PID and a status indicator, but no way to send it prompts or read responses. Without tool-call visibility, the streamed agent reply is just text. The user has to guess what the agent is doing. Real coding agents (Claude Code, Cursor, Zed) show tool invocations inline so the user can:
- See which files the agent touched.
- Catch mistakes early (agent edited the wrong file).
- Trust the agent's progress during long-running work.
### Scope ### Scope
1. **Backend: switch the opencode process to ACP mode** 1. **Backend: forward tool_call events through the stream**
- Default `opencodeCommand` stays `opencode`; new `process.args` field defaults to `["acp"]` so the launched command is `opencode acp`. Configurable via `configs/config.yaml` and the `CODESPACE_OPENCODE_ARGS` env var. - In `internal/acp/handleNotification`, when `sessionUpdate == "tool_call"`, emit a `StreamEvent` to all active stream consumers:
- The `internal/process` package still captures stdout/stderr and exposes stdin via the WebSocket. ACP over stdio sits on top in the new `internal/acp` package — no transport changes to the existing process WS route. ```go
case StreamEvent{Type: "tool_call", ToolCallID: ..., Kind: ..., Title: ..., Status: ..., Content: ...}
```
- Also append a stable representation of the tool call to history so the next `GET /acp/history` returns it (so reloading the panel shows past tool calls). The `Message` model gains an optional `Kind: string` and `ToolCallID: string` field; `Role` stays as "agent" for v1 (or add a new role "tool" — TBD).
- The `Update` struct in `internal/acp/messages.go` already carries `ToolCallID string`. Capture the full `up.Update` payload for the history entry.
2. **Backend: ACP-aware HTTP API** 2. **Frontend: ToolCallBlock component**
- Implemented as four HTTP routes per ACP method. The frontend does not need to know JSON-RPC. - New `web/src/components/acp/ToolCallBlock.tsx`.
- Routes: - Props: `{ kind, title, status, content? }` where:
- `GET /api/workspaces/:id/acp/status` — observational: `{workspaceId, ready, sessionId, running, pid, error}`. Does NOT start the process. - `kind` is the tool name (e.g. "read", "edit", "bash", "fetch", "grep").
- `GET /api/workspaces/:id/acp/history` — accumulated `{sessionId, messages: [{role, text, time}]}`, oldest first. In-memory only. - `title` is a short human-readable summary (the agent's intent).
- `POST /api/workspaces/:id/acp/prompt` — body `{content: string}`. Returns `{sessionId, stopReason, text}`. Starts the process + session on demand (synchronous, 5-min default timeout). - `status` is one of "pending" | "running" | "completed" | "failed" (the ACP `tool_call` update lifecycle — pending → running → completed/failed). Map to icons: `Loader2` (spinning) for running, `Circle` for pending, `CheckCircle2` for completed, `XCircle` for failed.
- `POST /api/workspaces/:id/acp/cancel` — best-effort interrupt. - `content` is optional: for `kind: read` the file path or first lines; for `kind: edit` the diff; for `kind: bash` the command. Rendered as a small monospace block (collapsed by default, expand on click).
- Visual: small card, ~border + ~bg-muted, ~rounded-md, ~text-xs. Distinct from message bubbles so it's clear the agent did something, not said something.
- When `content` is large (e.g. a long file), truncate to a preview and show "show more".
3. **Frontend: ACP panel** 3. **Frontend: integrate into AcpPanel**
- Implemented as `web/src/components/acp/AcpPanel.tsx` with subcomponents `MessageBubble`, `MarkdownView`, `ThinkingIndicator`. Renders markdown with code-block highlighting (react-markdown + react-syntax-highlighter Prism oneDark), with auto-scroll, error toast fade, and a "thinking…" indicator while in flight. - Streaming events with `type: "tool_call"` append a ToolCallBlock to a per-prompt list.
- Hooks: `useAcpStatus` (3s poll), `useAcpHistory` (5s poll), `useAcpPrompt` (mutation), `useAcpCancel` (mutation). - The list is shown ABOVE the in-flight agent bubble (so tool calls appear first, then the agent's reply references them). On `complete`, the list is "committed" alongside the agent message.
- History endpoint returns tool calls; the AcpPanel render path coalesces: history messages + a per-message array of `ToolCall` items that appeared during that turn.
- When clicking a ToolCallBlock with a file path, optionally open the file in the editor (nice-to-have, defer to v2 if time-pressed).
4. **Frontend: integration with the workspace shell** 4. **Data model**
- Bottom panel converted to a tabbed area: `Terminal` / `Agent` (decided on **Tabs + Sibling tab**, not replace, not split). Tab state lives in `workspaceUiStore.bottomTab`. - Extend `AcpMessage` / `internal/acp/history.go` `Message`:
- `StatusBar` shows `agent: ready` / `initializing…` / `<error>` pill when the ACP session is initialized; hides when not. ```go
- The existing `process/*` routes and raw process WebSocket remain untouched (the new acp routes are additive). type Message struct {
Role string // "user" | "agent"
Text string
Time time.Time
MessageID string
ToolCalls []ToolCall // NEW: tool calls that occurred during this agent turn
}
type ToolCall struct {
ToolCallID string
Kind string
Title string
Status string
Content string
}
```
- The `session/update` with `sessionUpdate: "tool_call"` updates the tool call's status (pending → running → completed/failed). Correlate by `toolCallId`.
### Concrete tasks — all done 5. **Tests**
- `internal/acp/client_test.go`: extend `TestClientStreamEmitsChunkAndComplete` (or add a new test) that drives a fake tool_call sequence (pending → running → completed) and asserts the history contains the merged tool calls.
1. ✅ Switched opencode default to `opencode acp` via `process.args`. - `internal/acp/history_test.go` (new) or `client_test.go`: verify `AppendOrCreate` on tool calls works (or add a separate `History.SetToolCalls(role, messageId, calls)` method).
2. ✅ Added `internal/acp` package: NDJSON transport + JSON-RPC 2.0 client (NDJSON framing with split-line buffering, request/response correlation, notification dispatch, per-workspace `Client` + `Service`). - Frontend: no new test infra; rely on visual verification in the dev server.
3. ✅ Added `internal/api/acp_handler.go` with the four routes.
4. ✅ Added `web/src/lib/api/acp.ts` with the four hooks.
5. ✅ Added `web/src/components/acp/AcpPanel.tsx` + subcomponents.
6. ✅ Added `react-markdown` + `react-syntax-highlighter` + `remark-gfm` to `web/package.json`.
7. ✅ Wired the ACP panel into `WorkspaceShell.tsx` as a tabbed bottom panel.
8. ✅ Documented the ACP routes in `README.md` (section + env var + example curl).
### Tests
- `internal/acp/client_test.go` covers NDJSON split-lines, request/response correlation, notification dispatch, and agent-initiated request handling for `fs/*` (real dispatch) and `terminal/*` (MethodNotFound).
- E2E smoke test against the live `opencode acp` binary: created a workspace, sent a prompt, received `text: "Hi there!"` non-empty, history has 4 messages (1 user + 3 agent chunks), 0 `invalid session/update` log entries.
### Out of scope (deferred to v2) ### Out of scope (deferred to v2)
- Streaming responses over WebSocket (v1 is single round-trip per prompt). - Tool-call approval UI (the agent runs everything; no interrupt-and-confirm dialog).
- Tool-call approval UI. - Re-running a tool call.
- Diff visualization for `edit` calls (just show the raw content for v1).
- Streaming partial tool-call content.
- Multiple agents / agent switching. - Multiple agents / agent switching.
- Authentication.
- Cross-workspace agent memory. - Cross-workspace agent memory.
- ACP session persistence across opencode restarts (history is in-memory and cleared on process restart — acceptable for v1). - Authentication.
- `terminal/*` agent tool calls (return `MethodNotFound -32601`; the agent will fail those tool calls gracefully). - ACP session persistence across opencode restarts.
### Open questions to resolve before implementation
- Should the in-memory history distinguish "agent text" from "tool calls" by message type, or are tool calls an array on the agent message? — Default: array on the agent message (one agent turn can have many tool calls).
- What's the shape of the `content` field for non-trivial tool calls? — For `read`, the file content; for `edit`, a diff; for `bash`, the output. Inspect a real `session/update` payload before settling the field name (`content` vs `output` vs `result`).
- Status transitions: does the agent send multiple `tool_call` updates with the same `toolCallId` (pending → running → completed), or just one final update? — Likely multiple; need to verify against a real opencode acp session.