Skip to content

fix(web): interleave tool calls with text during SSE streaming#1064

Merged
coleam00 merged 3 commits into
devfrom
archon/task-fix-issue-1054
Apr 16, 2026
Merged

fix(web): interleave tool calls with text during SSE streaming#1064
coleam00 merged 3 commits into
devfrom
archon/task-fix-issue-1054

Conversation

@coleam00

@coleam00 coleam00 commented Apr 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • During live SSE streaming, tool calls were always rendered below all text in a conversation bubble instead of being interleaved between text segments
  • Root cause: onText in ChatInterface.tsx was missing a boundary check — when text arrived after a tool call, it appended to the same streaming message rather than starting a new segment
  • Fix: adds a single guard in onText that mirrors the existing server-side rule in persistence.ts (line 72): if the last streaming message already has tool calls, seal it and open a new streaming message for the incoming text

Changes

  • packages/web/src/components/chat/ChatInterface.tsx: added tool-call boundary check inside the onText SSE handler (~16 lines added, 0 removed)

Validation

Check Result
Type check (bun run type-check) ✅ Pass — all 9 packages
Lint (bun run lint --max-warnings 0) ✅ Pass — 0 errors, 0 warnings
Format (bun run format:check) ✅ Pass
Tests (bun run test) ✅ Pass — all suites

Manual verification steps:

  1. Send a message that triggers tool use — tool cards appear interleaved between text segments during streaming
  2. Navigate away and back — layout matches the live streaming view exactly
  3. Send multiple sequential tool calls — tools group in one segment between text chunks
  4. Send a plain text message — no regression

Fixes #1054

Summary by CodeRabbit

  • Refactor

    • Restructured chat message handling logic for improved maintainability and reliability.
    • Simplified header title display code.
  • Tests

    • Added comprehensive test coverage for message handling workflows, including edge cases for workflow results, status boundaries, and tool interactions.

During SSE streaming, tool calls always appeared below all text because
onText appended to the existing message even when it already had tool
calls. The server-side persistence already segments at this boundary.
Mirror that rule in the client's onText handler: when the last streaming
message has tool calls, seal it and start a new message for incoming text.

Fixes #1054

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

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f34a72fd-ae88-40ee-a0dc-307beae5edac

📥 Commits

Reviewing files that changed from the base of the PR and between 536584d and 4292c3a.

📒 Files selected for processing (4)
  • packages/web/src/components/chat/ChatInterface.tsx
  • packages/web/src/components/chat/MessageList.tsx
  • packages/web/src/lib/chat-message-reducer.test.ts
  • packages/web/src/lib/chat-message-reducer.ts

📝 Walkthrough

Walkthrough

Extracted SSE text event handling from ChatInterface into a pure, testable reducer function (applyOnText) that deterministically manages streaming message segmentation based on workflow results, status boundaries, and tool-call interleaving rules. Includes comprehensive test coverage for boundary conditions and refactored MessageList's status-to-title mapping.

Changes

Cohort / File(s) Summary
Reducer Module & Tests
packages/web/src/lib/chat-message-reducer.ts, packages/web/src/lib/chat-message-reducer.test.ts
New pure reducer that implements deterministic SSE text handling with segmentation rules for workflow results (with runId dedup), workflow-status boundaries (🚀/✅), tool-call boundaries, and streaming assistant message lifecycle. Comprehensive test suite covers all boundary conditions and control flows.
ChatInterface Refactoring
packages/web/src/components/chat/ChatInterface.tsx
Replaced inline setMessages reducer logic in onText handler with call to applyOnText, removing local control flow for workflow dedup, streaming state management, and message appending. Updated SSE reconnect failure handler with structured error logging.
MessageList Refactoring
packages/web/src/components/chat/MessageList.tsx
Converted nested ternary expression for headerTitle derivation to explicit if/else control flow; no functional change to status mapping logic.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A streaming stream, so tangled and tight,
Now flows through a pure reducer's light!
Tool calls dance 'mid text without fight,
Segmented cleanly, boundaries tight
Tests guard every twist, every height!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch archon/task-fix-issue-1054

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coleam00

