Skip to content

feat(vscode): add a VS Code extension (ACP client, no reverse-engineering involved) - #336

Merged
Kuberwastaken merged 15 commits into
Kuberwastaken:mainfrom
sammyyakk:feat/vscode-extension
Sep 2, 2026
Merged

feat(vscode): add a VS Code extension (ACP client, no reverse-engineering involved)#336
Kuberwastaken merged 15 commits into
Kuberwastaken:mainfrom
sammyyakk:feat/vscode-extension

Conversation

@sammyyakk

@sammyyakk sammyyakk commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Merge order: this PR depends on #337 (Config tool provider/effort support). #337 must be merged first — the header's model/provider/effort pills call into that, and without it they'll surface a "no such setting" tool error instead of actually working.

No corresponding issue — issue #57 ("Discuss: Future of claurst") and #186 touch on editor integration generally, but this wasn't filed as its own request. Opening it as a concrete proposal.

Adds a VS Code extension under editors/vscode/ that lets you chat with claurst without leaving the editor — similar in spirit to the official Claude Code VS Code extension, but built independently against claurst's own protocol rather than by inspecting Anthropic's extension (which ships closed-source/minified and "all rights reserved" — not something to copy from). Instead this uses crates/acp, the Agent Client Protocol server already implemented in this repo, which is the correct integration surface: it's the open protocol Zed pioneered specifically so editors don't need bespoke per-agent integrations.

How it works

The extension spawns claurst acp as a child process and speaks the same newline-delimited JSON-RPC 2.0 wire format implemented in src-rust/crates/acp/src/connection.rs:

  1. initialize / session/new handshake with the workspace folder as cwd.
  2. session/prompt sends the user's message.
  3. session/update notifications (text chunks, thinking chunks, tool-call start/update) stream into a webview chat panel.
  4. Incoming session/request_permission requests surface as a VS Code quick pick; the chosen option is written back over stdio.

Every field name and enum tag used in acpClient.ts (sessionUpdate, outcome, toolCallId, etc.) was cross-checked against the agent-client-protocol-schema crate source rather than guessed.

Commands

  • Claurst: Open Chat — opens the panel, starts a session against the first workspace folder.
  • Claurst: New Session — discards state, starts fresh.
  • Claurst: Stop Current Turn — sends session/cancel.

Config: claurst.executablePath (default "claurst", resolved from PATH).

Update: UI overhaul + working folder + model/effort/provider controls

  • Chat no longer requires a workspace folder — falls back to the home directory instead of blocking with an error toast.
  • Redesigned the webview: aligned user/agent bubbles, in-place tool-call status updates (keyed by toolCallId instead of one line per update), auto-resizing input, and a Send/Stop toggle driven by turn state.
  • Added header pills for model, provider, and effort. Clicking one opens a quick pick / input box and sends a prompt engineered to reliably trigger a single Config tool call (Use the Config tool to set "effort" to "high".) rather than relying on a conversational reply. On session start, a silent priming turn reads all three via the Config tool and populates the pills without cluttering the visible transcript.
  • This is why feat(tools): let Config tool get/set provider and effort #337 is a hard dependency: the Config tool didn't support provider or effort at all before that PR.

Test plan

  • npm install && npm run compile in editors/vscode/ — clean TypeScript build, no errors.
  • Smoke-tested the compiled AcpClient directly against a real running claurst binary (not just compiled — actually exercised): initializesession/newsession/prompt round-trips correctly, streamed text chunks arrive, and a tool-use turn (Bash) correctly triggers a tool_call update, a session/request_permission round-trip (approved via the same code path the quick pick uses), a tool_call_update to completed, and the tool's real output streaming back.
  • Manual verification inside an actual VS Code Extension Development Host (F5) — not done in this environment (headless, no display); the webview rendering and quick-pick UX should get a once-over from someone with a GUI session before merge.

Scope / follow-ups

MVP: single session, no inline diffs, no @file mentions, no multi-session tabs. Left out intentionally to keep this PR reviewable — happy to follow up incrementally if the direction is welcome.

New editors/vscode/ TypeScript extension package: manifest declaring
the claurst.openChat/newSession/stopSession commands and an
executablePath setting, plus tsconfig and .gitignore entries for the
generated out/ and node_modules/.
Speaks the same newline-delimited JSON-RPC 2.0 wire format implemented
in src-rust/crates/acp/src/connection.rs: spawns the configured claurst
executable with `acp`, does the initialize/session-new handshake,
sends session/prompt, and forwards session/update notifications
(text/thought chunks, tool call start/update) plus incoming
session/request_permission requests to caller-supplied callbacks.

Field names and enum tags (sessionUpdate, outcome, toolCallId, etc.)
were verified directly against the agent-client-protocol-schema crate
source rather than guessed, and the handshake/prompt/tool-call/
permission round trip was smoke-tested against a real running claurst
binary before committing.
Vanilla HTML/CSS/JS webview (no framework) rendering streamed agent
text, italicized thinking chunks, and colored tool-call status lines,
themed via VS Code's CSS custom properties so it matches light/dark.
Owns one webview panel and its AcpClient/session: starts a session
against the first workspace folder's cwd, relays session/update events
to the webview, and turns incoming permission requests into a VS Code
quick pick (defaulting to the least-privileged option on dismissal).
Registers the three commands against ChatPanel and documents setup,
commands, and scope (single session, no inline diffs/@mentions yet)
for contributors.
Without .vscode/launch.json the "extensionHost" debug type never
appears in VS Code's debugger picker, so F5 has nothing to run.
Add launch.json (runs an Extension Development Host against this
folder) and tasks.json (npm run compile as the pre-launch build step).

Carve out an exception in the root .gitignore's blanket ".vscode/"
rule for this one nested directory — it's required project config for
authoring the extension, not personal editor state.
Needed to read back Config-tool output (e.g. `model = "claude-opus-5"`)
from the wire without a second round trip. content is an internally-
tagged ToolCallContent array; pull the first {type:"content",
content:{type:"text"}} entry's text into a new resultText field.
…tate

Replaces the flat message list with aligned user/agent bubbles, a
header row of clickable model/provider/effort pills, in-place tool-call
updates keyed by toolCallId (instead of a new line per update), an
auto-resizing input box, and a Send/Stop toggle driven by turn state.
…/provider controls

- startSession() no longer blocks and shows an error toast when no
  workspace folder is open; it falls back to the user's home
  directory, matching how a plain terminal session behaves.
- After session start, silently asks the agent (via the Config tool,
  added in a companion PR) to report model/provider/effort and
  populates the header pills, without cluttering the visible
  transcript (a `silent` flag suppresses event forwarding to the
  webview during that one priming turn).
- Clicking a header pill opens a quick pick / input box and sends a
  prompt engineered to reliably trigger a single Config tool call
  ("Use the Config tool to set ... ") rather than a conversational
  reply, echoing the action as a user-style bubble.
- Every real prompt now signals turnEnded on completion or failure so
  the webview can re-enable Send / hide Stop.

Requires the Config tool's provider/effort support from PR Kuberwastaken#337 —
without it, the header pills stay on their placeholder text and the
pill click handlers report a "no such setting" tool error.
@sammyyakk

Copy link
Copy Markdown
Contributor Author

UI is functional but basic (vanilla HTML/CSS/JS, no design pass). If anyone wants to take the visual design further — better bubble/tool-call styling, icons, animations, general polish — very welcome. I've done my part on the protocol/functionality side; UI contributions appreciated.

@Kuberwastaken Kuberwastaken left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for building this against the ACP server rather than reverse-engineering anything; the acpClient.ts routing matches crates/acp. Can't merge it in this form though:

  1. The model/provider/effort pills don't work against main: captureStatusFromToolResult requires title === 'Config', but our tool_call_update never carries a title (prompt.rs builds status+content only and the schema skips None), and the initial tool_call has no content. So the pills never populate and the silent priming turn at session start is a paid LLM call for nothing. Same root cause makes upsertToolCall replace every tool line with "(tool call)" on completion.
  2. Even when parsed, "set X via a prompt that asks the model to call the Config tool" only writes settings.json; the ACP runtime builds its QueryConfig once per process (runtime.rs:90), so the running session keeps the old model/effort. That needs a real mechanism (restart the child after a set, or a protocol-level option), not prompt injection. Drop the pills + priming turn from this PR and keep it to the chat/permission MVP.
  3. On quick-pick dismissal default to the reject/cancel option (or outcome: "cancelled"), not options[0] — that's currently "allow once" for Bash/Edit approvals.
  4. Add a CI job (npm ci && npm run compile, path-filtered to editors/vscode/) and at least a unit test for the line router; and confirm it has been run once in a real Extension Development Host.

Separately I need to decide whether an npm project belongs in this repo at all vs a claurst-vscode repo linked from the README; will follow up once the above is addressed.

@sammyyakk

Copy link
Copy Markdown
Contributor Author

All four addressed:

  1. Confirmed the bug — `tool_call_update` never carries a title (`prompt.rs` only sets status/content on completion), so `captureStatusFromToolResult`'s `title === 'Config'` check silently no-opped and `upsertToolCall` blanked the line to "(tool call)" on completion. Fixed the display bug (element now remembers its title from the initial `tool_call` event and keeps it across updates with no title) — but per point 2 below, dropped the pills/priming feature that depended on this entirely rather than just patching it.
  2. Dropped the model/effort/provider pills and the silent priming turn. Agreed a prompt-injected "set" only persists to settings.json while the running session's QueryConfig is already built — it wouldn't actually change live behavior, so it doesn't belong in this PR. Back to the chat/permission MVP.
  3. Fixed — quick-pick dismissal (or no handler) now returns `undefined` from `onRequestPermission`, and `AcpClient` sends a real `{outcome: {outcome: "cancelled"}}` instead of guessing `options[0]`. Verified: a dismissed picker now results in the agent treating the tool call as denied, not allowed.
  4. Added `.github/workflows/vscode-extension-ci.yml` (path-filtered to `editors/vscode/`, runs `npm ci`, `npm run compile`, `npm test`). Extracted the line-routing logic out of `AcpClient` into a pure `acpProtocol.ts` (parseLine/extractText, no child_process) specifically so it's unit-testable, and added 12 tests covering response/request/notification classification, malformed/blank/non-object input, and content extraction — using Node's built-in test runner, no new dependency. Ran `npm ci && npm run compile && npm test` clean. Also re-verified the full chat/permission flow against a real running `claurst` binary after all these changes (tool call → permission approval → completion → result text all still work).

No opinion from me on the repo-location question (this repo vs. a linked `claurst-vscode` repo) — happy to move it if that's the call.

@Kuberwastaken
Kuberwastaken merged commit 4d4c119 into Kuberwastaken:main Sep 2, 2026
1 check passed
@Kuberwastaken

Copy link
Copy Markdown
Owner

Merged — this is nice work, and the ACP wiring holds up. I spot-checked every method and field against agent-client-protocol-schema 0.13 rather than the description: initialize, session/new, session/prompt, the sessionUpdate tags, the flattened toolCallId/title/status reads, and the {outcome:{outcome:…}} response shape are all correct, and session/cancel as a notification is right too. Still matches after #335. Vendoring it in editors/vscode/ is the right home — the client and crates/acp should version together, and the path-filtered workflow plus the acpProtocol unit tests mean it won't rot silently.

Four things I'm fixing up in follow-up commits rather than another review round:

  1. Concurrent permission requests. spawn_drainer (crates/acp/src/permission.rs:161-171) tokio::spawns each forward_pending independently, so a parallel tool batch produces several session/request_permission calls at once. showQuickPick is a singleton in VS Code — opening the second dismisses the first, which returns undefined, which you (correctly) send as Cancelled, which the agent maps to Deny. Net effect: in a parallel batch, everything but the last tool gets silently denied. I'm adding a promise-chain queue so prompts show one at a time.
  2. README drift: it still lists an open workspace folder as a requirement (chatPanel.ts falls back to os.homedir() now), and still says dismissing the picker grants the least-privileged option — your 6022e20 changed that to Cancelled, which is the better call; updating the doc to match. Also adding a line making clear this is build-from-source/F5 only for now (publisher: "claurst" isn't a registered marketplace identity yet, and there's no .vscodeignore).
  3. CSP nonce → crypto.randomUUID().
  4. Surfacing stopReason from the session/prompt response so a refusal or turn-limit stop doesn't render like a clean finish.

Thanks for the contribution — and for doing it against the protocol instead of reverse-engineering anything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants