Skip to content

Stream AI reasoning as ephemeral thinking chunks#1729

Merged
gugu merged 1 commit into
mainfrom
feature/ai-thinking-stream
Apr 23, 2026
Merged

Stream AI reasoning as ephemeral thinking chunks#1729
gugu merged 1 commit into
mainfrom
feature/ai-thinking-stream

Conversation

@gugu
Copy link
Copy Markdown
Contributor

@gugu gugu commented Apr 23, 2026

Backend emits NDJSON-framed chunks (thinking / thinking_reset / thinking_commit / text) so intermediate tool-loop reasoning is shown transiently in the AI panel and replaced between iterations, leaving only the final answer persistent in the chat history. Also fixes pre-existing biome lint violations in the touched files.

Summary by CodeRabbit

  • New Features

    • Added visibility into AI reasoning with a dedicated "thinking" state display during AI processing
  • Improvements

    • Enhanced streaming AI responses to separately display intermediate thinking and final output
    • Improved visual styling for AI thinking messages across light and dark themes

Backend emits NDJSON-framed chunks (thinking / thinking_reset / thinking_commit / text) so intermediate tool-loop reasoning is shown transiently in the AI panel and replaced between iterations, leaving only the final answer persistent in the chat history. Also fixes pre-existing biome lint violations in the touched files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 23, 2026 12:18
@gugu gugu merged commit c279c85 into main Apr 23, 2026
15 of 18 checks passed
@gugu gugu deleted the feature/ai-thinking-stream branch April 23, 2026 12:19
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 44974504-17f8-4d32-90f1-b3945f00bfc2

📥 Commits

Reviewing files that changed from the base of the PR and between 0ac237d and 2c9ed83.

📒 Files selected for processing (6)
  • backend/src/entities/ai/use-cases/request-info-from-table-with-ai-v7.use.case.ts
  • frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css
  • frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.html
  • frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.ts
  • frontend/src/app/services/ai.service.ts
  • frontend/src/app/services/api.service.ts

📝 Walkthrough

Walkthrough

This pull request refactors AI streaming to emit structured JSON-line events instead of raw text chunks. The backend now sends thinking, thinking_reset, and thinking_commit events during tool-loop execution. The frontend introduces a typed AiStreamChunk interface, updates services to parse these events, and adds UI to display thinking/processing states separately from final responses.

Changes

Cohort / File(s) Summary
Backend Streaming Logic
backend/src/entities/ai/use-cases/request-info-from-table-with-ai-v7.use.case.ts
Changed from raw SSE text writes to structured JSON-line events. Each model chunk emits a thinking event; control-flow milestones emit thinking_reset (before tool execution) and thinking_commit (when no tool calls pending). Accumulator updates deferred to commit time. Logger statements condensed to single-line format.
Frontend Type Definitions & Parsing
frontend/src/app/services/api.service.ts
Added AiStreamChunk union type for structured events. Updated postStream to parse newline-delimited JSON, yielding only successfully parsed chunks. Removed async from postResponse method. Simplified header assignments to dot notation.
Frontend Service Layer
frontend/src/app/services/ai.service.ts
Updated AiStreamResponse interface and sendMessage return type to yield typed AsyncGenerator<AiStreamChunk> instead of raw strings.
Frontend Component Logic
frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.ts
Modeled messages with typed AiStreamChunk events. Added optional thinking field to message entries, initialized to empty string. _consumeStream now processes chunk types: accumulates thinking, resets/commits on milestones, appends text chunks. Strongly typed _loadingStepsInterval.
Frontend Component Template
frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.html
Conditional rendering: displays separate "thinking" UI when message.thinking exists and message.text is empty; renders markdown only when message.text exists. Added explicit type="button" to all button elements.
Frontend Component Styling
frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css
Added .ai-thinking class with italic, semi-transparent text styling suited for preformatted content. Added dark theme override via prefers-color-scheme: dark.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Component
    participant AiService as AI Service
    participant ApiService as API Service
    participant Backend as Backend (SSE)

    Client->>AiService: sendMessage(query)
    AiService->>ApiService: postStream(request)
    Backend-->>ApiService: {type:'thinking', content:'...'} (newline)
    ApiService->>ApiService: Parse JSON line
    ApiService-->>AiService: yield AiStreamChunk
    
    AiService->>Client: Stream chunk
    Client->>Client: Accumulate thinking field
    
    Backend-->>ApiService: {type:'thinking_reset'} (newline)
    ApiService->>ApiService: Parse JSON line
    ApiService-->>AiService: yield AiStreamChunk
    Client->>Client: Reset thinking, await tool execution
    
    Backend-->>ApiService: {type:'text', content:'response'} (newline)
    ApiService->>ApiService: Parse JSON line
    ApiService-->>AiService: yield AiStreamChunk
    Client->>Client: Append to text, clear thinking
    
    Backend-->>ApiService: Stream ends
    ApiService->>ApiService: Flush buffered tail
    ApiService-->>AiService: Final chunks
    AiService-->>Client: Stream complete
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Streams flow structured now, in JSON lines so neat,
Thinking thoughts separated, before responses complete,
From raw chunks to typed events, a cleaner path we trace,
Control flows clear and tidy, in this async space! ✨

