Skip to content

fix(web): show the whole last turn in the Claude response viewer's brief view - #400

Merged
Ark0N merged 1 commit into
Ark0N:masterfrom
shenlvkang-collab:pr/claude-viewer-last-turn
Sep 12, 2026
Merged

fix(web): show the whole last turn in the Claude response viewer's brief view#400
Ark0N merged 1 commit into
Ark0N:masterfrom
shenlvkang-collab:pr/claude-viewer-last-turn

Conversation

@shenlvkang-collab

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #369. The eye button's brief view rendered data.text, which is one row: the last assistant row of the transcript. A Claude answer is a median of 3 model messages (p90 11) split around tool calls, so the brief view usually showed the tail of an answer ("Done.", "Let me look.") while the substance appeared only after More. The full view was fine, which is why the brief one read as broken by comparison.

  • The brief view now asks ?context=turn. The Claude reader answers with the assistant messages of the last answered turn (selectLastAnsweredTurn() in response-viewer-transcript.ts: the highest turn that has an assistant row, so a prompt queued after the answer does not blank the view), and the frontend renders them exactly as the full view renders that turn — one badge, then continuation segments, gated on the numeric turn as before.
  • data.text is unchanged in every context — still the last assistant row, never messages.at(-1) — because agent pollers hash it (skills/codeman/preamble.sh).
  • Readers that emit no turns (Codex, the pane parser, DeepSeek, an older server) return text only for context=turn, and the brief view keeps its single card for them. The terminal-buffer fallback is untouched.

Docs: skills/codeman/reference/endpoints.md (new row), docs/architecture-invariants.md (the viewer paragraph). Changeset: patch.

Test plan

  • test/routes/session-routes-claude-last-response.test.ts: context=turn returns the last answered turn's assistant rows (not the queued prompt after it) with text still the last row; empty list before the first answer.
  • test/response-viewer-last-turn.test.ts (new): the pure selector (highest answered turn, no numeric turns → empty, turn 0 kept) and the brief view in jsdom (requests context=turn, one badge + segments, falls back to text, ignores user/blank rows).
  • test/response-viewer-turn-segments.test.ts, test/response-viewer-transcript.test.ts unchanged and green.
  • tsc --noEmit, eslint, prettier.

🤖 Generated with Claude Code

https://claude.ai/code/session_01McLWqCWBuQYGuPMScb4Aou

…ief view

The eye button rendered `data.text`, which is one row: the last assistant
row of the transcript. A Claude answer is a median of 3 model messages
(p90 11) split around tool calls, so the brief view usually showed the tail
of an answer ("Done.", "Let me look.") and the substance appeared only after
More. The full view was fine, which is why the brief one read as broken by
comparison.

The brief view now asks `?context=turn`. The reader answers with the
assistant messages of the last ANSWERED turn (`selectLastAnsweredTurn`: the
highest `turn` that has an assistant row, so a prompt queued after the
answer does not blank the view) and the frontend renders them exactly as
the full view renders that turn: one badge, then continuation segments,
gated on the numeric `turn` as before.

`data.text` is unchanged in every context — still the last assistant row,
never `messages.at(-1)` — because agent pollers hash it. Readers that emit
no turns (Codex, the pane parser, DeepSeek, an older server) return `text`
only for `context=turn`, and the brief view keeps its single card for them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McLWqCWBuQYGuPMScb4Aou
@Ark0N
Ark0N merged commit 77d9ad5 into Ark0N:master Sep 12, 2026
2 checks passed
Ark0N pushed a commit that referenced this pull request Sep 12, 2026
Each item is from the pre-merge review of the PR it names, applied on master
rather than by pushing to a contributor branch.

#400 (response viewer, shenlvkang-collab)
- The brief view opened at `scrollTop = 0`, right when it was a single card
  holding the last row. Now that it renders the whole turn, the top is the
  turn's first narration line and the answer can be screens below it, while
  loadFullContext already scrolls to the bottom of the same turn. A multi-row
  turn now opens at its newest text; a single card still opens at the top.

