Skip to content

feat(vscode): scaffold VS Code extension for issue #35#145

Open
Anuj-72 wants to merge 6 commits into
itigges22:mainfrom
Anuj-72:feat/vscode-extension
Open

feat(vscode): scaffold VS Code extension for issue #35#145
Anuj-72 wants to merge 6 commits into
itigges22:mainfrom
Anuj-72:feat/vscode-extension

Conversation

@Anuj-72

@Anuj-72 Anuj-72 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

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 against docs/API.md, with the TUI as the reference client (session_id conventions, session_allowed_tools re-sent per turn, cancel = abort + best-effort POST /cancel).

Scope (per maintainer review)

  1. v1 API surfacePOST /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).
  2. Diff rendering covers write_file, edit_file, and ast_edit — computed client-side at permission time (ast_edit result carries no new content, so best-effort splice + snapshot-vs-disk post-view for the exact applied change).
  3. Settings — proxy URL, service token (Bearer; SecretStorage via 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. /ready polling drives an optional status-bar badge.

Plan (commits land on this branch)

  • 1. Scaffold — manifest, settings schema, command stubs, build (esbuild + tsc + eslint), vitest harness, README
  • 2. Client core — SSE parser, atlasClient (agent/cancel/permission/ready), API types, unit tests + mock proxy fixture
  • 3. Minimal chat — webview sidebar, streamed text, tool chips, done/error, cancel, history (last-40)
  • 4. Permission flow — notification + inline card, session-allowed set, permission-mode wiring
  • 5. Diffs — virtual-doc provider, per-tool diff computation, snapshot post-result view
  • 6. Polish — status bar, workspace-mismatch warning, error-envelope mapping, progress line, CI job (paths: 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 tiny GET /workspace returning the mounted path would make it exact — happy to file separately if wanted.

Testing

  • npm run compile (tsc + eslint + esbuild) clean; npm test (vitest) passes
  • Manual E2E against live proxy (atlas up) planned per commit: chat turn, edit permission + diff, allow-for-session across turns, mid-turn cancel, 401 path, status-bar states

Anuj-72 added 2 commits July 17, 2026 03:51
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.
@Anuj-72

Anuj-72 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

Commit 2 landed: client core (src/client/)

Typed transport layer over the proxy's public API — no UI yet, that's commits 3–4.

  • types.ts — request/event/error shapes from docs/API.md (closed ErrorCode set, event payloads, /ready//version bodies)
  • sse.ts — pure SSE frame parser mirroring tui/chat.go: comment skipping, [DONE] sentinel, malformed frames skipped not fatal, chunk-split and multibyte-safe buffering
  • atlasClient.ts — the three required endpoints plus status: /v1/agent (streaming, AbortSignal-cancellable), /cancel (best-effort), /v1/permission (404-as-resolved, echoes tool_call_id, scope: once|session), /ready, /version. Bearer auth; errors switch on the envelope error code, never detail.
  • test/ — 27 vitest tests incl. a mock proxy (node:http) that streams real SSE and can block the agent stream until a permission decision arrives, so the pause→approve→resume flow is exercised end-to-end at the transport level

Zero runtime deps (Node 18 fetch). Next: commit 3, minimal chat webview (session_id minting, history cap, cancel wiring).

…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).
@Anuj-72

Anuj-72 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Commit 3 landed: minimal chat (src/session/, src/ui/, media/)

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.

  • turnManager.ts — vscode-free turn/session state mirroring the TUI's conventions: fresh 24-char hex session_id per turn (tui/model.go newSessionID), rolling history capped at 40 with assistant text re-wrapped in the {"type":"text"} envelope (buildChatHistory parity — raw text would corrupt the next turn's parse), session_allowed_tools re-sent on every turn once populated, cancel = stream abort + best-effort POST /cancel. Partial assistant text survives a cancelled turn into history.
  • chatView.tsWebviewViewProvider owning all state; the webview is a dumb renderer. Every message is recorded to a transcript and replayed into any re-created view (sidebar closed/reopened). Settings read fresh per turn; service token from SecretStorage (ATLAS: Set Service Token command) with the plaintext setting as a dev override; 401 surfaces a native "Set Token" action.
  • media/chat.js + chat.css — vanilla JS, textContent-only rendering under a nonce'd CSP (default-src 'none'), VS Code theme variables. Streamed assistant bubbles, tool chips resolving ✓/✗ with elapsed time.
  • test/turnManager.test.ts — 12 tests pinning session-id shape, request shape, envelope re-wrap, 40-cap, allowlist re-send, busy guard, cancel semantics (39 total).

One deliberate interim: permission_request is auto-denied with a visible note, so no turn ever sits on the 600s server-side permission timeout while the approval UI doesn't exist yet. Next: commit 4, the real permission flow (allow once / allow for session / deny, first-answer-wins, permission_denied dismissal).

…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
@Anuj-72

Anuj-72 commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Commit 4 — interactive permission flow (8acb96f)

Implements the /v1/permission handling flagged as required v1 scope (without it the extension hangs for ATLAS_PERMISSION_TIMEOUT_SEC on the first edit).

  • New src/session/permissionFlow.ts (vscode-free, ~170 lines): PermissionFlow + PendingPermission with first-answer-wins settle(). Auto-allow via a shared sessionAllowedTools set → posts allow/once; "allow for session" adds to the set + posts allow/session; handleDenied(tool) dismisses the oldest pending card by tool name; endTurn() dismisses all; settleById routes webview answers, stale id → no-op. Post is fire-and-forget with an onPostError hook.
  • chatView.ts: interim auto-deny removed. permission_requesthandleRequest, permission_deniedhandleDenied + note. Dual surface: inline webview card and native showInformationMessage (Allow Once / Allow for Session / Deny). Both the prompt and its resolution are recorded in the transcript, so replay is faithful.
  • media/chat.js: permissionCards map, addPermissionCard / resolvePermissionCard, textContent-only rendering under the existing nonce CSP.
  • chat.css: warning-themed .permission-card.
  • Tests: test/permissionFlow.test.ts (11 tests). Suite now 50/50 green; tsc, eslint, esbuild all clean.

"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.
@Anuj-72

Anuj-72 commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Commit 5 — diff rendering for write_file / edit_file / ast_edit (e0c895b)

Client-side edit prediction surfaced through native vscode.diff and a read-only atlas-diff: virtual document — for both permission review and applied changes.

  • New src/session/editPreview.ts (pure, vscode-free): FILE_EDIT_TOOLS set, editTargetPath(), predictEdit(). write_file = disk-or-empty vs content; edit_file = first-occurrence/replace_all splice via indexOf+slice (deliberately not String.replace, which mishandles $ patterns — has a regression test), missing old_strkind:'snippet' old vs new + note; ast_edit = regex/indent splice for function:NAME (async + decorator aware), class:NAME, and naive nesting-aware <tag> (incl. <html> doctype-strip, mirroring proxy/tools.go). All ast predictions marked approximate:true; no match → whole-file fallback + note.
  • New src/ui/diffProvider.ts: DiffProvider, scheme atlas-diff:, stash cap 64 docs. openDiff() puts the basename in the URI for syntax highlighting; openPreview() renders a read-only <name>.diff virtual doc.
  • chatView.ts: dispatch is now async/awaited. Snapshots are taken at TOOL_CALL time (deliberate deviation from permission-time — covers accept-edits/yolo where no permission_request fires), FIFO per tool name. recordAppliedDiff reads on-disk at tool_result; the "View change" chip diffs snapshot vs on-disk exactly, falling back to the edit_file diff_preview when the file is unreadable (workspace mismatch). permission_request predicts the edit before handleRequest; cards get a "View Diff" button + note, and the native notification's "View Diff" re-raises itself.
  • media/chat.js: canDiff / note / diffId params, viewPermissionDiff / viewAppliedDiff inbound.
  • chat.css: .permission-note, .view-diff, .chip-diff.
  • Tests: test/editPreview.test.ts (19 tests). Suite 69/69 green; tsc, eslint, esbuild all clean.

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.
@Anuj-72

Anuj-72 commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Commit 6 — status bar, mismatch detection, error mapping, progress line, CI (final polish)

  • src/ui/statusBar.ts: polls GET /ready for a status bar item, pauses while a turn streams (setStreaming), fetches calibration once at start + on manual refresh. Click opens a quick-pick menu (atlas.statusMenu). 401 → unauthorized (error background); unreachable → warning background. Config atlas.statusBar.enabled + pollIntervalSec (min 5), live-applied via onDidChangeConfiguration.
  • src/workspace/mismatch.ts: passive fs.stat heuristic after the first file-op tool_result, one-time warning with a "don't show again" backed by workspaceState (atlas.mismatchWarningDismissed).
  • src/util/errors.ts: renderError{message, action, prominent}, mapping all 12 proxy error codes from proxy/api_version.go (unauthorized → set-token action), unknown codes passed through verbatim for forward-compat.
  • Progress line: chat.js setProgress + #progress element; chatView posts on llm_prompt_progress (%) / v3_progress / thinking, cleared on turn end.
  • .github/workflows/vscode-extension.yml: paths-filtered to extensions/vscode/**, node 20, SHA-pinned actions, runs type-check + lint + test + production bundle.
  • extension.ts wires DiffProvider + ChatViewProvider + StatusBar; commands atlas.statusMenu / atlas.refreshStatus added.
  • Tests: test/errors.test.ts (8) + test/mismatch.test.ts (17). Full suite 94/94 green; tsc, eslint, node esbuild.js --production all clean.

This completes the v1 scope agreed earlier in the thread.

@Anuj-72
Anuj-72 marked this pull request as ready for review July 19, 2026 14:18
@Anuj-72
Anuj-72 requested a review from itigges22 as a code owner July 19, 2026 14:18
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.

1 participant