Skip to content

feat(chat): open a transcript hit at its message, and give the transcript a way up and a way down - #165

Open
titan-ron wants to merge 3 commits into
mainfrom
titan/transcript-navigation-45e1b3
Open

titan-ron wants to merge 3 commits into
mainfrom
titan/transcript-navigation-45e1b3

Conversation

@titan-ron

Copy link
Copy Markdown
Collaborator

Summary

A craft pass on the transcript, the surface a person spends most of the day in, following the review that merged as #164. No reskin: everything here lives inside the settled design system.

A transcript-search hit opens at its message. Picking a hit in the ⌘K palette used to open the session at the bottom and leave you scrolling for the words it had just shown you. The hit now rides along as the chat's anchor; once the log is in, the chat finds the row by its words, speaker and time (never by index — the searcher and the parser count messages differently), brings it into the DOM window with context above, scrolls it to the middle, rings it for three seconds and says so in the status region. Applied once per anchor, so a live session growing underneath never re-scrolls; words the log no longer says open at the bottom as before.

A way up. "showing the last 400 of 1,200 messages" was a dead end. The line is now the control that shows the next 400, and the rows land above the viewport without moving what is on screen: the scroll offset is kept in a layout effect, and .messages turns the browser's own scroll anchoring off so the adjustment is made once.

A way down. The auto-scroll deliberately never hijacks a scroll-up — which meant rows arriving below were invisible news. A "New messages" key now appears at the bottom edge the moment the log grows while you are scrolled up, and takes you there; reaching the bottom by hand clears it. It is a sticky, zero-height last child, so showing and hiding it never moves a row; hidden it is out of the tab order and the accessibility tree.

Both controls are shared with the roundtable (transcript-window.tsx), where a seat streaming into its live block counts as news the same as an entry landing.

A bug the new tour shot caught. The transcript re-pinned itself to the bottom whenever App re-made the binding object mid-turn (the native id from the CLI's first event, a parent chip arriving), so a reader who had scrolled up while the agent wrote was yanked back down on the next row — the one hijack the rule forbids. The pin and the DOM window now reset per conversation (provider, directory, native id), not per object.

Also: the tour's roundtable fixture read "Codex turn failed: turn failed: …"; it now carries the text main really writes.

Known gap, not built here

Re-opening a session whose turn Cockpit itself is running detaches the view from that turn: the chat looks idle with Send enabled while main is mid-turn, and the turn's events are dropped. It needs the busy entry to carry the turn id so the view can re-attach on open, and has a dedupe question (disk log vs stream, both at message granularity) worth its own PR.

Design-system pages (chat.md, palette.md, roundtable.md) and the user guide (chat.md, sessions.md) updated.

Verification

  • npm run typecheck clean
  • npm test: 112 files, 1697 tests passed — new transcript-anchor.test.ts (words, speaker, time, fallbacks), and chat-view, command-palette and roundtable-view cases for the window, the anchor and the key
  • npm run build && npm run test:e2e: 46 passed, 6 skipped (the opt-in packaged and full-screen specs)
  • npm run ui:tour: two new shots — chat-from-search (the ringed message) and chat-new-below (the key while a turn streams, read from the top); the latter is what found the re-pin bug, and a Playwright probe against the tour world confirmed the key holds at scrollTop 0 through a whole streamed turn

The consensus table's fixture gave its failed seat the text "turn failed: process
exited with code 1", and the view prefixes every error entry with "<Seat> turn failed:"
— so the shot read "Codex turn failed: turn failed: …", a doubling main never produces
(its entry text is the CLI's own line, "codex exited with code 1"). The fixture now says
what main would.
…transcript a way up and a way down

Three things the transcript owed its reader.

A transcript-search hit opened its session at the bottom, leaving the person to scroll
for the message the palette had just shown them. The hit now rides along as the chat's
`anchor` (`TranscriptAnchor`), and once the log is in, `findAnchor` names the row by its
words, speaker and time — never by index, since the searcher and the chat's parser count
messages differently — brings it into the DOM window with a few rows of context, scrolls
it to the middle, rings it for a moment (`.anchored`) and says so in the status region.
Applied once per anchor, so a live session growing underneath never re-scrolls; words
the log no longer says open at the bottom as before.

The DOM window ("showing the last 400 of 1,200 messages") was a dead end: the sys-row
that said so is now the control that shows the next batch (`EarlierRow`), and the rows
land above the viewport without moving what is on screen — `useTranscriptWindow` re-adds
the height in a layout effect and `.messages` turns the browser's own scroll anchoring
off so the adjustment is made once.

And while the reader is scrolled up, the auto-scroll stays off on purpose — so rows
arriving below were invisible news. `useUnseenBelow` marks them the moment the log grows
off the bottom, and `JumpToLatest` — a sticky, zero-height last child whose "New
messages" key hangs above the bottom edge without moving a row — takes them there;
reaching the bottom by hand clears it. Hidden it is visibility: hidden, out of the tab
order and the accessibility tree.

Both controls are shared with the roundtable (`transcript-window.tsx`), where a seat
streaming into its live block counts as news the same as an entry landing. The tour
shoots the anchored chat and the key; the design pages and the guide say all three.
…s mid-turn

The transcript re-pinned itself to the bottom whenever the binding object changed, and
App re-makes that object mid-turn — the native id from the CLI's first event, a parent
chip arriving — so a reader who had scrolled up while the agent wrote was yanked back
down on the next row: the one hijack the auto-scroll rule forbids. The tour's new
`chat-new-below` shot is what caught it (the "New messages" key never showed because the
pin had quietly come back).

The pin and the DOM window now reset per conversation — provider, directory and native
id — not per object, so the same session keeps the reader's place through those updates
and a different one still opens at the bottom. The shot waits for a pressable Send
before its second turn (the log's own liveness holds it a beat after a turn ends) and
for the key itself, since its zero-height line reads as hidden to Playwright.
@github-code-quality

github-code-quality Bot commented Sep 22, 2026

Copy link
Copy Markdown

Code Coverage Overview

Languages: TypeScript

TypeScript / code-coverage/unit

The overall line coverage in commit 3dfcac1 in the titan/transcript-nav... branch remains at 77%, unchanged from commit 5ad0df7 in the main branch.

TypeScript / code-coverage/component

The overall line coverage in commit 3dfcac1 in the titan/transcript-nav... branch remains at 87%, unchanged from commit 5ad0df7 in the main branch.

Show a line coverage summary of the most impacted files.
File main 5ad0df7 titan/transcript-nav... 3dfcac1 +/-
src/renderer/sr...ndtableView.tsx 88% 88% 0%
src/renderer/src/logos.tsx 95% 95% 0%
src/renderer/src/App.tsx 70% 71% +1%
src/renderer/src/ChatView.tsx 93% 95% +2%
src/renderer/sr...cript-anchor.ts 0% 65% +65%
src/renderer/sr...ript-window.tsx 0% 100% +100%

Updated September 22, 2026 15:17 UTC

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant