Skip to content

feat(*): pin a details tab in the trajectory view - #989

Merged
0xKT merged 5 commits into
mainfrom
feat/traj_details_tab_pin
Oct 19, 2026
Merged

0xKT merged 5 commits into
mainfrom
feat/traj_details_tab_pin

Conversation

@forrestjgq

Copy link
Copy Markdown
Contributor

Summary

The trajectory view's details pane opens an entry on the overview, or on the tab last chosen for that entry, so a reader checking the same aspect of many steps (relations, usage, parameters) has to pick that tab again for every step. This adds a pin for the tab:

  • A pin button in the details pane's head, just left of the close button (or "Back to list" when the pane covers the list on a narrow area). It pins the tab on screen and reads as pressed while that tab is the pinned one; on any other tab it offers to pin that tab instead, replacing the previous pin. The glyph is hollow and faint until pressed, then solid in the colour the selected tab is underlined in.
  • Whenever the pane arrives at an entry (a new selection, or the pane reopened), it opens on the pinned tab if that entry's strip has it. Otherwise the entry shows what it would without a pin: the tab the reader last chose for it, or the overview. A reread of the entry on screen, a tab click or a click on the row already open is not an arrival, so the reader is never pulled off a tab mid-read.
  • The pinned tab carries a small solid pin in the strip, named "Pinned" so the tab's accessible name says so, which shows what is pinned while the reader is on another tab.
  • On an arrival the selected tab is shown whole. The strip's overflow arrows appear only after the strip has first been scrolled to the selected tab, and they take 44 px from it; the keep-in-view step now runs again when they come or go, so a pinned tab at the far right no longer arrives half hidden under the right arrow.
  • Pins are per conversation. They live in memory, so switching to another conversation and back always restores them, and they are mirrored to the browser tab's sessionStorage through the existing lib/persist.ts slot, so they survive a reload. A tab opened fresh starts with no pin; a duplicated tab, or a page opened from this one with an opener, starts with a copy, the same as the page's other per-tab view state.

Key decisions:

  • visibleTabs() in detailStore.ts is now the one definition of the strip's tabs, shared by the strip and the pin rule, so "the entry has the pinned tab" and "the strip shows it" cannot drift apart.
  • The storage slot has no cap. The default cap of 20 would evict a conversation's pin while switching back to it still promises the pin. Each entry is one tab id written only by the reader's click.
  • Front end only: no gateway, RPC or Python change. ui-web/CONTEXT.md defines the pinned tab. The three new catalogue entries regenerate the TUI copy.
  • The docs site (docs-site/docs/webui.md) is not changed: its trajectory section is a three-line overview that does not go into the details pane's tabs, and the button carries its own tooltip. Walk screenshots were checked locally and are not committed, per the repository asset rule.

Four commits: the store semantics; the control, the tab mark, the catalogue entries and the term; the fix that keeps the selected tab whole once the strip's arrows appear; and a test follow-up that makes that fix's regression test nudge the strip for real.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

  • cd ui-web && npx vitest run --maxWorkers=8: 249 files, 3968 tests passed. 18 tests are new (12 in detailStore.test.ts, 3 in Details.test.tsx, 3 in Tabs.test.tsx); the slowest new one takes 27.5 ms.

  • In ui-web: npm run gen:check (240 methods match), npm run lint (0 errors; the 4 warnings are already on main), npm run type-check: pass.

  • In ui-web: npm run build && uv run --no-sync python build.py && node scripts/check-page.mjs && node scripts/check-css.mjs && node scripts/check-class-namespace.mjs: pass.

  • In ui-tui: npm run lint, npm run lint:rpc, npm run lint:i18n, npm run type-check, npm test -- --maxWorkers=8 (2106 passed, 13 skipped), npm run build: pass.

  • UV_NO_SYNC=1 make check-source-language check-large-files check-commits: pass.

  • Browser walk of the offline page (index.html?stub=1, Playwright with Chromium): 33 of 33 checks pass. It covers pin and unpin, another entry opening on the pinned tab, an entry without that tab keeping the tab the reader chose, a reopened pane, a pinned muted tab, two conversations each restoring its own pin, a reload, a page opened fresh (no pin), a page opened with window.open (a copy, independent afterwards), and the narrow overlay, in light and dark themes. A second walk pins raw, relations and overview in turn and opens all 8 entries for each, measuring the selected tab against the strip: 11 of 24 arrivals were clipped by up to 44 px before the fix, none after.

  • Mutation checks: restoring the default cap of 20 fails the 25-conversation test; dropping the in-memory pins on a conversation switch fails the storage-unavailable test; running the keep-in-view step only on a tab change fails the new arrows test.

  • Relevant tests pass locally

  • Relevant lint / type checks pass locally

  • User-facing docs or screenshots are updated when needed

Risk

  • Security impact considered

  • Backward compatibility considered

  • Rollback path is clear for risky changes

  • User-visible changes: a pin button in the details head, a pin mark on the pinned tab, and the tab an entry opens on once something is pinned. With nothing pinned, the pane behaves exactly as before.

  • sessionStorage gains one key, raven.gui.view.trajectory.pin, holding one tab id per conversation that has a pin; unpinning deletes that conversation's entry. Nothing leaves the page.

  • When sessionStorage is unavailable (private mode, quota), pins still work for the page's lifetime, including conversation switches, but do not survive a reload.

  • Rollback: revert this PR. No data migration; the leftover sessionStorage key is never read and goes away when the browser tab closes.

Related Issues

N/A

forrestjgq and others added 4 commits October 17, 2026 00:04
…ctory store