✨ 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 feature/ai-thinking-stream

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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the AI request streaming protocol so the backend emits NDJSON-framed chunks that distinguish ephemeral “thinking” from final “text”, and updates the Angular client to parse/render those chunks in the AI panel (showing transient thinking that is reset between tool-loop iterations).

Changes:

  • Backend: emit newline-delimited JSON chunks (thinking, thinking_reset, thinking_commit, text) instead of raw text writes during the tool loop.
  • Frontend: parse NDJSON stream into typed AiStreamChunks and update the AI panel to display transient thinking until a commit/final text is produced.
  • UI/lint touch-ups: button type="button", interval typing, mermaid typing, and related styling for the thinking display.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
frontend/src/app/services/api.service.ts Introduces AiStreamChunk and NDJSON parsing in postStream()
frontend/src/app/services/ai.service.ts Propagates stream type change to AsyncGenerator<AiStreamChunk>
frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.ts Consumes typed chunks and maintains thinking vs text message state
frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.html Renders transient thinking and adds type="button" to buttons
frontend/src/app/components/dashboard/db-table-view/db-table-ai-panel/db-table-ai-panel.component.css Adds styling for .ai-thinking
backend/src/entities/ai/use-cases/request-info-from-table-with-ai-v7.use.case.ts Writes NDJSON chunks via writeChunk() and introduces thinking reset/commit semantics

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 121 to +129
const { done, value } = await reader.read();
if (done) break;
yield decoder.decode(value, { stream: true });
if (done) {
const tail = buffer.trim();
if (tail) {
const parsed = tryParseChunk(tail);
if (parsed) yield parsed;
}
break;
}
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In chunkStream(), when done is true you parse the remaining buffer, but you never flush the TextDecoder state. With { stream: true }, multi-byte characters can be buffered internally and will be lost unless you call decoder.decode() once at the end to flush pending bytes before processing the tail.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +33
const parsed = JSON.parse(line);
if (parsed && typeof parsed.type === 'string') {
return parsed as AiStreamChunk;
}
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tryParseChunk currently accepts any JSON object with a string type and then casts it to AiStreamChunk. This can yield invalid shapes at runtime (e.g., {type:"thinking"} without content, or unknown type values), which will later be treated as valid and can append undefined into the UI. Consider validating type against the allowed set and, for thinking/text, also validating typeof content === 'string' before returning.

Suggested change
const parsed = JSON.parse(line);
if (parsed && typeof parsed.type === 'string') {
return parsed as AiStreamChunk;
}
const parsed: unknown = JSON.parse(line);
if (!parsed || typeof parsed !== 'object') {
return null;
}
const chunk = parsed as { type?: unknown; content?: unknown };
switch (chunk.type) {
case 'thinking':
return typeof chunk.content === 'string'
? { type: 'thinking', content: chunk.content }
: null;
case 'thinking_reset':
return { type: 'thinking_reset' };
case 'thinking_commit':
return { type: 'thinking_commit' };
case 'text':
return typeof chunk.content === 'string'
? { type: 'text', content: chunk.content }
: null;
default:
return null;
}

Copilot uses AI. Check for mistakes.
| { type: 'thinking_commit' }
| { type: 'text'; content: string },
): void {
response.write(JSON.stringify(chunk) + '\n');
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stream is now NDJSON-framed (JSON.stringify(chunk) + "\n"), but the response headers are still configured as text/event-stream in setupResponseHeaders(). This content-type mismatch can confuse intermediaries/clients and is not a valid SSE payload format. Please update the headers to reflect NDJSON (e.g., application/x-ndjson; charset=utf-8) or change the payload to proper SSE framing if SSE is intended.

Suggested change
response.write(JSON.stringify(chunk) + '\n');
response.write(`data: ${JSON.stringify(chunk)}\n\n`);

Copilot uses AI. Check for mistakes.
gugu added a commit that referenced this pull request Apr 23, 2026
Update mockStream to yield AiStreamChunk objects instead of raw strings
so _consumeStream consumes them correctly, and align the expected AI
message with the component's thinking field after #1729.

Also add pnpm overrides for fast-xml-parser and uuid to fix advisories.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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