Skip to content

Commit 384283d

Browse files
phurynclaude
andcommitted
Multi-session Agent Dashboard + eager non-blocking primer (v1.4.8)
Keeps a pool of live grok sessions instead of one — instant lossless re-focus (each session buffers its webview posts, so focusing replays the buffer with no grok reload or kill), status dots in the history dropdown (a derived, persisted unread badge that survives idle reaping and a VS Code restart), and silent idle-TTL + LRU reaping that never touches the focused or a working/needs-you session. Also fixes the pre-turn primer latency. The hidden plan-mode primer now fires eagerly + non-blocking the moment a session goes live (new, restore, post-/compact) instead of in front of the user's first prompt. The first real send awaits the same in-flight primer turn and is released the instant it acks; the user's bubble shows immediately with a "Grokking…" indicator standing in until grok produces content. The primer was trimmed to v4 — the product-blurb paragraph + repo URL that tempted agentic grok into a 15–40s pre-turn workspace exploration are gone, replaced by an explicit do-not-act / "reply with just ok" constraint. The plan-verdict protocol itself is unchanged. 473 tests (15 new); live gate green (5 passed, image-gen SKIP). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1fb8737 commit 384283d

18 files changed

Lines changed: 1696 additions & 387 deletions

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## 1.4.8 — 2026-06-15
4+
5+
> Run several Grok sessions at once — switch between them instantly, and see at a glance which one needs you.
6+
7+
### Features
8+
9+
- **Multi-session Agent Dashboard.** The sidebar now keeps several sessions *alive* at once instead of one at a time. Switching between them from the history dropdown is **instant and lossless** — the conversation you switch away from keeps running in the background (mid-turn, mid-approval, anything), and switching back replays its exact state with no reload. Picking a session that isn't live anymore loads it from history as before. ([src/sidebar.ts](src/sidebar.ts), [src/session.ts](src/session.ts))
10+
- **Status dots in the history dropdown.** Every session shows a dot so you can see what each one is doing without opening it. It's **gray** at rest, and only lights up when there's something to know: **blue** = working, **yellow** = needs you (a permission, question, or plan to review), **green** = finished with output you haven't opened yet, **red** = finished with an error you haven't opened. The green/red marker is an *unread* badge — it clears the moment you open the session, and it's **persisted**, so it survives the idle cleanup below and even a VS Code restart. Walk away, come back, and the green sessions are exactly the ones with results waiting. ([media/chat.js](media/chat.js), [media/chat.css](media/chat.css), [src/session-pool.ts](src/session-pool.ts))
11+
- **Idle sessions are cleaned up automatically.** To keep a pile of background sessions from each holding a live process, a session left untouched for an hour — or beyond a cap of ~8 live — is quietly shut down (never one that's working or waiting on you). It reappears in history and reloads on click, so nothing is lost. ([src/session-pool.ts](src/session-pool.ts))
12+
- **Updating the Grok Build CLI warns about sessions in progress.** With multiple sessions now able to run at once, the *Update Grok Build CLI* action confirms before it restarts when any session is mid-turn or waiting on you — so an update doesn't silently interrupt work in a background session. ([src/sidebar.ts](src/sidebar.ts))
13+
- **No more long pause before Grok starts.** Sending your first message used to sit silent for 15–40 seconds before anything appeared. Behind the scenes the extension primes each session with a hidden plan-mode instruction, and that primer was running *in front of* your first message and — because Grok Build is an agentic CLI — was wandering off to read files and search the workspace before your real prompt even ran. The primer now fires **the moment a session goes live**, silently in the background, so it's almost always finished before you hit send; if you're quick, your message shows immediately and is released the instant the primer settles. The primer text itself was also trimmed to just the protocol it needs to teach (the product blurb and repo link that were tempting Grok to go exploring are gone), so it completes in a beat instead of dozens of seconds. ([src/sidebar.ts](src/sidebar.ts), [src/grok-primer.ts](src/grok-primer.ts), [src/session.ts](src/session.ts))
14+
- **A "Grokking…" indicator while you wait.** Every turn now shows an animated *Grokking…* placeholder the instant you send, so there's immediate feedback that Grok received your message — it's replaced in place the moment the first thought, reply, or tool action arrives. ([media/chat.js](media/chat.js), [media/chat.css](media/chat.css))
15+
316
## 1.4.7 — 2026-06-15
417

518
> Sharper math, and one-click export for equations and diagrams.

CLAUDE.md

Lines changed: 13 additions & 7 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,36 @@ The active editor is added as an **implicit** chip automatically (toggle with `g
137137

138138
</details>
139139

140+
<details>
141+
<summary><strong>Agent Dashboard</strong> — run several sessions at once, switch instantly, see which need you</summary>
142+
143+
Keep more than one session **alive at the same time**. Start a new session with **+** while another is mid-turn, and switch between them from the history dropdown — the one you leave keeps running in the background (mid-turn, mid-approval, anything), and switching back replays its exact state with **no reload**. Picking a session that isn't live anymore loads it from history as before.
144+
145+
Each row in the dropdown shows a **status dot** so you can see what every session is doing without opening it. It's **gray** at rest and only lights up when there's something to know:
146+
147+
| Dot | Meaning |
148+
|---|---|
149+
| 🔵 Blue | Working — a turn is in flight |
150+
| 🟡 Yellow | Needs you — a permission, question, or plan is waiting |
151+
| 🟢 Green | Finished, with output you **haven't opened yet** |
152+
| 🔴 Red | Finished with an error you haven't opened |
153+
| ⚪ Gray | At rest — idle, already read, or not loaded |
154+
155+
The green/red dot is an **unread** badge: it appears when a session finishes while you're looking at *another* one, and clears the moment you open it. It's persisted, so it survives idle cleanup **and** a VS Code restart — fire off a few agents, walk away, and the green dots are exactly the sessions with results waiting.
156+
157+
To keep a pile of background sessions from each pinning a live process, a session left untouched for an hour (or beyond ~8 live) is quietly shut down — never one that's working or waiting on you — and reloads from history on click, losing nothing.
158+
159+
</details>
160+
161+
<details>
162+
<summary><strong>Instant feedback</strong> — a <em>Grokking…</em> indicator the moment you send, with no startup pause</summary>
163+
164+
Every message you send shows an animated **Grokking…** placeholder immediately, so there's always feedback that Grok received it — it's replaced in place the instant the first thought, reply, or tool action streams in.
165+
166+
There's also no longer a long silent pause before that first response. The extension primes each new session with a hidden plan-mode instruction; that primer now runs **eagerly in the background** the moment the session goes live (and on resume, and after `/compact`) instead of sitting in front of your first message — so it's almost always finished before you hit send. If you *are* quick, your message appears right away and is released the instant the primer settles. The primer text was also slimmed down so it completes in a beat rather than wandering off to read your workspace first.
167+
168+
</details>
169+
140170
<details>
141171
<summary><strong>Session history</strong> — resume, rename, or delete any past session</summary>
142172

@@ -261,7 +291,7 @@ Grok's own **slash commands** (`/imagine`, `/compact`, …) autocomplete in the
261291

262292
The extension is intentionally **thin**: it speaks JSON-RPC over `grok agent stdio` and renders the results. Grok owns sessions, memory, MCP, models, and tool execution; the extension mediates file reads/writes, terminal requests, diff previews, the webview UI — and **Plan Mode**.
263293

264-
Plan Mode is the one place the extension is *not* thin. The CLI's `exit_plan_mode` is unreliable (it reports "approved" to any reply), so the extension enforces planning itself: a **gate** blocks workspace writes and non-read-only commands until you approve, and a hidden **primer** message teaches Grok to read your real verdict (`[Plan approved]` / `[Plan rejected]` / `[Plan cancelled]`) from your next message.
294+
Plan Mode is the one place the extension is *not* thin. The CLI's `exit_plan_mode` is unreliable (it reports "approved" to any reply), so the extension enforces planning itself: a **gate** blocks workspace writes and non-read-only commands until you approve, and a hidden **primer** message teaches Grok to read your real verdict (`[Plan approved]` / `[Plan rejected]` / `[Plan cancelled]`) from your next message. The primer is fired **eagerly and silently** the instant a session goes live (not in front of your first prompt), and is kept lean so it doesn't add a startup pause — your first real message simply waits, in code, for the silent primer turn to finish (Grok runs one turn at a time) and is released the moment it does.
265295

266296
Full diagram, message flow, module map, and design notes: **[docs/architecture.md](docs/architecture.md)**.
267297

docs/architecture.md

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,77 @@ When the panel opens (or you click **+** for a new session):
5252
permission requests, mode changes) back into the chat.
5353

5454
The composer unlocks as soon as the session is live. The extension's hidden
55-
"primer" message (below) is **not** sent at startup — it rides along with your
56-
first real prompt, so there's no startup round-trip to wait on.
55+
"primer" message (below) fires **eagerly and silently** the moment the session
56+
goes live — in the background, without blocking the composer — so it's almost
57+
always finished before you send. Your first real prompt simply `await`s the same
58+
in-flight primer turn (grok runs one turn at a time) and is released the instant
59+
it acks. While any user turn is waiting on grok — including that brief held-behind-
60+
primer gap — the chat shows an animated **Grokking…** placeholder, replaced in
61+
place by the first thought / message / tool card.
62+
63+
## The session pool (Agent Dashboard)
64+
65+
The sidebar shows one conversation at a time, but it keeps a **pool of live
66+
sessions** behind it — one spawned `grok agent stdio` process each, with exactly
67+
one *focused* (the one you see). All the per-session state lives in a
68+
[`Session`](../src/session.ts) object; the sidebar holds `focused` plus a `Set` of
69+
every live `Session` (`pool`). The point is **lossless re-focus**: a backgrounded
70+
session keeps streaming into its own *view buffer* (every webview post that built
71+
its chat, in order), so re-focusing it is a `clearMessages` + replay of that
72+
buffer — no grok reload, no process kill, even mid-turn or mid-approval.
73+
74+
Switching focus (`focusSession`) never touches grok: it swaps `this.focused`,
75+
replays the target's buffer to the webview, and re-pushes the mode/sessions UI.
76+
Clicking a session that *isn't* live (cold — it was reaped, or predates this
77+
window) loads it from grok's on-disk history into a fresh pool member instead
78+
(`openSession`).
79+
80+
Two details make the pool safe:
81+
82+
- **Per-session generation guard.** Each `Session` owns a `gen` counter, bumped
83+
only when *its* client is torn down. Handlers capture their session's `gen` when
84+
wired, so a backgrounded session's in-flight events are never judged "stale" just
85+
because focus moved elsewhere (the old global counter would have done exactly
86+
that).
87+
- **Session-scoped emit.** `emit(session, …)` buffers to that session and only
88+
forwards to the webview when it's the focused one; `post(…)` is for UI-wide
89+
messages (status dots, the sessions list) that aren't tied to one chat.
90+
91+
**Status dots.** Every row in the history dropdown shows a dot. It's **gray** at
92+
rest — and "at rest" is deliberately one bucket: idle, already-read, cold, or
93+
loaded-from-disk all look the same, because the warm-process-vs-cold distinction is
94+
an implementation detail no user should have to reason about. It lights up only
95+
when there's something to know: **blue** working, **yellow** needs-you (a pending
96+
permission / question / plan review), **green** *finished with output you haven't
97+
opened yet*, **red** *finished with an error you haven't opened*.
98+
99+
The green/red dot is an **unread badge**, not a live state. When a session's turn
100+
ends while you're looking at a *different* session, a persisted `unread` flag is set
101+
(in the same `globalState` session-meta that holds rename overrides); opening that
102+
session clears it. Because the flag lives in metadata rather than the live process,
103+
the badge **survives both the idle reaping below and a full VS Code restart** — so
104+
you can fire off several agents, walk away, and come back to find the green dots are
105+
exactly the sessions with results waiting. There's no timer: a session you never
106+
open stays green, because it genuinely *is* still unread. The actual color is a pure
107+
function ([`computeDot`](../src/session-pool.ts)) of `(live status, unread,
108+
unreadError)`, so the policy is unit-tested without a process pool. The host pushes
109+
one changed dot at a time (cheap, no disk read) and the full map on each list
110+
refresh.
111+
112+
**Reaping** ([src/session-pool.ts](../src/session-pool.ts)). A live process per
113+
session isn't free, so the pool is bounded — silently. The pure `selectReapable`
114+
picks victims under two rules: an **idle TTL** (a session untouched for an hour is
115+
torn down, swept every 5 min) and an **LRU cap** (at most ~8 live; the
116+
least-recently-used eligible sessions are evicted past it). It **never** reaps the
117+
focused session or a `working`/`needs-you` one — so the cap can be exceeded when
118+
everything spare is busy, by design. Reaping just kills the process and recomputes
119+
the dot — a reaped session that's still unread **stays green**, a read one goes
120+
gray — and re-clicking the row reloads the session from disk.
121+
122+
One safety valve sits next to this: the explicit **Update Grok Build CLI** action
123+
tears down every live session to swap the binary, so it now confirms first if any
124+
session is `working` or `needs-you` (the silent startup auto-update runs before
125+
anything is in flight, so it doesn't ask).
57126

58127
## Plan Mode — the one part that isn't thin
59128

@@ -76,15 +145,26 @@ the extension can't trust the wire verdict. Two mechanisms cover the gap:
76145
the real decision from the **next** user message instead, as a bracketed
77146
marker: `[Plan approved]` / `[Plan rejected]` / `[Plan cancelled]` (optionally
78147
followed by a free-form comment). Approve → drop the gate + send "implement it
79-
now"; Keep planning → the gate stays up. The primer is sent **lazily**
80-
prepended as its own hidden turn right before the first real prompt, on both
81-
new **and** restored sessions. It is *re-sent* on the first send after a
82-
restore: a primer buried in replayed history isn't reliably honored (a
83-
`/compact` can drop it from effective context), so the extension re-asserts it
84-
rather than trusting the replay. When grok replays an earlier primer as a user
85-
message on restore, the pure `isPrimerText()` helper detects it so the bubble
86-
is hidden and not counted toward plan positions — but that detection does
87-
**not** mark the session primed.
148+
now"; Keep planning → the gate stays up. The primer fires **eagerly and
149+
non-blocking** (`ensurePrimed`) — its own hidden turn, kicked off the moment a
150+
session goes live (new **and** restored, and after `/compact`) rather than in
151+
front of the user's first prompt. It returns a reused `session.primingPromise`,
152+
so a first send that races the background primer awaits the *same* turn and is
153+
released when it acks. It is *re-sent* on go-live after a restore: a primer
154+
buried in replayed history isn't reliably honored (a `/compact` can drop it from
155+
effective context), so the extension re-asserts it rather than trusting the
156+
replay. The silent turn is hidden by a session-level `suppressContent` flag,
157+
which deliberately lets `userMessage`/`agentStart` through so a racing user send
158+
still paints its own bubble + Grokking indicator. **Primer v4** is kept minimal
159+
on purpose: grok-build is agentic, and the old v3 primer's product-blurb
160+
paragraph + repo URL + "acknowledge briefly" line were tempting grok into a
161+
15–40s pre-turn exploration of the workspace before the user's message even ran
162+
— so v4 keeps only the plan protocol and adds an explicit *do not use tools /
163+
read files / search the workspace / take any action; reply with just `ok`*
164+
constraint. When grok replays an earlier primer as a user message on restore,
165+
the pure `isPrimerText()` helper detects it so the bubble is hidden and not
166+
counted toward plan positions — but that detection does **not** mark the session
167+
primed.
88168

89169
The full pedagogical write-up lives in
90170
[research/understanding-plan-mode.md](../research/understanding-plan-mode.md).
@@ -96,6 +176,8 @@ The full pedagogical write-up lives in
96176
| [src/extension.ts](../src/extension.ts) | Entry point — registers commands, keybindings, output channel |
97177
| [src/sidebar.ts](../src/sidebar.ts) | Webview provider, message routing, fs handlers, diff preview, logout, generated-media serving (`postGeneratedMedia``asWebviewUri`, base64 fallback) |
98178
| [src/acp.ts](../src/acp.ts) | ACP client — spawns CLI, manages session lifecycle, emits events |
179+
| [src/session.ts](../src/session.ts) | Per-session state bag — one `Session` per live `grok agent stdio` process (the sidebar holds a *pool* of these + one focused) |
180+
| [src/session-pool.ts](../src/session-pool.ts) | Pure reaping policy (`selectReapable`) — idle-TTL + LRU cap over the live-session pool |
99181
| [src/acp-dispatch.ts](../src/acp-dispatch.ts) | Pure protocol helpers — line parsing, update routing, response + generated-media extraction (`isMediaGenToolCall`/`extractGeneratedMediaPaths`) |
100182
| [src/cli-locator.ts](../src/cli-locator.ts) | Locate the `grok` binary; cross-platform |
101183
| [src/terminal-manager.ts](../src/terminal-manager.ts) | Headless shells for the agent's `terminal/*` calls |

media/chat.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,19 @@ code {
337337
word-wrap: break-word;
338338
}
339339

340+
/* "Grokking…" — the generic waiting placeholder. Matches the Thinking header's
341+
muted font + animated loading-dots, but without the expand chevron (it carries
342+
no body) and is not interactive. Replaced in place by real content. */
343+
.grokking {
344+
display: inline-flex;
345+
align-items: center;
346+
color: var(--vscode-descriptionForeground);
347+
font-size: 11px;
348+
padding: 3px 10px;
349+
margin: 2px 0;
350+
user-select: none;
351+
}
352+
340353
/* ---- file chips in user message history ---- */
341354

342355
.msg-chips {
@@ -1447,6 +1460,20 @@ body.dragging { outline: 2px dashed var(--vscode-focusBorder); outline-offset: -
14471460
}
14481461
.history-row:hover { background: var(--vscode-list-hoverBackground); }
14491462
.history-row.active { background: var(--vscode-list-activeSelectionBackground); }
1463+
.history-row-dot {
1464+
flex: 0 0 auto;
1465+
width: 8px;
1466+
height: 8px;
1467+
border-radius: 50%;
1468+
/* Gray "at rest" is the default — idle, already-read, cold, and loaded-from-disk
1469+
all look the same; only the states worth knowing about light up. */
1470+
background: var(--vscode-descriptionForeground);
1471+
}
1472+
.history-row-dot.dot-none { background: var(--vscode-descriptionForeground); }
1473+
.history-row-dot.dot-working { background: var(--vscode-charts-blue); }
1474+
.history-row-dot.dot-needs-you { background: var(--vscode-charts-yellow); }
1475+
.history-row-dot.dot-unread { background: var(--vscode-charts-green); }
1476+
.history-row-dot.dot-error { background: var(--vscode-errorForeground); }
14501477
.history-row-main { flex: 1; min-width: 0; }
14511478
.history-row-name {
14521479
font-size: 13px;

0 commit comments

Comments
 (0)