The details pane remembers one pinned tab per conversation. Each time the
pane arrives at an entry (a new selection with the pane open, or the pane
reopened), the pin picks that entry's tab when the descriptor offers it;
otherwise the entry keeps the tab it would show without a pin. A reread of
the entry on screen, a tab click or a click on the open row is not an
arrival, so the reader is never pulled off a tab mid-read.

Pins live in memory per conversation and survive a conversation switch, so
returning to a conversation never depends on storage. They are mirrored to
the tab's sessionStorage through the persist slot with no cap, so a reload
brings them back and no conversation's pin is evicted.

visibleTabs is now the one definition of the strip's tabs, shared by the
strip and the pin. No visible control yet.

Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
The details pane's head gets a pin button just left of the close button
(or "Back to list" on a narrow area). It pins the tab on screen for the
conversation and reads as pressed while that tab is the pinned one; on any
other tab it offers to pin that tab instead. The pin glyph is hollow and
faint until pressed, then solid in the colour the selected tab is
underlined in.

The pinned tab carries a small solid pin in the strip, named "Pinned" so
the tab's accessible name says so, which tells the reader which tab is
pinned while they are on another one.

Adds the three catalogue entries (regenerating the TUI copy) and defines
the pinned tab in ui-web/CONTEXT.md.

Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
…trip's arrows appear

A strip remounts on every arrival at an entry, without arrows. The
selected tab was scrolled to the strip's edge first; the overflow measured
next brought in the two 22 px arrows, which take their room from the
strip, and the tab at the right edge ended up 44 px under one of them. A
pinned tab far right made this the common case: "Raw record" read
"Raw reco".

The keep-in-view effect now runs again when the arrows come or go. A
nudge with the arrows does not change whether they show, so it still
scrolls freely and is not pulled back to the selected tab.

Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
…egression test

The "not on a nudge" half clicked the left arrow, which is disabled while
the strip sits at its start, so no nudge happened and the test could not
tell a keep-in-view step that follows each arrow's state from one that
follows only whether the arrows show. It now clicks the enabled right
arrow, dispatches the scroll the browser would, and checks that the strip
moved by one step and both arrows turned enabled while the selected tab
was not scrolled back into view.

Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No blockers; this can merge as far as I am concerned.

Reviewed the full github/main...HEAD diff at the current GitHub base and covered the repository rules in AGENTS.md and CONTEXT-MAP.md/ui-web/CONTEXT.md, the pin state and persistence flow, React callers and accessibility behavior, selection/session/revision compatibility, commit history, and the tests for signs of weakened assertions. I found no plain error or merge blocker.

Verification:

  • npm test: 249 files, 3968 tests passed
  • focused trajectory tests: 3 files, 72 tests passed
  • npm run type-check: passed
  • npm run lint -- --quiet: passed
  • uv run pytest tests/test_resume_remote.py tests/test_subagent_acp_remote.py tests/test_subagent_cli_remote.py -x: 134 passed on rerun (an initial run had one unrelated machine-process cleanup timeout; the exact test passed alone before the clean rerun)
  • uv run python scripts/check_source_language.py github/main...HEAD: passed
  • git diff --check github/main...HEAD: passed

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No blockers; this can merge as far as I am concerned.

This revision merges current main into the previously reviewed feature head. git range-diff shows the four feature commits are patch-identical, and I found no integration issue with the updated shared UI or localization baseline. I rechecked the current github/main...HEAD diff, affected callers and context, compatibility, repository rules, commit history, and the tests for weakened coverage.

Verification:

  • focused trajectory tests: 3 files, 72 tests passed
  • npm run type-check: passed
  • npm run lint -- --quiet: passed
  • uv run python scripts/check_source_language.py github/main...HEAD: passed
  • git diff --check github/main...HEAD: passed

@0xKT

0xKT commented Oct 18, 2026

Copy link
Copy Markdown
Member

Not a blocker. One case where the pin breaks this PR's own ui-web/CONTEXT.md promise; it does not change my grade.

Severity of this one finding, not a verdict on the pull request. Any blocking findings from this pass are review threads on the changed files; GitHub renders those collapsed, as a file name with no text.

The entry says the pinned tab is opened "never while the reader is already on the entry". Reopening a row whose descriptor went stale while the pane was closed sets arrival (ui-web/src/features/trajectory/detailStore.ts:381) and defers the pin until the re-read lands (:385), while the pane keeps showing the stale descriptor with its tab strip. A tab the reader clicks in that window goes through setTab (:407-412), which leaves arrival set, so when the read lands accept() (:678) calls land() and moves the reader to the pinned tab. Reproduced through the real components (TrajectoryApp driven with fireEvent): pin relations, close the pane, a new revision lands, reopen the row, click origin while the read is out, and the pane ends on relations. Without a pin it stays on origin; with a pin and no click it lands on relations, as documented.

Suggested fix: clear arrival in setTab, since the reader's own choice on the entry supersedes the pin. In a scratch copy, adding arrival: null to that patch makes the clicked case end on origin, keeps the no-click case on relations, and the trajectory test files still pass.

@gloryfromca

Copy link
Copy Markdown
Member

No blockers; suggestions only, and they are marked inline.

Confirmed. While a stale descriptor is being reread after reopen, the old tab strip remains interactive; setTab() changes the tab but leaves arrival pending, so accept() later calls land() and overwrites that newer user choice with the pin. Clearing arrival on an actual tab change is consistent with the documented rule that the pin must not move a reader already interacting with the entry.

I am carrying this as the existing nonblocking follow-up, so the change can still merge as it stands. The focused trajectory suite remains green (3 files, 72 tests), and npm run type-check passes.

@0xKT
0xKT merged commit e9b5a51 into main Oct 19, 2026
24 checks passed
@0xKT
0xKT deleted the feat/traj_details_tab_pin branch October 19, 2026 02:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants