Skip to content

feat(app): jump to an earlier prompt by searching session messages - #63585

Merged
usrnk1 merged 3 commits into
devfrom
jump-to-message
Nov 12, 2026
Merged

usrnk1 merged 3 commits into
devfrom
jump-to-message

Conversation

@usrnk1

@usrnk1 usrnk1 commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

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

  • Repository check (35 tasks), app typecheck, changed-file lint, and 30 focused unit tests passed.
  • Focused browser regression for paginated history: 1/1 passed.
  • Known baseline issues from review: the separate E2E typecheck reports UI CSS/Vite type errors; two other timeline regression cases also fail on the base.

Visual

Same CSV session, light theme, 1280 × 800 viewport, after pressing ⌘⌥G.

Before (dev) After
Transcript on dev Jump to message showing sent prompts

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

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fixes #55672
Related: #52500

Before

After

const id = props.session.identity.params.id

if (!id) return void dialog.close()
const controller = new AbortController()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. acd7f0f resolves this thread.

  • Machine (jump-to-message-machine.ts): transitionJumpToMessage is pure. Every async result is checked against attempt, so a late result from an aborted or superseded load does nothing. closed absorbs every event, so nothing can jump after disposed fires. A selected event 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 one AbortController and aborts it before each new load. It adds a 60 s timeout on reveal and 30 s on each message.list page, and cancel aborts in-flight work. Selecting a prompt that's already loaded cancels the background history load before jumping. The jump effect closes the dialog, which runs onCleanup and sends disposed while the state is already closed. That's a harmless no-op.
  • Retry: "Try again" only shows in the failed state, and rows from the loaded timeline stay selectable while it's up. After a failed open you're back on the list with openFailed, 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 })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 until message.get(id, messageID) finds the message. It throws once more(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. The while then checks again, so it doesn't loop hot or fetch twice.
  • The state machine keeps the two loads apart. selected only starts open.load from ready, so the shared controller never cancels the picker's history load. Stale attempts 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.

@usrnk1

usrnk1 commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

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.

@usrnk1
usrnk1 enabled auto-merge (squash) November 12, 2026 18:40
@usrnk1
usrnk1 merged commit f738b32 into dev Nov 12, 2026
10 checks passed
@usrnk1
usrnk1 deleted the jump-to-message branch November 12, 2026 18:41
@amrkmn amrkmn mentioned this pull request Nov 13, 2026
1 task
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: Keyboard jump-to-message list in the desktop/web session view

1 participant