Skip to content

Keep adjacent assistant answers visible in completed turns - #2133

Open
SawyerHood wants to merge 1 commit into
mainfrom
bb/fix-1355-stop-hook-worked-for
Open

Keep adjacent assistant answers visible in completed turns#2133
SawyerHood wants to merge 1 commit into
mainfrom
bb/fix-1355-stop-hook-worked-for

Conversation

@SawyerHood

@SawyerHood SawyerHood commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

What was wrong

When a Claude Code Stop hook blocks a stop, Claude Code injects the hook's reason as a synthetic user message and the model answers again. That synthetic message never becomes a bb thread event (provider-claude-code only forwards tool_result user messages), so one bb turn ends with two agentMessage items back to back: the real answer, then a one-line reply to the hook. Completed-turn grouping in @bb/thread-view keeps exactly one terminal message per segment (findLastTerminalTimelineMessage) and slices everything before it into the collapsed Worked for summary. The real answer, including any question for the user, disappears behind a row that reads like a duration stat, in the app and in bb thread log.

Issue: #1355. Report: https://get-bb.github.io/reports/issues/1355.html (reproduced live on a fresh instance from 16ceb3a; the code at the root cause is unchanged on current main).

What changed

packages/thread-view/src/completed-turn-grouping.ts: an assistant text that is directly followed by more assistant text, with no work in between, is a complete response, not narration about upcoming tool activity. groupCompletedTurnSummaryMessages now promotes such messages to ungrouped-message items (the terminal message counts as the follower of the last summary message). The existing collapse is unchanged for text followed by tool activity, so ordinary text -> tool -> text turns still render as user -> Worked for -> last answer.

Behavior for the issue's shape (answer, hook reply): both assistant rows are visible at rest and no summary row is emitted. For narration, command, answer, hook reply: Worked for holds the narration and the command, then the answer and the hook reply are both visible.

Deviation from the report's primary proposal: the report suggests surfacing every top-level assistant text. That changes every agentic turn (interim narration such as "Let me check X" would leave the summary), which #320 deliberately collapses, and #1508's review measured a 1,500-event page producing ~1,500 rows (past the 200-row completed-timeline cache limit). This change is scoped to the shape that loses the answer. Remaining gap: a hook that makes the model call a tool before answering (answer, [hook], tool, reply) still folds the answer, because nothing in the event stream marks the boundary; fixing that needs provider-claude-code to surface the synthetic hook message through the bridge protocol, which is a contract change and a separate PR.

No wire change. The app and bb thread log share this row builder, so both pick it up.

How you verified

New tests, which fail on origin/main and pass with this change:

  • packages/thread-view/test/completed-turn-grouping.test.ts: keeps an assistant response visible when more assistant text follows it directly, folds narration that precedes work and keeps the response that precedes the terminal text, keeps every response in a run of adjacent assistant texts. Two existing tests that encoded the old behavior with two adjacent assistant texts now use a command message as the folded work.
  • packages/thread-view/test/completed-turn-summary-rendering.test.ts: keeps an assistant answer visible when the provider re-queries and the model answers again, folds narration before work but keeps the answer that precedes a hook reply.

With completed-turn-grouping.ts checked out from origin/main the rendering test fails with:

- Expected
+ Received
  [
    "conversation:user",
-   "conversation:assistant",
+   "turn:4-4",
    "conversation:assistant",
  ]

Commands:

  • pnpm exec turbo run typecheck --filter=@bb/thread-view --filter=@bb/server --filter=@bb/app --filter=@bb/cli --filter=@bb/client-core: 8/8 pass.
  • pnpm exec turbo run test --filter=@bb/thread-view --filter=@bb/server --filter=@bb/cli --filter=@bb/client-core: thread-view 21/21 files, cli 48/48, client-core 20/20, server 194/195 (the one failure is internal-skill-trees.test.ts mode 0644 vs 0664, a pre-existing local umask difference unrelated to this change).
  • apps/app vitest run src/components/thread/timeline: 28/28 files.

