Skip to content

Commit 3e89706

Browse files
committed
Session-history pagination + popover/empty-session fixes (v1.4.10)
1 parent 91d3aaa commit 3e89706

13 files changed

Lines changed: 1300 additions & 104 deletions

CHANGELOG.md

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

3+
## 1.4.10 — 2026-06-18
4+
5+
> Session history that stays fast with thousands of sessions.
6+
7+
### Features
8+
9+
- **Session history loads in pages and stays fast at scale.** The history dropdown used to read and parse *every* saved session on each open, which got slow once a project had hundreds or thousands of them. It now loads the **most recent 100** (newest first by last activity) and pulls in older ones as you **scroll to the bottom**. The **search box** filters by name across your **entire** history — not just the loaded page — so you can still find an old session instantly. Behind the scenes it orders sessions with one cheap directory `stat` each (no file reads), reads only the page you're looking at, and caches by file modification time so re-opening the dropdown costs effectively no disk reads. ([src/sessions.ts](src/sessions.ts), [src/sidebar.ts](src/sidebar.ts), [media/chat.js](media/chat.js), [media/chat.css](media/chat.css))
10+
- **Switching model or reasoning effort on a fresh session no longer clutters history.** Some model and effort changes need the session to restart. If you flip them a few times right after opening a session — before you've actually said anything — each restart used to leave behind an empty, identical session in your history. Now an empty session (one where only the hidden setup has run) restarts cleanly with no "Summarize & Restart vs. Just Restart" prompt, and the throwaway session is removed instead of piling up. If you had renamed that session, the name carries over to the restarted one. ([src/sidebar.ts](src/sidebar.ts), [src/sessions.ts](src/sessions.ts))
11+
12+
### Fixes
13+
14+
- **History dropdown no longer opens clipped off the right edge.** Opening the session-history popover quickly (before its rows had finished loading) could position it too far right, so it spilled past the panel edge and only looked right after closing and reopening. The popover is now right-aligned to the panel (respecting the edge padding) and grows leftward, so it stays fully on-screen no matter how its contents resize as sessions load in. In a narrow panel it also caps its width to fit, so a long session name truncates with an ellipsis instead of pushing the popover off the left edge. Resizing the panel while the dropdown is open now re-fits it live (no need to close and reopen), and switching to another panel tab or extension closes it so it can't reappear mis-sized when you come back. ([media/chat.js](media/chat.js))
15+
16+
### Internal
17+
18+
- **Opt-in performance simulation for the history popover.** A new `npm run test:perf` suite (kept out of `npm test` and CI) builds a 5000-session in-memory store and asserts the access-count improvement: first open drops file reads from 5000 to 100 (~98%), a repeat open does zero reads (modification-time cache), and search warms the catalog once then stays read-free — with a modeled-latency projection and a real in-memory parse-cost wall-clock. ([test/sessions.perf.ts](test/sessions.perf.ts), [vitest.perf.config.ts](vitest.perf.config.ts), [package.json](package.json))
19+
20+
### Docs
21+
22+
- Documented the pagination design in [docs/architecture.md](docs/architecture.md) (§ History at scale) and [CLAUDE.md](CLAUDE.md) (§ History pagination), and updated the *Session history* feature note in the [README](README.md).
23+
324
## 1.4.9 — 2026-06-16
425

526
> Make the chat bigger — just the chat.

CLAUDE.md

Lines changed: 26 additions & 10 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,9 @@ There's also no longer a long silent pause before that first response. Plan Mode
170170
</details>
171171

172172
<details>
173-
<summary><strong>Session history</strong> — resume, rename, or delete any past session</summary>
173+
<summary><strong>Session history</strong> — resume, rename, delete, or clear past sessions</summary>
174174

175-
The clock icon lists every session the CLI saved for this project. Click a row to resume — Grok replays the conversation, with inline images, plans, and reasoning intact. Hover to rename (pencil) or delete (trash); names default to the first message. Renames are stored by the extension and never touch Grok's own files.
175+
The clock icon lists the sessions the CLI saved for this project, most recent first. Click a row to resume — Grok replays the conversation, with inline images, plans, and reasoning intact. Hover to rename (pencil) or delete (trash); names default to the first message. The list loads the **most recent 100** and pulls in older ones as you **scroll**, and the **search box** filters by name across your whole history — so it stays fast even with thousands of sessions. **Clear all history** at the bottom of the dropdown removes every session for this project in one step (after a confirm), keeping the one you're currently in. Renames are stored by the extension and never touch Grok's own files.
176176

177177
</details>
178178

docs/architecture.md

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ The full pedagogical write-up lives in
187187
| [src/chips.ts](../src/chips.ts) | File-chip CRUD (pure) |
188188
| [src/prompt-builder.ts](../src/prompt-builder.ts) | Chip → prompt-string with `@path` refs and fenced blocks (pure) |
189189
| [src/slash-filter.ts](../src/slash-filter.ts) | Slash-command autocomplete filter (pure) |
190-
| [src/sessions.ts](../src/sessions.ts) | Disk-driven session listing/delete + name overrides (pure) |
190+
| [src/sessions.ts](../src/sessions.ts) | Disk-driven session listing/delete + name overrides (pure) `indexSessions` (stat-only ordering), `readSessionEntries` (windowed read), `listSessions` (whole-list), `clearSessions` |
191191
| [src/file-ref.ts](../src/file-ref.ts) | Open-file ref parsing + large-file inline-read guard (pure) |
192192
| [src/plan-review.ts](../src/plan-review.ts) | Plan-snapshot Markdown filename generation (pure) |
193193
| [src/voice.ts](../src/voice.ts) | Voice-input pure helpers — STT request/response, ffmpeg args, device parsing, key resolution |
@@ -196,6 +196,41 @@ The full pedagogical write-up lives in
196196
| [media/chat.{js,css}](../media/) | Webview UI |
197197
| [media/webview-helpers.js](../media/webview-helpers.js) | Pure webview helpers (file-ref detection, relative-time, mic-button state machine, trailing send-phrase highlight, math extraction `splitMath`/`stripUnsupportedTex`, and the deferred subagent classifier `isSubagentToolCall`/`subagentLabel`) — shared between webview and tests |
198198

199+
## History at scale
200+
201+
The history dropdown lists every session the CLI saved for this workspace, and that
202+
store can grow into the thousands. The old path read and `JSON.parse`d *every*
203+
`summary.json` on every open, then rendered every row — linear cost that stalled the
204+
popover at scale. It now loads **one page at a time** (`SESSION_PAGE_SIZE = 100`,
205+
newest-first), built from two pure primitives in
206+
[src/sessions.ts](../src/sessions.ts):
207+
208+
- `indexSessions` does **one `stat` per session dir, no reads** — it orders every id
209+
newest-first by `summary.json` **mtime**. mtime is the cheap last-activity proxy:
210+
grok rewrites that file (it holds `updated_at`) on every turn. We sort by mtime
211+
*because the id is a UUIDv7 whose timestamp is creation, not last activity* — an
212+
id-sort would order by when the session was first opened, which is wrong.
213+
- `readSessionEntries` reads + parses `summary.json` for **exactly the visible page's
214+
ids** and applies name overrides.
215+
216+
The host (`postSessionsList` in [src/sidebar.ts](../src/sidebar.ts)) orders everything
217+
cheaply with `indexSessions`, then drives an **mtime-keyed read cache** so a re-open /
218+
load-more / search only re-reads entries whose `summary.json` actually changed —
219+
steady-state opens cost ~zero reads. **Search is server-side and complete**: a query
220+
warms the whole catalog once (cache-backed) and filters by display name across *all*
221+
sessions, not just the loaded page. One wrinkle the disk scan can't cover on its own:
222+
a *brand-new* session has no `summary.json` yet, so opening history the instant a
223+
session goes live would drop the active row until grok flushes the file. The host fixes
224+
that by synthesizing a top-pinned row from in-memory state for any live session not yet
225+
on disk (first, unfiltered page only — those ids can't appear on a later page). The
226+
webview appends pages on scroll-near-bottom (de-duped by id, one request per boundary)
227+
and debounces the search box. An opt-in
228+
perf simulation ([test/sessions.perf.ts](../test/sessions.perf.ts) via
229+
`npm run test:perf`, kept out of `npm test`/CI) asserts the op counts at N=5000: first
230+
open drops reads 5000→100 (~98%), steady-state re-open is 0 reads, search warms once
231+
then 0. **Clear all** remains the relief valve for an overgrown store; pagination is
232+
the steady-state fix.
233+
199234
## Design choices worth knowing
200235

201236
- **Pure modules split for testability.** Everything tagged "(pure)" above has no
@@ -227,7 +262,13 @@ The full pedagogical write-up lives in
227262
`grok.defaultModel` and restarts — `newSession` re-applies the model *before* the
228263
primer runs, while the agent is still rebindable. No history → transparent
229264
restart; with history → the same Summarize / Just-Restart choice as an effort
230-
change.
265+
change. A restart on a *primer-only* session (no real conversation — common when
266+
you flip models/effort right after opening) takes the no-prompt path **and**
267+
discards the abandoned grok session dir afterward, so repeated switches don't pile
268+
up identical empty sessions in history; the pure `carrySessionName` moves any user
269+
rename onto the fresh session so the chosen name survives. The same cleanup runs on
270+
the effort-change empty-session branch, guarded so a dead client on a session *with*
271+
history keeps its history.
231272
- **Generated media is path-based, not an ACP image block.** `/imagine` and
232273
`/imagine-video` write a file into the session dir and report its *path* as
233274
JSON-in-text on the completed tool result. The host parses the path, classifies

media/chat.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,6 +1457,36 @@ body.dragging { outline: 2px dashed var(--vscode-focusBorder); outline-offset: -
14571457
font-size: 12px;
14581458
text-align: center;
14591459
}
1460+
.history-more {
1461+
padding: 8px;
1462+
color: var(--vscode-descriptionForeground);
1463+
font-size: 11px;
1464+
text-align: center;
1465+
opacity: 0.7;
1466+
}
1467+
.history-footer {
1468+
padding: 4px;
1469+
border-top: 1px solid var(--vscode-editorWidget-border, rgba(255,255,255,0.08));
1470+
}
1471+
.history-clear-all {
1472+
display: flex;
1473+
align-items: center;
1474+
justify-content: center;
1475+
gap: 6px;
1476+
width: 100%;
1477+
padding: 6px 8px;
1478+
font-size: 12px;
1479+
font-family: inherit;
1480+
color: var(--vscode-descriptionForeground);
1481+
background: transparent;
1482+
border: none;
1483+
border-radius: 4px;
1484+
cursor: pointer;
1485+
}
1486+
.history-clear-all:hover {
1487+
color: var(--vscode-errorForeground);
1488+
background: var(--vscode-toolbar-hoverBackground, rgba(255,255,255,0.08));
1489+
}
14601490
.history-row {
14611491
display: flex;
14621492
align-items: center;

0 commit comments

Comments
 (0)