Copy link
Copy Markdown
Owner Author

🔍 Comprehensive PR Review

PR: #1064 — fix(web): interleave tool calls with text during SSE streaming
Reviewed by: 3 specialized agents (code-review, error-handling, test-coverage)
Date: 2026-04-10


Summary

Minimal, correct fix that adds one boundary guard in the onText SSE handler — exactly mirroring the server-side persistence.ts:72 segmentation rule. No bugs introduced. The change is production-ready.

Verdict: ✅ APPROVE

Severity Count
🔴 CRITICAL 0
🟠 HIGH 0
🟡 MEDIUM 2 (both pre-existing / deferrable)
🟢 LOW 3 (theoretical / pre-existing)

🟡 Medium Issues (Needs Decision — recommend: defer as issues)

1. Silent .catch on SSE reconnect re-fetch (pre-existing)

📍 packages/web/src/components/chat/ChatInterface.tsx:544

The .catch() on the re-fetch after SSE reconnect has zero logging. If the fetch fails (network error, 500, auth expiry), no breadcrumb exists. The user sees the spinner cleared but no error message, and there's nothing in logs.

This is pre-existing and out-of-scope for this PR. Suggested fix: add console.error with context inside the catch.

Options: Fix now | Create issue | Skip — Recommend: Create Issue


2. No unit test for the new onText boundary guard

📍 packages/web/src/components/chat/ChatInterface.tsx:344-357

The new guard is pure state-reducer logic — no DOM, no hooks, no I/O — and is straightforwardly unit-testable. No test was added. A future change to onText could silently re-introduce issue #1054 with no test catching it.

The component layer currently has zero tests, so this PR isn't uniquely regressing coverage. The test-coverage agent provided a sample test outline:

View suggested test outline
// src/lib/chat-message-reducer.test.ts
describe('applyOnText — tool-call boundary', () => {
  test('starts a new segment when last message has tool calls', () => {
    // ...
    expect(next).toHaveLength(2);
    expect(next[0].isStreaming).toBe(false);
    expect(next[1].content).toBe('Post-tool text');
  });

  test('appends to same segment when no tool calls', () => {
    // ...
    expect(next).toHaveLength(1);
  });

  test('treats undefined toolCalls same as empty array', () => {
    // ...
    expect(next).toHaveLength(1);
  });
});

Options: Fix now (extract reducer + add tests) | Create issue | Skip — Recommend: Create Issue


🟢 Low Issues

View 3 low-priority observations
Issue Location Note
Guard doesn't mark in-flight tool calls complete ChatInterface.tsx:345-357 Theoretical only — SSE event ordering prevents it; onToolResult backward search covers it anyway; YAGNI/KISS apply — leave as-is
Workflow-status guard + empty last.content corner case ChatInterface.tsx:325 Pre-existing inconsistency vs server-side; out of scope
Workflow hydration .catch has no user feedback ChatInterface.tsx:254 Pre-existing; background non-blocking call; logging present

✅ What's Good

  • Exact server parity: Guard condition (last.toolCalls?.length ?? 0) > 0 directly mirrors persistence.ts:72. Comment even cites the line number — auditable without digging through history.
  • Correct placement: New guard sits AFTER the workflow-status check, preserving existing boundary priority ordering.
  • Correct isStreaming transitions: Closing previous message with isStreaming: false and opening new one with isStreaming: true is consistent with all other boundary cases.
  • Consistent message shape: New segment object (id, role, content, timestamp, isStreaming, toolCalls: []) matches every other message-creation site in the handler.
  • Minimal diff: +16 lines, 0 deletions — the smallest change that fixes fix(web): tool calls not interleaved with text during streaming — always appear at bottom #1054.
  • CLAUDE.md compliance: YAGNI ✅, KISS ✅, no silent fallbacks ✅, TypeScript strict ✅

