fix(web): show the whole last turn in the Claude response viewer's brief view - #400
Conversation
…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
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>
|
Merged, thank you! This is a genuinely good catch: One thing applied on master at merge time ( 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 Shipping in the next release. |
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.?context=turn. The Claude reader answers with the assistant messages of the last answered turn (selectLastAnsweredTurn()inresponse-viewer-transcript.ts: the highestturnthat 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 numericturnas before.data.textis unchanged in every context — still the last assistant row, nevermessages.at(-1)— because agent pollers hash it (skills/codeman/preamble.sh).textonly forcontext=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=turnreturns the last answered turn's assistant rows (not the queued prompt after it) withtextstill 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 (requestscontext=turn, one badge + segments, falls back totext, ignores user/blank rows).test/response-viewer-turn-segments.test.ts,test/response-viewer-transcript.test.tsunchanged and green.tsc --noEmit, eslint, prettier.🤖 Generated with Claude Code
https://claude.ai/code/session_01McLWqCWBuQYGuPMScb4Aou