Manual: on a dev instance built from this branch, a claude-code thread in a scratch repo with a blocking Stop hook (the report's 4a repro) produced events agentMessage, agentMessage, turn/completed. bb thread log now prints the three-bullet answer with its closing question as an Assistant row, followed by the hook reply as a second Assistant row, with no Worked for row; the app shows the same two rows at rest.

Fixes #1355

AGENT GENERATED: by Claude Opus 5

Independent verification

Verified by a second agent on a fresh checkout of this branch (40224ee60, origin/main is an ancestor; no conflicts) in its own worktree and dev instance.

Commands:

  • pnpm install --frozen-lockfile --prefer-offline && pnpm exec turbo run build

  • Fail-before: git checkout origin/main -- packages/thread-view/src/completed-turn-grouping.ts, then pnpm exec vitest run test/completed-turn-grouping.test.ts test/completed-turn-summary-rendering.test.ts in packages/thread-view: 5 failed / 19 passed. The five new tests are exactly the ones that fail. Rendering test assertion:

    - Expected
    + Received
      [
        "conversation:user",
    -   "conversation:assistant",
    +   "turn:4-4",
        "conversation:assistant",
      ]
    

    Grouping test: expected [ { kind: 'summary', …(5) } ] to deeply equal [ { kind: 'ungrouped-message', …(1) } ].

  • Pass-after: restored the PR file, same command: 24/24 pass.

  • pnpm exec turbo run typecheck --filter=@bb/thread-view --filter=@bb/server --filter=@bb/cli --filter=@bb/client-core --filter=@bb/app --filter=@bb/mobile: 9/9 successful.

  • pnpm exec turbo run test --filter=@bb/thread-view --filter=@bb/server --filter=@bb/cli --filter=@bb/client-core: thread-view 21/21 files (385 tests), cli 48/48 (453), client-core 20/20 (238). See the server result note below.

  • Ad-hoc probes through renderTimelineFixture (not committed): reasoning, text, reasoning, text (extended thinking between the answer and the hook reply) still renders both assistant rows at rest; text, tool, text still collapses to user -> Worked for -> assistant (the deliberate Split completed turn summaries at user boundaries #320 behavior); a steer between two answers keeps the existing per-segment preservation; text, text, text, tool, text shows the first two, folds the third with the tool.

Repro on the fixed branch (report section 4a, real claude-code turn with a blocking Stop hook on a scratch repo, own dev instance): persisted events were item/completed agentMessage (seq 22), item/completed agentMessage (seq 29), turn/completed (seq 32) with no hook event between, the same shape as the report. bb thread log (default minimal format) printed User, Provisioned thread, then the full three-bullet answer with its closing question as an Assistant row, then the hook reply as a second Assistant row, with no Worked for row. GET /api/v1/threads/:id/timeline (what the app renders) returned conversation:user, system:operation, conversation:assistant, conversation:assistant and no turn row. The bug no longer reproduces. The provider still drops the synthetic hook message (plugins/provider-claude-code/src/delta-translation.ts translateUserMessage returns [] when there are no tool results), so the fix is entirely in the shared row builder, as described.

CI: all required checks green on the PR head (Checks, Tests server/packages/integration/app-1..3, Package Smoke ubuntu+macos, version check).

Residual risks (agreed with the PR body): answer, [hook], tool, reply still folds the answer because no event marks the hook boundary; that needs provider-claude-code to surface the synthetic user message (wire change, separate PR). Any provider that emits two adjacent assistant items for one response now shows both rows. answer, answer2, error shows answer at rest but still folds answer2 before the error row (pre-existing behavior for error terminals, not made worse). Textual merge overlap with #2071 in the same loop of completed-turn-grouping.ts.

AGENT GENERATED: by Claude Opus 5

When a Claude Code Stop hook blocks a stop, Claude Code injects the
hook's reason as a synthetic user message and the model answers again.
bb never sees that boundary, so one turn ends with two assistant texts
back to back. Completed-turn grouping kept only the last text visible
and folded the real answer into the collapsed "Worked for" summary.

Treat an assistant text that is directly followed by more assistant
text (no work in between) as a complete response and keep it as an
ungrouped row. Text followed by tool activity still collapses as
interim narration.

Co-Authored-By: Claude <noreply@anthropic.com>
@SawyerHood
SawyerHood marked this pull request as ready for review August 21, 2026 03:46
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.

With a stop hook, most of the Assistant text is hidden inside "Worked for [time]"

1 participant