Text Duplication in Chat Response UI #9362

Closed
opened 2026-02-16 18:12:16 -05:00 by yindo · 1 comment
Owner

Originally created by @albinartush on GitHub (Feb 14, 2026).

Originally assigned to: @rekram1-node on GitHub.

Description

Bug Report: Text Duplication in Chat Response UI

Summary

Sometime, AI-generated text responses were being duplicated in the opencode client UI, displaying the same content twice (e.g., "Direttive correttamente assimilate. Procedere con l'assimilazione dei tools disponibili?Direttive correttamente assimilate. Procedere con l'assimilazione dei tools disponibili?").

Root Cause Analysis

The duplication was caused by a dual rendering mechanism in the client-side UI component that was not properly synchronized:

1. Affected Component

File: packages/ui/src/components/session-turn.tsx

2. Rendering Architecture

The SessionTurn component renders assistant messages through two separate paths:

Path A: AssistantMessageItem (Line 729-747)

  • Renders all parts of assistant messages including text parts
  • Controlled by the hideResponsePart prop to filter out the last text part
  • Displays text during the streaming process

Path B: Summary Section (Line 756-793)

<Show when={!working() && response()}>
  <div data-slot="session-turn-summary-section">
    <Markdown text={response() ?? ""} cacheKey={responsePartId()} />
  </div>
</Show>
  • Renders the final response text at the bottom when working() is false
  • Intended to show a clean summary after streaming completes

3. The Bug

The hideResponsePart logic in the AssistantMessageItem component (Line 158-181) was not working correctly:

Original problematic code:

if (props.hideResponsePart) {
  const responsePartId = props.responsePartId
  if (responsePartId && responsePartId === lastTextPart()?.id) {
    parts = parts.filter((part) => part?.id !== responsePartId)
  }
}

Issues identified:

  1. The comparison responsePartId === lastTextPart()?.id was unreliable due to SolidJS reactivity timing
  2. lastTextPart() is a memo that might return a different reference or stale value
  3. The condition was too restrictive - it required both IDs to match exactly, which could fail during streaming state transitions

Impact

  • All streaming text responses were duplicated in the UI
  • User experience severely degraded with repeated text
  • Only affected the display layer, not the underlying data or API responses

Fix Applied

Modified the filtering logic in session-turn.tsx (Line 158-181):

Fixed code:

if (props.hideResponsePart) {
  const responsePartId = props.responsePartId
  const lastPart = lastTextPart()
  
  // DEBUG: Log for tracking duplication issues
  console.log("[SessionTurn] hideResponsePart:", props.hideResponsePart, 
              "responsePartId:", responsePartId, 
              "lastTextPart?.id:", lastPart?.id,
              "lastTextPart?.text:", lastPart?.text?.slice(0, 50))
  
  if (responsePartId) {
    // FIX: Always filter when hideResponsePart is true and we have a valid responsePartId
    // Don't compare with lastTextPart, filter directly by ID
    const filtered = parts.filter((part) => part?.id !== responsePartId)
    console.log("[SessionTurn] Parts filtered:", parts.length, "->", filtered.length)
    parts = filtered
  }
}

Key Changes:

  1. Removed unreliable comparison: No longer depends on responsePartId === lastTextPart()?.id
  2. Simplified logic: Filter by responsePartId directly when hideResponsePart is true
  3. Added debugging: Console logs to track filtering behavior during development

Verification Steps

  1. Build the opencode client: npm run build or pnpm build
  2. Start a new chat session
  3. Send any message to trigger AI response
  4. Observe that text appears only once, not duplicated
  5. Check browser console for [SessionTurn] debug logs

Additional Notes

  • The ChromeOpenAPIProxy server was working correctly and sending responses only once
  • This was purely a client-side rendering issue
  • The fix maintains backward compatibility with existing functionality
  • No changes required to the proxy or API layer

Related Files

  • Client UI: packages/ui/src/components/session-turn.tsx
  • Stream Processing: packages/opencode/src/session/processor.ts
  • Event Handling: packages/app/src/context/global-sync/event-reducer.ts

Plugins

No response

OpenCode version

No response

Steps to reproduce

No response

Screenshot and/or share link

No response

Operating System

No response

Terminal

No response