#401 (loopback links as web tabs, shenlvkang-collab)
- Drop `*.localhost` from the auto-route set. Every other member is an address
  literal that can only mean this box; a `*.localhost` DNS name is not one, and
  a resolver with a search domain retries `evil.localhost` as
  `evil.localhost.<search domain>`. The link source is agent-written terminal
  output, so that set is the whole confinement on a tap that makes Codeman
  fetch a URL server-side and persist it. The page-side test stays broader
  (`isOnBoxHostname`), where a false positive only declines to proxy.
- A link to the origin root navigated nothing: the path was flattened to '',
  which openWebview reads as "no deep link", leaving an open frame where it was.
- `this.webviews` being set does not mean it is loaded. initWebviews() assigns a
  truthy empty map and only then awaits the list, so a tap during page load
  found nothing to reuse and POSTed a duplicate record. Join the in-flight
  refresh instead.
- One dashboard per dev server rather than per host spelling, which is what the
  method's own comment already promised.
- Toast on the auto-create: it writes webviews.json, broadcasts over SSE and
  adds a Run-dropdown row on every signed-in device, with a new tab as its only
  previous signal.

#362 (remote omp continuation, timkjr)
- Accept the allowlisted `mode === 'omp'` arm as-is; a blanket registry render
  would hand deepseek a locally-resolved --profile and bypass claude's own
  overlay. A registry-declared switch is the follow-up if a third mode needs it.
- Revert the whole-file Prettier reformat of docs/remote-sessions.md (docs/ is
  hand-formatted and outside `npm run format`), keeping only the two new
  sections.
- Correct three stale passages: architecture-invariants' `exec claude
  --dangerously-skip-permissions`, the `exec <cli>` paragraph (claude and omp
  now have their own arms, and the claude pane's PID is the login shell), and
  omp-integration's `-c 'omp'`. RemoteCommandMode gains deepseek and omp.
- Add the missing `_maybeCaptureOmpSessionId` remote-guard test; the sibling
  guard in `_pinOmpRespawnId` had one and this path runs earlier, on the first
  idle turn.

#388 (keyCode 229 recovery, aakhter)
- Gate notifyCanonicalData on shouldSuppressTerminalQueryResponse and
  isTerminalFocusOrMouseReport. onData also carries the DA/DSR/CPR/OSC replies
  xterm answers during Ink redraws and its SGR mouse and focus reports; any of
  those landing between the keydown and the candidate's resolution was read as
  "xterm spoke for this keystroke", standing the recovery down and leaving the
  character dropped, worst on a busy agent pane. Reached through
  window.CodemanTerminalInput: the predicates live in a module IIFE that closes
  long before this call site, so bare references would throw into the
  surrounding try/catch and stop the notify from ever running.

Every fix has a test that fails without it (verified by reverting each).
Full gate green on the combined tree: 358 files, 6849 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Ark0N

Ark0N commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Merged, thank you! This is a genuinely good catch: data.text is one row and a Claude turn is a median of 3, so the eye button was showing the "Done." tail of an answer whose substance was in the rows above it. The measurement behind it (285 real transcripts, p50 2 rows / p90 13) is exactly the kind of evidence that makes a change like this easy to accept, and I appreciated that you pinned data.text for agent pollers with a test rather than just asserting the contract held.

One thing applied on master at merge time (02b0e278): the view opened at body.scrollTop = 0, which was right when it was a single card holding the last row, but with the whole turn rendered the top of the scroller is the turn's first narration line. loadFullContext() already scrolls to the bottom of that same turn, so the two views disagreed about which end mattered. It is now body.scrollTop = turnMessages.length > 1 ? body.scrollHeight : 0, so a multi-row turn opens at its newest text and a single card still opens at the top, with a test covering both.

The remaining review notes (the duplicated numeric-turn continuation gate, a mid-turn queued prompt fusing two answers under one badge, and no route test pinning the non-Claude context=turn behaviour) are cleanups rather than blockers. Happy to take any of them as a follow-up if you want them, otherwise they go in a later pass.

Shipping in the next release.

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.

2 participants