feat(vscode): scaffold VS Code extension for issue #35#145
Conversation
Set up extensions/vscode/ as a thin client over the atlas-proxy HTTP API: extension manifest with settings (proxy URL, service token, permission mode, status bar) and command stubs, TypeScript + esbuild build, eslint, vitest test harness, and the src layout for the client/session/ui modules landing in follow-up commits. No functional behavior yet.
Implement the typed client layer over the atlas-proxy public API: - src/client/types.ts: request/event/error shapes from docs/API.md, matching the proxy's Go struct field tags - src/client/sse.ts: pure SSE frame parser mirroring tui/chat.go (comment skipping, [DONE] sentinel, malformed-frame tolerance, chunk-split and multibyte-safe buffering) - src/client/atlasClient.ts: /v1/agent streaming, /cancel (best-effort), /v1/permission (404-as-resolved), /ready, /version; bearer auth; stable error-envelope mapping via AtlasApiError - test/: vitest suites (27 tests) plus a mockProxy fixture — a real http.Server streaming canned SSE, including a permission pause-and-resume flow driven through the client tsconfig gains skipLibCheck: vitest's bundled declarations reference DOM WebSocket types absent from lib ES2022.
|
Commit 2 landed: client core ( Typed transport layer over the proxy's public API — no UI yet, that's commits 3–4.
Zero runtime deps (Node 18 |
…ement
Add a webview chat view backed by a vscode-free TurnManager that mirrors
the TUI's protocol conventions: per-turn 24-char hex session ids, rolling
history capped at 40 entries with assistant text re-wrapped in the
{"type":"text"} envelope, session_allowed_tools re-sent on every turn,
and cancel wired to stream abort plus best-effort POST /cancel.
The webview is a dumb renderer: all state lives in the extension host,
which replays the transcript into any re-created view. Rendering is
textContent-only under a nonce'd CSP. The service token is read from
SecretStorage (atlas.setToken command) with the plaintext setting as a
dev override; 401s surface a "Set Token" action.
permission_request is interim auto-deny (with a visible note) so turns
never sit on the server-side permission timeout; the approve/deny UI
lands in the next commit.
Covered by 12 TurnManager unit tests (39 total).
|
Commit 3 landed: minimal chat ( First interactive slice — a sidebar chat that streams real turns from the proxy. Protocol brain and rendering are split so the protocol part stays testable without VS Code.
One deliberate interim: |
…native prompts - src/session/permissionFlow.ts: vscode-free permission state machine — first-answer-wins settling, session allowlist auto-allow (scope "once", matching TUI), remote permission_denied dismissal by tool name, turn-end cleanup, advisory POSTs with swallowed errors - src/ui/chatView.ts: wire PermissionFlow into event dispatch (replaces interim auto-deny), dual-surface prompts (inline webview card + native notification), transcript-replay-safe prompt/resolution messages - media/chat.js + chat.css: permission cards with Allow Once / Allow for Session / Deny actions and resolved-state rendering - test/permissionFlow.test.ts: 11 tests covering auto-allow, scopes, race arbitration, remote denial, turn end, POST failure paths
|
Commit 4 — interactive permission flow ( Implements the
"View Diff" on permission cards lands in the next commit (diff rendering), which needs the diff provider. |
Predict edits client-side and surface them via native vscode.diff and a read-only atlas-diff: virtual doc, for both permission review and applied changes.
|
Commit 5 — diff rendering for write_file / edit_file / ast_edit ( Client-side edit prediction surfaced through native
This wires up the "View Diff" button deferred from commit 4. |
…ress, CI Poll /ready for a status bar item (pauses while streaming), warn on workspace/proxy mount mismatch, map all 12 proxy error codes to friendly messages, show a live progress line, and add a paths-filtered CI workflow for the extension.
|
Commit 6 — status bar, mismatch detection, error mapping, progress line, CI (final polish)
This completes the v1 scope agreed earlier in the thread. |
|
@Anuj-72 Reviewing today- just had to finish up testing on some other updates around some issues in the harness I found. |
There was a problem hiding this comment.
@Anuj-72 Overall: this is supper close to mergeable I pulled the branch, ran the full pipeline in a clean node:20 container (tsc, eslint, all 94 tests, production bundle: all green), and reviewed it against the proxy source and the TUI as the reference client. Wire-level protocol fidelity is strong, the webview is properly locked down (strict CSP, nonce'd scripts, no HTML sinks), zero runtime deps is exactly right, and the tests are real tests, not mocks testing mocks. All three scope adjustments from #35 are implemented as agreed.
Five things are blocking; all look like they fixable without restructuring anything.
1. done.summary is dropped (src/ui/chatView.ts:393-395). The done handler breaks without rendering data.summary, and turnManager.ts only accumulates text events. On a tool-shaped turn the model's final answer arrives ONLY in done.summary (proxy/agent.go:616); the loop-breaker guidance strings ("Made your change... run it yourself to confirm") arrive the same way. As-is, every agentic turn ends with tool chips and then silence. The TUI renders it as a chat row (tui/model.go:1573); the extension should do the equivalent. types.ts:111 already defines DoneEventData; it just isn't used.
2. Deny desyncs the tool FIFO queues (src/ui/chatView.ts:274-281, src/workspace/mismatch.ts:151-190, media/chat.js:123-145). The proxy emits tool_call BEFORE the permission gate and emits no tool_result when a call is denied (proxy/agent.go:748-763); same shape on the truncated-args gate and the workspace-boundary rejection. All three of your tool-name FIFO queues enqueue at tool_call and only dequeue at tool_result, and the permission_denied handler doesn't consume them. Net effect: one denied edit, then the next allowed edit pairs with the stale entry, and the user gets the "proxy is likely mounted on a different directory" warning on a correctly mounted install, plus a wrong applied-diff. A deny is a completely normal flow, so this will be hit early. Suggest consuming the queues on permission_denied and clearing them at turn end as a backstop.
3. Node's default fetch body timeout kills long permission waits (src/client/atlasClient.ts:90). While a permission request is pending the proxy emits nothing on /v1/agent (the 15s heartbeat is only on /events), and the documented fail-safe timeout is ATLAS_PERMISSION_TIMEOUT_SEC = 600s. Undici's default body idle timeout is ~300s, so a user who steps away for five minutes comes back to a dead stream. Pass a dispatcher with the body timeout disabled (or raised past 600s) for the agent stream.
4. working_dir is sent as "." (src/session/turnManager.ts:59-63). The comment says this is the TUI convention; it isn't. The TUI sends its host cwd (tui/model.go:384), and proxy/agent.go:2662-2669 documents that convention: on Docker it drives host-to-container path translation, and on a bare-metal proxy "." resolves to the proxy's own process cwd, so every file op lands in the wrong tree. Send the workspace folder's fsPath.
5. Settings need scoping (package.json:71-95). atlas.proxyUrl, atlas.serviceToken, and atlas.permissionMode have no "scope", so a cloned repo's .vscode/settings.json can override them. Since the status bar auto-polls with the Bearer token attached, a malicious repo can point proxyUrl at its own host and receive the SecretStorage token without any user action (or silently flip the mode to yolo). "scope": "machine-overridable" on all three closes it.
Non-blocking, worth fixing while you're in there:
- V3's longest stages (
v3_plansearch,v3_divsampling,v3_select,v3_probe,v3_self_test,v3_plan) fall through to the unhandled-event log, so the progress line goes stale exactly when V3 is slowest (chatView.ts:367-375vsproxy/tools.go:3289-3315). Also addturn_startto the deliberate drop-list; right now a normal turn spams the output channel. editPreview.ts:124-140labels a multi-matchedit_fileprediction as exact, but the proxy rejects that call (tools.go:1510, uniqueness check), so the user approves a diff that can never apply. Mark multi-match as approximate or pre-detect the rejection.resolveLocalPath(chatView.ts:578-584) accepts absolute paths and..from model-controlled tool args and reads them into snapshot buffers without a user gesture. Nothing egresses, but I'd rather it refuse reads outside the workspace folder.- No
view.onDidDisposehandling; a post to a disposed webview mid-turn throws and kills the stream consumption (chatView.ts:618-626). Related: theawait this.makeClient()inrunTurnsits outside the try, so a SecretStorage failure makes Send silently do nothing. - The root
.gitignoreout/entry is redundant (the extension-local one covers it) and unanchored; scope it or drop it. mismatch.ts:82/93treats mtime equality asmismatch; on coarse-mtime filesystems that's a false "wrong mount" warning. Your own header note lists mtime granularity as a reason to stay silent; inconclusive should win there.
Process, on my side: I'm retargeting this to dev (everything lands there first; it merges cleanly, I checked). I'll also add an npm block to dependabot and won't put the path-filtered CI job in required checks. Please add a docs/MAP.md row for extensions/, a CHANGELOG entry, and a one-line pointer in docs/API.md's "Building a non-TUI client" section as part of this PR (repo rule: code changes pair with their docs updates).
Fix the five blockers and I'm ready to land this. Really solid work, especially the client/session split keeping the protocol layer testable; the SSE parser tests caught exactly the cases that bite real clients.
Two review blockers from PR itigges22#145: - done.summary was dropped: on tool-shaped turns the model's final answer arrives only in the done event (proxy/agent.go), so every agentic turn ended in silence. The chat now renders a non-empty summary as an assistant bubble (TUI parity, tui/model.go), and TurnManager records it as the assistant history entry when no text streamed, so the next turn's history is not empty either. - a denied tool call desynced the tool-name FIFO queues: the proxy emits tool_call before the permission gate and no tool_result on deny, but the edit-snapshot, mismatch-detector, and webview-chip queues only dequeued on tool_result. One deny made the next allowed call pair with stale state (wrong applied diff, false mount-mismatch warning). All three queues now consume on permission_denied, and the webview settles leftover chips at turn end as a backstop for the truncated-args and workspace-boundary gates, which emit neither event.
Two review blockers from PR itigges22#145: - undici's default body idle timeout (~300s) killed the /v1/agent stream during long permission waits: the proxy emits nothing on that stream while a permission_request is pending, and the documented fail-safe is ATLAS_PERMISSION_TIMEOUT_SEC = 600s. The agent stream now uses a dispatcher with bodyTimeout disabled, built from the global dispatcher's constructor (zero-runtime-deps: no undici import). Runtimes without the undici global fall back to plain fetch. - working_dir was sent as ".", which resolves to the proxy's own process cwd on bare-metal installs and breaks host-to-container path translation on Docker. The chat now sends the workspace folder's fsPath, matching the TUI convention of sending its host cwd (tui/model.go).
Review blocker 5 plus two hardening items from PR itigges22#145: - atlas.proxyUrl, atlas.serviceToken, and atlas.permissionMode gain "scope": "machine-overridable" so a cloned repo's .vscode/settings.json cannot silently redirect the status-bar poll (and its bearer token) to a hostile host or flip the mode to yolo. - resolveLocalPath no longer accepts absolute paths or ..-escapes from model-controlled tool args; reads stay inside the workspace folder. Callers already degrade gracefully (no diff preview, inconclusive mismatch verdict). - the webview reference is dropped on onDidDispose (a post to a disposed webview throws mid-stream), and makeClient moved inside runTurn's try so a SecretStorage failure renders an error card instead of Send silently doing nothing.
Non-blocking review items from PR itigges22#145: - the long V3 stages (v3_plansearch, v3_divsampling, v3_select, v3_probe, v3_self_test, v3_plan) now drive the progress line — they share the {stage, detail} shape of the stages already handled (proxy/tools.go v3StageToEvent), and without them the line went stale exactly where V3 is slowest. turn_start joins the deliberate drop-list so normal turns stop spamming the output channel. - a multi-match edit_file prediction without replace_all is now labeled approximate with an explanatory note: the proxy requires a unique match and rejects such calls, so an "exact" diff there could never apply. - an unchanged mtime after write-over/edit is now 'inconclusive' (silent) instead of 'mismatch' — coarse-mtime filesystems can stamp a real change with the same time, and mtime granularity was already listed as a reason to stay quiet. - drop the root .gitignore out/ entry this branch added; the extension-local .gitignore covers it and the unanchored form shadowed unrelated trees.
Docs pairing for the VS Code extension PR (repo rule: code changes pair with their docs updates): extensions/ row in the MAP.md directory table, an Unreleased changelog entry, and a pointer in API.md's "Building a non-TUI client" section.
Swap the placeholder activity bar icon for a custom one (media/ATLAS.svg), referenced from viewsContainers.activitybar in package.json. Old media/icon.svg removed. package-lock.json: drop two stale "extraneous" entries under vite-node's nested @types/node / undici-types left over from an unrelated npm install resync; no dependency change.
…uj-72-ATLAS into feat/vscode-extension
|
@itigges22 Thanks for the thorough review — all five blockers and all six non-blocking items are addressed, plus the three docs updates. Five commits, mapped below. Full pipeline green after each one (tsc, eslint, 102 tests — up from 94, production bundle). Blockers
Non-blocking items — all six taken:
One note on the changelog: the version-contract test at this branch's base fails on an One more change beyond the review scope: |
feat: VS Code extension — thin client over atlas-proxy (#35)
Relates to #35 (VS Code part; JetBrains deferred). Scaffold-only in this PR — later commits (2–6 in plan below) complete the feature before this can close the issue.
What this is
A VS Code extension under
extensions/vscode/that wraps the atlas-proxy agent loop as a thin SSE client — no agent logic in the extension. Built againstdocs/API.md, with the TUI as the reference client (session_id conventions,session_allowed_toolsre-sent per turn, cancel = abort + best-effortPOST /cancel).Scope (per maintainer review)
POST /v1/agent(SSE) +POST /cancel+POST /v1/permission. Permission approve/deny UI is in scope as required (auto-answer for session-allowed tools, native notification + chat card otherwise; 404 on decision POST treated as already-resolved).write_file,edit_file, andast_edit— computed client-side at permission time (ast_editresult carries no new content, so best-effort splice + snapshot-vs-disk post-view for the exact applied change).ATLAS: Set Service Token, plaintext setting as dev override), permission mode (default/accept-edits/yolo). No model preference setting.Also per review: no file sync (VS Code watcher picks up bind-mounted writes); instead a passive heuristic detects workspace ≠ proxy mounted dir and warns once.
/readypolling drives an optional status-bar badge.Plan (commits land on this branch)
atlasClient(agent/cancel/permission/ready), API types, unit tests + mock proxy fixturepaths: extensions/vscode/**)Out of scope
JetBrains extension, model preference setting, file sync, cross-reload chat persistence.
Follow-up suggestion
No endpoint exposes the proxy's mounted working dir, so the workspace-mismatch check is heuristic (post-edit
fs.stat). A tinyGET /workspacereturning the mounted path would make it exact — happy to file separately if wanted.Testing
npm run compile(tsc + eslint + esbuild) clean;npm test(vitest) passesatlas up) planned per commit: chat turn, edit permission + diff, allow-for-session across turns, mid-turn cancel, 401 path, status-bar states