Originally created by @albinartush on GitHub (Feb 14, 2026). Originally assigned to: @rekram1-node on GitHub. ### Description # Bug Report: Text Duplication in Chat Response UI ## Summary Sometime, AI-generated text responses were being duplicated in the opencode client UI, displaying the same content twice (e.g., "Direttive correttamente assimilate. Procedere con l'assimilazione dei tools disponibili?Direttive correttamente assimilate. Procedere con l'assimilazione dei tools disponibili?"). ## Root Cause Analysis The duplication was caused by a **dual rendering mechanism** in the client-side UI component that was not properly synchronized: ### 1. Affected Component **File**: `packages/ui/src/components/session-turn.tsx` ### 2. Rendering Architecture The `SessionTurn` component renders assistant messages through two separate paths: **Path A: AssistantMessageItem (Line 729-747)** - Renders all parts of assistant messages including text parts - Controlled by the `hideResponsePart` prop to filter out the last text part - Displays text during the streaming process **Path B: Summary Section (Line 756-793)** ```tsx <Show when={!working() && response()}> <div data-slot="session-turn-summary-section"> <Markdown text={response() ?? ""} cacheKey={responsePartId()} /> </div> </Show> ``` - Renders the final response text at the bottom when `working()` is false - Intended to show a clean summary after streaming completes ### 3. The Bug The `hideResponsePart` logic in the `AssistantMessageItem` component (Line 158-181) was not working correctly: **Original problematic code:** ```typescript if (props.hideResponsePart) { const responsePartId = props.responsePartId if (responsePartId && responsePartId === lastTextPart()?.id) { parts = parts.filter((part) => part?.id !== responsePartId) } } ``` **Issues identified:** 1. The comparison `responsePartId === lastTextPart()?.id` was unreliable due to SolidJS reactivity timing 2. `lastTextPart()` is a memo that might return a different reference or stale value 3. The condition was too restrictive - it required both IDs to match exactly, which could fail during streaming state transitions ## Impact - All streaming text responses were duplicated in the UI - User experience severely degraded with repeated text - Only affected the display layer, not the underlying data or API responses ## Fix Applied Modified the filtering logic in `session-turn.tsx` (Line 158-181): **Fixed code:** ```typescript if (props.hideResponsePart) { const responsePartId = props.responsePartId const lastPart = lastTextPart() // DEBUG: Log for tracking duplication issues console.log("[SessionTurn] hideResponsePart:", props.hideResponsePart, "responsePartId:", responsePartId, "lastTextPart?.id:", lastPart?.id, "lastTextPart?.text:", lastPart?.text?.slice(0, 50)) if (responsePartId) { // FIX: Always filter when hideResponsePart is true and we have a valid responsePartId // Don't compare with lastTextPart, filter directly by ID const filtered = parts.filter((part) => part?.id !== responsePartId) console.log("[SessionTurn] Parts filtered:", parts.length, "->", filtered.length) parts = filtered } } ``` ### Key Changes: 1. **Removed unreliable comparison**: No longer depends on `responsePartId === lastTextPart()?.id` 2. **Simplified logic**: Filter by `responsePartId` directly when `hideResponsePart` is true 3. **Added debugging**: Console logs to track filtering behavior during development ## Verification Steps 1. Build the opencode client: `npm run build` or `pnpm build` 2. Start a new chat session 3. Send any message to trigger AI response 4. Observe that text appears only once, not duplicated 5. Check browser console for `[SessionTurn]` debug logs ## Additional Notes - The ChromeOpenAPIProxy server was working correctly and sending responses only once - This was purely a client-side rendering issue - The fix maintains backward compatibility with existing functionality - No changes required to the proxy or API layer ## Related Files - **Client UI**: `packages/ui/src/components/session-turn.tsx` - **Stream Processing**: `packages/opencode/src/session/processor.ts` - **Event Handling**: `packages/app/src/context/global-sync/event-reducer.ts` ### Plugins _No response_ ### OpenCode version _No response_ ### Steps to reproduce _No response_ ### Screenshot and/or share link _No response_ ### Operating System _No response_ ### Terminal _No response_
yindo added the bug label 2026-02-16 18:12:16 -05:00
yindo closed this issue 2026-02-16 18:12:16 -05:00
Author
Owner

@alexyaroshuk commented on GitHub (Feb 14, 2026):

? "Fix applied"? What is this? AI output?

@alexyaroshuk commented on GitHub (Feb 14, 2026): ? "Fix applied"? What is this? AI output?
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#9362