Repository navigation
feat(app): jump to an earlier prompt by searching session messages - #63585
Conversation
There was a problem hiding this comment.
The feature works as described. I ran the new e2e test in headless Chromium and it passes: Ctrl+Alt+G and the command palette both open the dialog, it finds a prompt that hasn't loaded into the timeline yet, Escape keeps the scroll position, and Enter jumps to the prompt. The rest of timeline-history.spec.ts passes too, apart from one test that also fails on the base. tsc -b and the nearby unit tests pass.
Public API is unchanged. This is app-only UI, a new session.jumpToMessage command bound to mod+alt+g by default.
Two non-blocking concerns, both inline: the dialog's loading and opening flow isn't modelled as a state machine, which packages/app/AGENTS.md asks for, and jumping to an older prompt loads the entire rest of the session.
| const id = props.session.identity.params.id | ||
|
|
||
| if (!id) return void dialog.close() | ||
| const controller = new AbortController() |
There was a problem hiding this comment.
The dialog's lifecycle (loading history → ready / failed, then opening → jumped / failed) is spread across two createResources, an AbortController, a selection signal and effects that close the dialog. packages/app/AGENTS.md asks for an explicit machine.ts-style state machine for flows like this: a pure transition, a thin driver that owns the abort and timeouts, and transition tests that cover failure, timeout, cancel and dispose. That would also make it simpler to retry after a failure instead of telling the user to reopen the dialog.
There was a problem hiding this comment.
Fixed in acd7f0f. The lifecycle is now a pure machine in jump-to-message-machine.ts (loading → ready / failed, ready → opening → closed). Each state carries an attempt id, so stale results are ignored. A thin driver in dialog-jump-to-message.tsx owns the abort controller and the timeouts, and disposed cancels everything. The two createResources, the selection signal and the close effects are gone. A failed history now shows a "Try again" button, and a failed open returns to the list so selecting the prompt retries. Transition tests are in jump-to-message-machine.test.ts: stale attempts, failure, retry, and dispose.
There was a problem hiding this comment.
Looks good. acd7f0f resolves this thread.
- Machine (
jump-to-message-machine.ts):transitionJumpToMessageis pure. Every async result is checked againstattempt, so a late result from an aborted or superseded load does nothing.closedabsorbs every event, so nothing can jump afterdisposedfires. Aselectedevent while a prompt is opening is ignored, so double-clicks don't start a second page-back. - Driver (
dialog-jump-to-message.tsx): it holds oneAbortControllerand aborts it before each new load. It adds a 60 s timeout onrevealand 30 s on eachmessage.listpage, andcancelaborts in-flight work. Selecting a prompt that's already loaded cancels the background history load before jumping. Thejumpeffect closes the dialog, which runsonCleanupand sendsdisposedwhile the state is alreadyclosed. That's a harmless no-op. - Retry: "Try again" only shows in the
failedstate, and rows from the loaded timeline stay selectable while it's up. After a failed open you're back on the list withopenFailed, and selecting the prompt again starts a new attempt. This matches your description.
I didn't find anything that breaks.
What I checked: I ran the tests in a temporary worktree at pr/head, because the shared repo/ checkout is behind it, then removed the worktree. All 7 new tests in jump-to-message-machine.test.ts pass. The one failure in src/session/commands/ is in export-machine.test.ts: solid-sonner calls a client-only API outside the browser. It fails the same way in the existing checkout, so it isn't from this PR.
Not checked: I couldn't get a clean tsc run because Playwright types are missing in this environment, so the output is full of unrelated component-test errors. I didn't run the e2e spec either.
I didn't change anything in repo/. The bun.lock modification there was already present and I left it alone.
| const [loaded] = createResource(selection, async (messageID) => { | ||
| const signal = AbortSignal.any([controller.signal, AbortSignal.timeout(60_000)]) | ||
|
|
||
| await data.session.message.loadMore(id, { all: true, signal }) |
There was a problem hiding this comment.
Choosing a prompt that isn't loaded yet calls loadMore(id, { all: true }), which pages the entire rest of the session (all messages and parts, 200 at a time) into the timeline store, even when the target is on the next page. On long sessions this can cost a lot of time and memory for one jump. Could it page until the target message is present and then stop? Per packages/app/AGENTS.md, it would also be good to benchmark this timeline path with strobe.
There was a problem hiding this comment.
Fixed in acd7f0f. Choosing an unloaded prompt now calls loadMore one page at a time and stops as soon as the message is in the store. It no longer uses { all: true }. I added an e2e case (jump to message pages back only as far as the prompt) where the target sits among 100 older messages, and it asserts the timeline never pages to the start. I didn't run a strobe benchmark: the change is in the dialog's loading path, not the timeline code, and it only reuses the existing single-page loadMore.
There was a problem hiding this comment.
I checked acd7f0f and the fix does what the earlier comment asked. I didn't run the e2e or unit tests.
What I checked:
- Paging stops at the target.
reveal(dialog-jump-to-message.tsx:137-149) loops untilmessage.get(id, messageID)finds the message. It throws oncemore(id)is false, so it can't spin when the target isn't there. - Timeline loads already in flight are handled. If the timeline is already paging,
loadMore(packages/client/src/solid/data.ts:1881-1894) waits for that load and returns. Thewhilethen checks again, so it doesn't loop hot or fetch twice. - The state machine keeps the two loads apart.
selectedonly startsopen.loadfromready, so the shared controller never cancels the picker's history load. Staleattempts are dropped.
One trade-off for distant targets: one-page loadMore fetches messagePageLimit = 20 (data.ts:92), not 200. Each page publishes on its own, rebuilding messageIndex and running reconcile over the whole array. Jumping to a prompt 2,000 messages back used to take ~10 requests and one publish. Now it takes ~100 sequential requests and ~100 prepends, and each prepend gets re-anchored in the timeline while the dialog is open. The cost is fine when the target is close. When it's far, it's slower than before and grows roughly quadratically with distance.
That's also why I'd question skipping strobe. The change is in the dialog, but 100 prepends of 20 messages each is a different load on the timeline than the one batch it handled before.
A cheap fix is to keep 200-row pages and a single publish, but stop once the target is fetched. A sketch for data.ts:
// options: { all?: boolean; until?: string; signal?; beforePublish? }
limit: options?.all || options?.until ? 200 : messagePageLimit,
...
fetched.push(...response.data.map(stored))
next = response.cursor.next ?? undefined
if (options?.until && response.data.some((m) => m.id === options.until)) break
if (!options?.all && !options?.until) break
} while (next)Then reveal becomes one loadMore(id, { until: messageID, signal }) call followed by the existing checks. Your new e2e still holds, since oldest > 0 keeps passing whenever the target isn't on the first page.
I made no changes in repo/. The local checkout is still at 409ccf8, and the modified bun.lock isn't mine.
|
Handled the two inline concerns from the review summary (state machine for the dialog, and paging only to the target). Details in each thread; fixed in acd7f0f. |


Summary
Open Jump to message from the command palette or with ⌘⌥G (Ctrl+Alt+G on Windows/Linux). Search sent prompts across the current session, including history that has not loaded into the timeline. Enter or click reveals the selected message; Escape preserves the reading position.
Checks
Visual
Same CSV session, light theme, 1280 × 800 viewport, after pressing ⌘⌥G.
dev)Rollout
The command currently ships without a feature flag; the shared flag registry is outside the approved client package scope.
Added by the opencode bot (AI-generated).
Fixes #55672
Related: #52500