Imported from phnx-labs/agi-ext (
AGENTS.md). Install upstream withnpx skills add phnx-labs/agi-ext. Copyright stays with the author.
AGI EXT
The VS Code / VSCodium extension for agents-cli. This repo is ONLY the extension: the CLI,
daemon, session index, and every mechanism live in agi-cli. The extension is a presentation
layer over one agents feed watch --json stream; every user action leaves it as exactly one
CLI verb. Product requirements: docs/PRD.md. Architecture, module graph, debt: docs/ARCH.md.
Publisher identity is FROZEN:
publisher: swarmify,name: swarm-extinpackage.json.swarmify.swarm-extis the authority in theswarm-ext://URI the CLI emits.
Core concepts
- Session: a harness conversation the CLI indexes, identified by its canonical CLI id.
A tab has an extension-local terminal id (
CL-<ts>-<n>) until the stream reveals the session. - Stream row (
SessionWatchRow): the CLI's projection of one session (phase, label, host, request, timeline, files, goal, checklist). Every fact on screen comes from a row. - Attention / needs-you: the CLI's open attention items with the harness's own choices.
The extension never classifies; membership is
attention.state === 'open'. - Leader and followers: one elected editor window owns the stream child and broadcasts it over a Unix socket; other windows follow. The child exists only while some window demands it.
- Presentation store: each window's projection of the stream (rows, attention, activity, per-scope reset state). Selectors derive views; it holds no lifecycle logic.
- Terminal identity: the registry binds a
vscode.Terminalto its session from stream facts; two live claims on one terminal are ambiguous until the stream removes one. - One-shot verbs: launch, resume, fork, stop, answer are each one
agents …command, run once per gesture. The CLI daemon is the only scheduler.
Layout
src/core pure presentation logic, no vscode import
src/monitor stream leader, demand gate, broadcast, readiness (debt)
src/vscode commands, terminals, the two sidebars, URI handler
src/shared pure session presentation the sidebars' webview scripts reuse, e.g. sessionCards.ts
ui/editor Reader custom editors (React)
scripts/ build.sh / install.sh / release.sh: the only entry points
Entry points
bun test --timeout 30000 (suite; ui/ from ui/) · bun run lint:dead (knip, wired into
scripts/build.sh) · bun scripts/gen-docs.ts (regenerates docs/reference.{html,json}
from package.json and ui/; --check, run by scripts/build.sh and the suite, fails while
they are stale) · scripts/build.sh <ver> (VSIX) · scripts/install.sh (local) ·
scripts/release.sh <ver> --confirm (Marketplace + Open VSX; routes to the box holding the
vs-marketplace secrets; needs a CHANGELOG section). Never hand-roll vsce/ovsx publish.
Invariants (source of truth in the file named)
- Exactly one
agents feed watch --jsonchild per machine, spawned by the leader only while a window demands it:MonitorHostinsrc/monitor/host.ts,SessionStreamDemandinsrc/monitor/sessionDemand.ts(enforced:host.demand.test.ts). The always-on version cost 31% CPU and leaked ssh fan-out; a side-channel fetch caused the 2026-08-25 CPU incident. - A reset replaces a scope; an event applies only if its sequence is newer;
hasSeenResetis per scope:SessionPresentationStore.applyinsrc/core/sessionPresentationStore.ts. - A missing field goes onto the CLI's
SessionWatchRow, never into a client fetch, transcript read, or lazy enrich. The sidebar (src/vscode/agentPanel.vscode.ts) renders synchronously from the store and honestly shows "Waiting for the session stream…" or the CLI'sunavailablereason; it never fakes an empty card. One named exception: a timeline filter too large for every row (agent messages, tool calls, reasoning) is onesessions <id> --include <role> --jsoncall on an explicit click (the chip, or Refresh once newer activity arrives), never on selection or a timer, and only for a session on this machine (loadTimelineFilter). A second: clicking an artifact the row names for a remote session copies that one file into~/.agents/.cache/remote-artifacts/<device>/with oneagents ssh <device> --argvcall (localArtifactUri); any other path, or one that leaves that folder, is refused. The Project panel opens an artifact through the same path:openListedArtifact(src/vscode/sessionCardActions.ts) finds the row by session id AND host and copies only an artifact that row lists. Nothing loads on selection. A third: a ticket row in the Project panel's Issues tab opens its Linear page as a tab and peeks at the description the Issues read already carried (renderTicketPeek,src/core/ticketPeek.ts); it reads nothing. A ticket tile for a ticket that read does not hold reads that one ticket withlinear tasks <ID> --json(ProjectPanelController.openTicketPage) for its page, on the click only, never on a timer. Comments live on the Linear page; the extension has no comment box. Everylinearcall (runLinear) setsLINEAR_PROFILEto the project'slinear.profile(defaultwhen it has none) and names the Linear project by id (--project <id>), so no folder picks the workspace. A fourth: the Project panel's goal lines are onelinear goals --jsonread when the panel shows (at most once a minute) or on Refresh (ProjectView.loadGoals); never per tab or per click. A fifth: merging a PR reads that one PR (agents projects prs <name> --json --repo R --number N) on the click for its head commit and whether you may bypass protection (src/vscode/prMerge.vscode.ts). A Review row's Merge is one click with the Settings method after an in-page "Undo · 5" (mergeNow), and only at the head commit the list read; the sidebar's Merge ▾ reads again after its dialog (mergeWithMenu);--adminonly after its own warning dialog. A sixth: ticket tiles show the title and state a Linear read in this window already returned (src/core/ticketFaces.ts); the Issues read (linear tasks --project <id> --json) also runs when Sessions shows, under the same once-a-minute cache. A tile never reads Linear. A seventh: the Project panel's project menu is oneagents projects list --jsonand onelinear projects --jsonread when it opens: every live Linear project through the agents project that links it, then agents projects whose link is missing or gone (projectMenu); detection only checks a row, and the pick is remembered per workspace (the list is cached a minute insrc/vscode/projectRegistry.vscode.ts, also feeds the repository-path stopgap when Sessions or the resume picker first draws); Review readsagents projects prs list <name> --json --repo <r>per repository in parallel, drawn as each answers. The reply box's mic (and the Lead's, which reuses it) is the one place the extension runs tools that are not the CLI: on the click,ffmpegrecords the default microphone andwhisper-clitranscribes on this machine (src/vscode/dictation.vscode.ts); nothing is sent anywhere. A request's pasted images are looked up by file name in this machine's~/.agents/.history/attachments/, where the CLI saved them. An eighth: ⌘K's artifact index (src/vscode/searchPalette.vscode.ts) runsartifacts list --json, reads the first 512 KB of the rendered HTML beside each source (title andagents://session/<id>; re-read only on a new mtime) andsessions artifacts <id> --jsonfor this machine's newest sessions, in the background when the picker opens, at most once a minute; the picker never waits on it. A ninth: Settings > Devices is oneagents devices list --jsonread when Settings shows (at most once a minute), on Refresh, and after a checkbox or star; each checkbox or star is oneagents devices enable|disableoragents devices config <name> auto-launch.preferredargv (deviceSettingArgv,src/core/panelSettings.ts). It draws the CLI'sautoSkip; it never decides placement. A tenth: Close Terminal With Recap finds the transcript with onesessions <id> --jsonread when the command runs (transcriptPathOf,src/vscode/recap.vscode.ts), so it works while no view demands the stream; the extension reads no transcript itself. - Both sidebars patch their content in place on every update (
patchChildren,PATCH_SCRIPTinsrc/shared/sessionCards.ts); the Active Session sidebar builds the reply box once; reassigningroot.innerHTMLper update made it jitter and erased what the owner was typing. A reused node keeps its listeners, so bindings go throughbindOnceand read their data-* values on click. A card carriesdata-key(pairs it with its own node) anddata-h(an unchanged hash is not patched); every thumbnail is<img data-thumb>filled fromTHUMB_SCRIPT's 480 px cache, never a full-sizesrc(the flicker in the 2026-10-13 video;sessionCards.patch.test.tsruns the real patch in happy-dom). - The Active Session sidebar is the one preview: a Project panel session click sets its
subject (
setAgentPanelSubjectFromProject), brings the tab this window binds to the session forward wherever its agent runs (select; Settings > Card click "Only show it" keeps it; ⌘K always fronts it), and spawns or resumes nothing. No per-selection fetch. A plain shell in front clears the subject, so the sidebar names the shell (followFrontTerminal). - Launch composition lives in
buildAgentLaunchCommand(src/core/agents.ts),harnessLaunchRegistrationsandbuildTabLaunchCommand(src/core/launchTarget.ts); no other code builds anagents runlaunch or starts a harness binary. A relaunch (Clear) stays on its tab's machine.--cwdis the exact open workspace, emitted once for every variant except Pick Project, which emits--project; theharness@account picker never combines with--strategyor a pinned version; the extension never scores devices or accounts and never resolves a target throughagents projects view <cwd>. - The front tab and every tab number come from
src/vscode/frontTab.vscode.ts: both tab events feedonDidChangeFrontTab, and a tab maps to its terminal by identity (assignTabs,src/core/tabIndex.ts), never by label alone. - A session has one name,
streamTitle(src/core/projectPanel.ts), on its tab, card, status bar, the Active Session sidebar and the resume picker. The extension generates no title and reports none back to the CLI; Rename Session types Claude's own/rename. - What a card says is computed once and both sidebars read it: a PR's word is
prVerdict(src/core/prStatus.ts); a state's dot colorSTATE_TONEand the time words are insrc/shared/sessionState.ts; the theme tokens and.card(RUSH_THEME_CSS), the escaper and the GitHub and Linear marks are insrc/shared/sessionCards.ts; harness logos come fromharnessLogoCss. A webview script reuses a host function by interpolating it (${relTime}), never a copy. - Every agent tab is created by
openTrackedTab(src/vscode/extension.ts): env, icon, registry facts, readiness and a 10 s bounded pid wait in one place; the only othercreateTerminalis the agents-cli installer. A tab the editor opened is registered byterminals.adopt(src/vscode/terminals.vscode.ts), on activation and after it. A prompt goes into a TUI only throughsubmitToTui(typed, then\ras its own keystroke). - Terminal rebind runs only on pushed reset/row facts, scoped to observer device and host;
start time is not ownership; a
previous: truerow cannot reclaim a tab (src/vscode/terminals.vscode.ts,src/core/remoteSessions.ts). - The Lead is agents-cli's process; the extension is a thin client (
src/core/lead.ts,src/vscode/lead.vscode.ts,src/vscode/leadTab.vscode.ts). Ask and Confirm are each oneagents lead ask|confirm --jsonchild, one turn at a time per window, killed after 120 s of silence or 15 min, or when the window closes; Esc never stops a turn. The Lead tab readsagents lead history|status --jsonwhen it opens and on Refresh, never on a timer.~/.agents/lead/surface.jsonis shared with FastWispr: while it saysbubble, ⌥Space is FastWispr's (agents.lead.bubblegates the keybinding); the extension never creates~/.agents/leadjust to watch it. A held ⌥Space is one press (PressGate), and the Lead's mic never takes over the reply box's recording (nextMic, a pure transition insrc/core/leadInput.ts). Tests run against the fake CLI insrc/core/testdata/lead-bin/agents. - Closing a tab sends
agents ps stop <id>only onTerminalExitReason.User(src/vscode/terminalReadiness.ts); reload and restore never kill a session. - Your move is only an open ask, stall or failure (
sessionStateOf,ballOfinsrc/shared/sessionState.ts); a finished turn that asked nothing is Idle. Asks order by the CLI's attentionurgency(urgencyOf; absent reads normal), then longest wait, thenATTENTION_KIND_RANK; the extension never computes urgency. A stall is a CLI signal,FloorPhasehas nostalledmember. Idle plus a non-empty feedpendingis Waiting (the agent's ball, never Your move);subAgentCountis cumulative and never decides liveness. - A card answers an ask with one
agents feed answer <attention.key> --choice|--text … --jsonat once, or after a 5 s Undo in the page when Settings > Undo asks for it (answerArgv,src/core/projectPanel.ts;data-undoon the ask), only for a key the stream shows open; the same click fronts that agent's tab through the card's ownselect. The extension posts no notifications of its own; banners are the AGI Menu's. - Project attribution is the CLI's own
confirmedProject(projectSessions,src/core/projectPanel.ts); the extension never guesses a project from a cwd. One stopgap until agents-cli aliases ship: a row whose checkout is one of the project's own repository paths is listed too, only when itsconfirmedProjectis empty or no registered project's name (projectFallback); a session another live project claims stays there. The Project panel and the resume picker apply the same rule. Delete it when the feed resolves old names. - One shell quoter (
shellQuote,src/core/shellQuote.ts) and one binary resolver and exec path foragents, the standalonesessions/secretsit installs, andlinear(resolveAgentsBin,runCliArgs,runAgents,src/core/agentsBin.ts). - Harness ids, labels, prefixes derive from
src/core/agents.cli.tsandPRESENTED_AGENTSinsrc/core/agents.ts; no hand-written list anywhere else,ui/editorincluded. src/corenever importsvscode; the only test double issrc/testing/vscodeDouble.ts.- A missing CLI noun is an upgrade error, never a filesystem fallback.
Design
The owner runs hundreds of sessions and catches up by glancing. Every surface answers "what is going on, where, and what came out of it" in one look; text explains only what a picture can't.
- Glance, then detail, with nothing moving. A card always shows the same preview: the harness logo and title, its state as the card's tint (red asks or blocked, green working or waiting, yellow idle or review; a question's wait ticks where the age sits), where it runs, the question or the agent's latest words in two lines, previews of images, and one row of chips with each service's own mark that scrolls sideways. Settings > A card shows turns each part off, down to the logo and title on one line (owner video, 2026-10-17). Hover only reveals icons; a card never grows under the pointer (owner video, 2026-10-12). The question opens to its full text on a click on it. Everything else a session made lives in the Active Session sidebar.
- Show, don't label. App icons (VSCodium, Ghostty) plus the workspace name (
agents,agents-cli), never "This window" or "Window 2". Thumbnails for images, chips for PRs and tickets, a tint for state. The tint is not the only signal: the word is in the card's tooltip and read to screen readers, and Settings > State word puts it back on the card. - Inputs are not outputs. What the user attached is part of their message and is marked
"attached". Artifacts are what the agent made: PRs, folders under
.agents/artifacts/, plans, pages, rendered images. A count of edited files is neither. - One card style. Outlined 1 px hairline cards with even gaps; no colored side borders (the one exception: the selected card's gold rail, so selection never looks like hover), no drop shadows (shadows are for menus, popovers and the floating reply, which sits over the panel). Hover shifts tone in place; nothing lifts. Gaps inside a card are at most half the gap between cards.
- Motion only for liveness and arrival. The Working dot breathes, a question's dot rings
slowly, and a question that just arrived glows three times; everything stops under
prefers-reduced-motion. Nothing flashes for as long as it waits. - Sort, show, group in one ⇅ menu with checkmarks, as in the AGI Menu. Group headers carry the icon, a count and one dot per state, so a folded group still says what is inside. Your move (asks · idle) is pinned above every grouping, a question first.
- Two header rows. The Project panel's header is the project with its goal beside it, then the tabs with that tab's own controls (☀, search, ⇅) at the right; no heading row of its own. Settings is the gear in the view's title bar, Refresh lives in Settings, and ⌃1–⌃4 switch the tabs (owner video and screenshot, 2026-10-17).
- A click selects and brings its tab forward; it never resumes. A card click shows the session and fronts its tab when the tab is in this window; switching tabs moves the selection back. Anything else is an explicit icon: focus (↗) or resume (▶); Close the tab is in ⋯, out of a stray click's way (with Undo; it asks first only when the agent is working or waiting), and multi-select closes several.
- Prototype first, with real data. A visual change starts as a prototype built from the live stream and the owner's real sessions, opened for them, rendered and inspected by you, before any extension code. Mock data and screenshots you did not look at prove nothing.
Known debt
The thin-client violations still in the tree are listed with their fate in docs/ARCH.md
("Findings"): watchForPlan,
sessionParse.ts, readinessDetector.ts (the one copy of every readiness probe; a window
runs its own while the monitor is down). Touching one means
deleting or shrinking it, never extending it.
Four tests assert on source text (autoLabelHydration: stream rebind before the sidebars
refresh; statusBarIdentity.spawnStorm: identity only from the store;
agentPanel.webview-syntax: the sidebar script parses and uses theme tokens;
pollTimers.replacement: no poll timer in the sidebar); read what each protects first.
Conventions
AGENTS.mdis canonical;CLAUDE.mdis a symlink to it. Edit onlyAGENTS.md.- Worktree under
.agents/worktrees/<slug>/+ PR for every change; never commit tomain. - No stubs, no fallbacks, no second scheduler, no mocks beyond
vscodeDouble. A DOM implementation (happy-dom) for running real webview scripts in tests is not a mock. - Every user-visible change updates
CHANGELOG.md; a behavior change updatesdocs/PRD.mdordocs/ARCH.mdin the same PR, thendocs/render.sh.
