Skip to content

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

Open
Anuj-72 wants to merge 15 commits into
itigges22:devfrom
Anuj-72:feat/vscode-extension
Open

feat(vscode): scaffold VS Code extension for issue #35#145
Anuj-72 wants to merge 15 commits into
itigges22:devfrom
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
@itigges22

Copy link
Copy Markdown
Owner

@Anuj-72 Reviewing today- just had to finish up testing on some other updates around some issues in the harness I found.

@itigges22 itigges22 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.

@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-375 vs proxy/tools.go:3289-3315). Also add turn_start to the deliberate drop-list; right now a normal turn spams the output channel.
  • editPreview.ts:124-140 labels a multi-match edit_file prediction 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.onDidDispose handling; a post to a disposed webview mid-turn throws and kills the stream consumption (chatView.ts:618-626). Related: the await this.makeClient() in runTurn sits outside the try, so a SecretStorage failure makes Send silently do nothing.
  • The root .gitignore out/ entry is redundant (the extension-local one covers it) and unanchored; scope it or drop it.
  • mismatch.ts:82/93 treats mtime equality as mismatch; 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.

@itigges22
itigges22 changed the base branch from main to dev July 20, 2026 20:13
@itigges22 itigges22 assigned itigges22 and Anuj-72 and unassigned itigges22 Jul 20, 2026
Anuj-72 added 9 commits July 21, 2026 02:45
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.
@Anuj-72
Anuj-72 requested a review from itigges22 July 21, 2026 14:35
@Anuj-72

Anuj-72 commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@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

  1. done.summary renderede4d5ce0. The done handler now posts a non-empty summary as an assistant-style bubble with a small "done" marker (TUI parity with model.go's chat row). One thing I did beyond the render: turnManager.ts also records the summary as the assistant history entry when no text streamed, so the next turn's replayed history isn't empty after an agentic turn either. Streamed text still wins when both exist. Covered by two new TurnManager tests and a mock-proxy tool-shaped turn.

  2. Deny FIFO desynce4d5ce0. Took your suggestion exactly: all three queues (edit snapshots in chatView.ts, the mismatch detector's pending ops, the webview's pending chips) now consume their oldest entry on permission_denied, and the webview settles any leftover pending chips at turn end as the backstop — that also covers the truncated-args and workspace-boundary gates, which emit neither tool_result nor permission_denied. The denied chip resolves to ✗ "denied" instead of spinning forever. Regression test: denied edit followed by allowed edit pairs with the correct snapshot, no false mount warning.

  3. Body timeout363d6cf. The agent stream now uses a dispatcher with bodyTimeout: 0. To keep zero runtime deps, it's built from the constructor of undici's global dispatcher (Symbol.for('undici.globalDispatcher.1'), initialized lazily via a data: URL fetch) rather than importing undici. On a runtime without that global it degrades to plain fetch instead of breaking. headersTimeout keeps its default, matching the TUI's response-header-only timeout approach.

  4. working_dir363d6cf. Now sends the workspace folder's fsPath; "." only remains as the fallback when no folder is open. The misattributed "TUI convention" comment is gone — thanks for the agent.go:2662 pointer, the Docker path-translation consequence wasn't on my radar.

  5. Settings scoping192c168. "scope": "machine-overridable" on atlas.proxyUrl, atlas.serviceToken, and atlas.permissionMode. Good catch on the token-exfiltration path via the status-bar poll.

Non-blocking items — all six taken:

  • V3 stages (9b74b7a): v3_plansearch / v3_divsampling / v3_select / v3_probe / v3_self_test / v3_plan now drive the progress line (they share the {stage, detail} shape), and turn_start is on the deliberate drop-list.
  • Multi-match edit_file (9b74b7a): predictions with a non-unique old_str and no replace_all are labeled approximate with a note saying the proxy will reject the call (verified against the uniqueness check in tools.go). Regression test added.
  • resolveLocalPath (192c168): absolute paths and .. escapes from tool args are refused; reads stay inside the workspace folder. Callers already degraded gracefully (no diff / inconclusive verdict), so no behavior change on the happy path.
  • Dispose + makeClient (192c168): onDidDispose drops the view reference so mid-turn posts to a disposed webview can't kill stream consumption, and makeClient moved inside runTurn's try so a SecretStorage failure renders an error card.
  • Root .gitignore (9b74b7a): dropped the out/ entry — checked history first, it was added by this branch's scaffold commit, so removal is safe; the extension-local .gitignore covers it.
  • mtime equality (9b74b7a): equal mtimes on write-over/edit now map to inconclusive (silent) rather than mismatch, consistent with the header note's granularity caveat. Tests updated to pin the new verdicts.
  • Docsf1e085c: extensions/ row in docs/MAP.md, an [Unreleased] entry in CHANGELOG.md, and the one-line pointer in docs/API.md's "Building a non-TUI client" section.

One note on the changelog: the version-contract test at this branch's base fails on an [Unreleased] heading, but dev already has the updated test that allows it (a381af6) — so it resolves with the retarget to dev. Retarget + dependabot/CI adjustments on your side sound good.

One more change beyond the review scope: 2c1a0f2 replaces the placeholder activity bar icon with a custom one. Small bit of context — I came across STORY.md in this repo while working through the codebase. Making something small here felt like a fitting way to mark that - hence a Female ChatBot icon. Won't talk much about it. Thank You.

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