📋 Suggested Follow-up Issues

Issue Title Priority
"Add logging to SSE reconnect re-fetch .catch" P3
"Extract onText reducer to chat-message-reducer.ts and add unit tests" P3

Reviewed by Archon comprehensive-pr-review workflow
Full artifacts: ~/.archon/workspaces/coleam00/Archon/artifacts/runs/86df3adf4a7a4b1da70719ded519aeb3/review/

- Add console.error logging to silent .catch on SSE reconnect re-fetch
  (ChatInterface.tsx:~544) so production failures are visible in logs
- Extract onText setMessages reducer to chat-message-reducer.ts as a
  pure function (applyOnText) with 14 unit tests covering all 6
  segmentation rules including the new tool-call boundary (issue #1054)
- Refactor ChatInterface.onText to delegate to applyOnText

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coleam00

Copy link
Copy Markdown
Owner Author

Fix Report: PR #1064

Date: 2026-04-10T00:00:00Z
Status: COMPLETE
Branch: archon/task-fix-issue-1054
Commit: dbe559e
Philosophy: Aggressive fix — lean towards fixing everything


Summary

Addressed all 2 MEDIUM findings from the consolidated review. No CRITICAL or HIGH findings existed. The 3 LOW findings were correctly left as-is per reviewer recommendations (YAGNI / pre-existing / out-of-scope).


Fixes Applied

Severity Finding Location What Was Done
MEDIUM Silent .catch on SSE reconnect re-fetch swallows errors with no logging ChatInterface.tsx:~544 Added console.error with conversationId and error message, matching the project's structured logging pattern used elsewhere in the file
MEDIUM No unit test for onText segmentation logic ChatInterface.tsx:344-357 Extracted the entire setMessages updater inside onText into a pure function applyOnText in packages/web/src/lib/chat-message-reducer.ts; refactored onText to delegate to it; added 14 unit tests in chat-message-reducer.test.ts covering all 6 segmentation rules

Tests Added

File: packages/web/src/lib/chat-message-reducer.test.ts

Test Suite Tests What Is Covered
Rule 4 — tool-call boundary 4 Splits on non-empty toolCalls; no split on empty []; no split on absent toolCalls; multiple tool calls still split
Rule 5 — append 1 Normal text appended to streaming message
Rule 6 — new message 3 Empty prev; last is user message; last assistant is not streaming
Rules 2 & 3 — workflow-status boundary 3 Incoming status + current has content; current is status + incoming is regular; incoming status + current is empty (no split)
Rule 1 — workflow-result 3 New non-streaming message created; closes streaming before result; deduplication by runId

All 14 tests pass. The @archon/web test suite count went from 87 to 101 tests (in src/lib/ batch).


Docs Updated

None required.


Skipped Findings

Severity Finding Reason
LOW Guard doesn't mark in-flight tool calls complete YAGNI — SSE event ordering prevents this from occurring; reviewer explicitly recommended Option A (leave as-is)
LOW Workflow-status guard + empty last.content corner case Pre-existing inconsistency, explicitly out-of-scope per review scope document
LOW Workflow hydration .catch has no user feedback Pre-existing; logging present; non-blocking background call; acceptable UX tradeoff

Blocked (Could Not Fix)

None.


Validation

Check Status
Type check ✅ All 9 packages pass
Lint ✅ 0 warnings, 0 errors
Tests ✅ All tests pass (14 new tests added)

…flowResultCard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coleam00 coleam00 marked this pull request as ready for review April 16, 2026 12:44
@coleam00 coleam00 merged commit b100cd4 into dev Apr 16, 2026
4 checks passed
@coleam00 coleam00 deleted the archon/task-fix-issue-1054 branch April 16, 2026 12:44
joaobmonteiro pushed a commit to joaobmonteiro/Archon that referenced this pull request Apr 26, 2026
…1054

fix(web): interleave tool calls with text during SSE streaming
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.

fix(web): tool calls not interleaved with text during streaming — always appear at bottom

1 participant