Skip to content

feat(agent): present reasoning parts inline in message stream (#122)#347

Open
hiqiancheng wants to merge 3 commits into
mainfrom
feat/interleaved-reasoning-parts
Open

feat(agent): present reasoning parts inline in message stream (#122)#347
hiqiancheng wants to merge 3 commits into
mainfrom
feat/interleaved-reasoning-parts

Conversation

@hiqiancheng

Copy link
Copy Markdown
Collaborator

Summary

When a model emits reasoning and answer text in alternating segments, the current architecture renders all reasoning as a single collapsible block above the answer, losing the temporal order. This PR introduces ReasoningMessagePart to present reasoning segments inline within the message parts stream, styled as collapsible cards with a brain icon and live duration.

Key behavior changes:

  • Reasoning chunks create ordered reasoning parts in the message parts[] array, preserving their temporal relationship with text and tool call segments
  • Each reasoning segment renders as a card matching the tool call card style: brain icon, status pill, duration
  • Expanded reasoning content is shown in italic with light-gray styling
  • Cards auto-expand when created and auto-collapse when streaming ends (unless user manually toggled)
  • Duration is frozen when the segment ends, displayed as "已推理 Xs"
  • Old messages with only a flat reasoning field continue to render via the legacy fallback section

Related issue or RFC

AI assistance disclosure

  • Tool(s) used: Claude Code
  • Scope of assistance: Implementation of all code changes, type checking, and test execution
  • Human review or rewrite performed: All changes reviewed by human before PR
  • Architecture or boundary impact: Projection layer (projection.ts) and UI layer (AssistantMessage.vue, new ReasoningPartItem.vue); no AgentService core or runtime impact

Testing evidence

  • Type check: pnpm --filter desktop type:check passed (vue-tsc --noEmit, no errors)
  • Unit tests: pnpm --filter desktop test:run passed (133 files, 673 tests)
  • Pre-commit hook skipped due to E: drive disk space issue (ENOSPC on npm cache write); tests verified separately
pnpm --filter desktop type:check    # passed
pnpm --filter desktop test:run      # 133 files, 673 tests passed

Risk notes

  • AgentService, runtime, MCP, or schema impact: Low — only projection layer and UI components affected
  • database baseline or migration impact: None
  • release or packaging impact: None

Screenshots or recordings

N/A — UI changes are internal to the conversation panel; no screenshot provided (dev server not running due to disk constraints).

Checklist

  • The PR title follows Conventional Commits and is valid for squash merge.
  • This PR is either ready for review or explicitly marked as a Draft PR.
  • I did not use [WIP] or similar title prefixes.
  • If AI materially assisted this PR, I disclosed the tools and scope and I personally reviewed every affected change.
  • I can explain the why, what, and how of this change without relying on an AI tool.
  • If this touches AgentService, runtime, MCP, or schema boundaries, there is an accepted RFC.
  • If this changes architecture or adds a new cross-boundary abstraction, there is an accepted RFC.
  • I ran pnpm test:pr for this code PR, or this is a docs-only change.
  • If I changed Rust behavior or tests, I reviewed pnpm test:coverage:rust or relied on CI coverage evidence.
  • If I changed desktop startup/window/search/popup/settings/E2E paths, I ran pnpm test:e2e locally or documented why CI is the first valid proof.
  • I added tests or explained why tests are not appropriate.
  • I updated docs when behavior changed.

PR #216 moved scripts/ci/pr-template-check.js to
apps/desktop/scripts/ci/pr-template-check.js but did not update the
workflow reference. This caused the PR Template Check workflow to fail
with "Cannot find module" on every PR.
Instead of rendering all reasoning as a single collapsible block above
the answer, reasoning chunks now create ordered `reasoning` parts in the
message parts array. Each reasoning segment is rendered as a card with a
brain icon and live duration, matching the tool call card style. The
expanded content is shown in italic with light-gray styling.

Reasoning cards auto-expand when created and auto-collapse when streaming
ends, unless the user has manually toggled them. Duration is frozen when
the segment ends and displayed as "已推理 Xs". Old messages with only a
flat `reasoning` field continue to render via the legacy fallback section.

Closes #122

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @hiqiancheng, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@github-actions github-actions Bot added area:frontend Frontend UI or view-layer changes area:agent-service AgentService and conversation runtime changes labels May 31, 2026
@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Reasoning progress now displays streaming status and completion state
    • Added real-time duration tracking for reasoning sessions
    • Reasoning parts now render in collapsible cards with status indicators
    • Improved multi-language support with new reasoning-related message labels

Walkthrough

This PR implements interleaved reasoning presentation by modeling reasoning as discrete message parts instead of a single aggregate buffer. The projection service now streams reasoning chunks into dedicated ReasoningMessagePart entries, a new component renders each reasoning segment inline with streaming state, and AssistantMessage integrates these parts into the message flow while preserving backward compatibility with legacy reasoning.

Changes

Interleaved Reasoning Presentation

Layer / File(s) Summary
ReasoningMessagePart data model
apps/desktop/src/types/session.ts
Introduces ReasoningMessagePart interface with type: 'reasoning', content, and optional startedAt / durationMs timing fields. Extends SessionPart union to include reasoning parts alongside text, tool, approval, and widget parts.
Reasoning part factory and utilities
apps/desktop/src/utils/session.ts
Adds createReasoningPart(content) helper that generates a UUID-based ReasoningMessagePart with type: 'reasoning', provided content, and current timestamp for startedAt.
Projection layer: reasoning part streaming
apps/desktop/src/services/AgentService/task/projection/projection.ts
Updates SessionTaskProjection to track activeReasoningPartId and stream reasoning chunks into dedicated parts. Appends content to the active part if still reasoning, otherwise creates a new part. Freezes active parts when transitioning to text or reaching terminal states. Extends visibility logic to treat non-empty reasoning parts as visible content.
ReasoningPartItem component
apps/desktop/src/views/SearchView/components/ConversationPanel/components/ReasoningPartItem.vue
Renders a collapsible reasoning card with streaming/completed status pill, optional pulse animation, and real-time elapsed duration. Auto-collapses when streaming ends unless user manually toggled it. Updates elapsed time every 1s while streaming if durationMs is absent.
AssistantMessage: reasoning parts integration
apps/desktop/src/views/SearchView/components/ConversationPanel/components/AssistantMessage.vue
Adds hasReasoningParts computed value to detect reasoning parts. Extends RenderedPart union with reasoning variant. Updates renderedParts logic to create rendered-part entries for part.type === 'reasoning' and render via ReasoningPartItem. Adjusts legacy reasoning conditional and auto-collapse behavior to only apply when no reasoning parts exist.
Internationalization
apps/desktop/src/i18n/messages.ts
Adds three i18n keys for reasoning part states: assistant.reasoningPart.streaming, assistant.reasoningPart.completed, and assistant.reasoningPart.duration (with {duration} placeholder) to both zh-CN and en-US message dictionaries.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Suggested labels

area:agent-service

Poem

📝 Reasoning now flows in time,
no longer piled up in a heap,
segments stream in measured rhyme,
thoughts and answers leap by leap.
ReasoningPartItem shines so bright,
keeping the truth in sight! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title follows Conventional Commits format with 'feat(agent):' prefix and accurately describes the main change of presenting reasoning parts inline in the message stream, matching the PR's primary objectives.
Description check ✅ Passed The description comprehensively covers all template sections: summary, related issue (#122), AI assistance disclosure, testing evidence, risk notes, and completed checklist items demonstrating thorough documentation.
Linked Issues check ✅ Passed The PR fully implements the objectives from issue #122: introduces ReasoningMessagePart for interleaved reasoning parts, renders them inline as collapsible cards with proper styling, preserves stream order, maintains backward compatibility, and ensures streaming stability.
Out of Scope Changes check ✅ Passed All changes are narrowly scoped to implementing interleaved reasoning presentation: message type definitions, projection layer updates, UI components, and i18n messages. No out-of-scope modifications to AgentService core, runtime, or schema.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/interleaved-reasoning-parts
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/interleaved-reasoning-parts

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/services/AgentService/task/projection/projection.ts (1)

429-440: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Consider resetting activeReasoningPartId in checkpoint restore for consistency.

While freezeActiveReasoningPart safely handles a stale activeReasoningPartId (by checking if the part exists), explicitly resetting the tracker during restoreCheckpointPresentation would make the lifecycle clearer and avoid an unnecessary lookup on the first post-restore reasoning chunk.

♻️ Suggested addition
 private restoreCheckpointPresentation(): boolean {
     if (!this.lastCheckpointPresentation) {
         return false;
     }

     this.snapshot.sessionHistory = cloneValue(this.lastCheckpointPresentation.sessionHistory);
     this.activeAssistantMessageId = this.lastCheckpointPresentation.activeAssistantMessageId;
     this.response = this.lastCheckpointPresentation.response;
     this.reasoning = this.lastCheckpointPresentation.reasoning;
+    this.activeReasoningPartId = null;
     this.widgets.resetTransientState();
     return true;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/services/AgentService/task/projection/projection.ts` around
lines 429 - 440, The restoreCheckpointPresentation method should also clear the
activeReasoningPartId to avoid a stale lookup later; update
restoreCheckpointPresentation to set this.activeReasoningPartId (or the
appropriate tracker field) to the value from lastCheckpointPresentation or to
undefined/null as intended by the lifecycle so that freezeActiveReasoningPart
won't have to handle a stale id on first post-restore reasoning chunk — locate
restoreCheckpointPresentation, lastCheckpointPresentation, and
activeReasoningPartId in projection.ts and add the explicit reset alongside
restoring sessionHistory, activeAssistantMessageId, response, and reasoning
before calling widgets.resetTransientState.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/desktop/src/views/SearchView/components/ConversationPanel/components/AssistantMessage.vue`:
- Line 18: The div element in AssistantMessage.vue that currently reads as a
single long line containing both the v-if expression (message.reasoning &&
!hasReasoningParts) and the class attribute (class="reasoning-section mb-4
w-full") should be reformatted onto multiple lines to satisfy Prettier; locate
the div in the template (the element using v-if with message.reasoning and
hasReasoningParts) and break the attributes across lines (e.g., put the opening
tag, v-if, and class on separate lines or each attribute on its own line) so the
line length is reduced while preserving the same v-if condition and class name
values.

---

Outside diff comments:
In `@apps/desktop/src/services/AgentService/task/projection/projection.ts`:
- Around line 429-440: The restoreCheckpointPresentation method should also
clear the activeReasoningPartId to avoid a stale lookup later; update
restoreCheckpointPresentation to set this.activeReasoningPartId (or the
appropriate tracker field) to the value from lastCheckpointPresentation or to
undefined/null as intended by the lifecycle so that freezeActiveReasoningPart
won't have to handle a stale id on first post-restore reasoning chunk — locate
restoreCheckpointPresentation, lastCheckpointPresentation, and
activeReasoningPartId in projection.ts and add the explicit reset alongside
restoring sessionHistory, activeAssistantMessageId, response, and reasoning
before calling widgets.resetTransientState.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 23239d35-697a-4175-a7c4-447caa598d48

📥 Commits

Reviewing files that changed from the base of the PR and between 3c3b10e and 9fe0989.

📒 Files selected for processing (6)
  • apps/desktop/src/i18n/messages.ts
  • apps/desktop/src/services/AgentService/task/projection/projection.ts
  • apps/desktop/src/types/session.ts
  • apps/desktop/src/utils/session.ts
  • apps/desktop/src/views/SearchView/components/ConversationPanel/components/AssistantMessage.vue
  • apps/desktop/src/views/SearchView/components/ConversationPanel/components/ReasoningPartItem.vue
📜 Review details
🧰 Additional context used
🪛 ESLint
apps/desktop/src/views/SearchView/components/ConversationPanel/components/AssistantMessage.vue

[error] 18-18: Replace ·v-if="message.reasoning·&&·!hasReasoningParts"·class="reasoning-section·mb-4·w-full" with ⏎························v-if="message.reasoning·&&·!hasReasoningParts"⏎························class="reasoning-section·mb-4·w-full"⏎····················

(prettier/prettier)

🔇 Additional comments (14)
apps/desktop/src/types/session.ts (1)

78-91: LGTM!

apps/desktop/src/utils/session.ts (1)

14-21: LGTM!

apps/desktop/src/services/AgentService/task/projection/projection.ts (4)

196-204: LGTM!


208-208: LGTM!

Also applies to: 247-248, 261-261, 291-291


452-470: LGTM!


493-495: LGTM!

apps/desktop/src/views/SearchView/components/ConversationPanel/components/ReasoningPartItem.vue (4)

65-76: LGTM!


78-107: LGTM!

Also applies to: 121-121


175-177: LGTM!

Also applies to: 184-191


115-126: LGTM!

Also applies to: 128-136, 152-173

apps/desktop/src/views/SearchView/components/ConversationPanel/components/AssistantMessage.vue (3)

264-266: LGTM!


71-75: LGTM!

Also applies to: 172-176, 294-303


359-366: LGTM!

apps/desktop/src/i18n/messages.ts (1)

508-510: LGTM!

Also applies to: 1251-1253

<!-- 推理过程显示(如果存在)-->
<div v-if="message.reasoning" class="reasoning-section mb-4 w-full">
<!-- 推理过程显示(兼容老消息:没有 reasoning parts 时)-->
<div v-if="message.reasoning && !hasReasoningParts" class="reasoning-section mb-4 w-full">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix prettier formatting violation.

The line exceeds prettier's preferred length and should be reformatted to multiple lines for readability.

🎨 Prettier fix
-                    <div v-if="message.reasoning && !hasReasoningParts" class="reasoning-section mb-4 w-full">
+                    <div
+                        v-if="message.reasoning && !hasReasoningParts"
+                        class="reasoning-section mb-4 w-full"
+                    >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div v-if="message.reasoning && !hasReasoningParts" class="reasoning-section mb-4 w-full">
<div
v-if="message.reasoning && !hasReasoningParts"
class="reasoning-section mb-4 w-full"
>
🧰 Tools
🪛 ESLint

[error] 18-18: Replace ·v-if="message.reasoning·&&·!hasReasoningParts"·class="reasoning-section·mb-4·w-full" with ⏎························v-if="message.reasoning·&&·!hasReasoningParts"⏎························class="reasoning-section·mb-4·w-full"⏎····················

(prettier/prettier)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/views/SearchView/components/ConversationPanel/components/AssistantMessage.vue`
at line 18, The div element in AssistantMessage.vue that currently reads as a
single long line containing both the v-if expression (message.reasoning &&
!hasReasoningParts) and the class attribute (class="reasoning-section mb-4
w-full") should be reformatted onto multiple lines to satisfy Prettier; locate
the div in the template (the element using v-if with message.reasoning and
hasReasoningParts) and break the attributes across lines (e.g., put the opening
tag, v-if, and class on separate lines or each attribute on its own line) so the
line length is reduced while preserving the same v-if condition and class name
values.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agent-service AgentService and conversation runtime changes area:frontend Frontend UI or view-layer changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Preserve interleaved reasoning instead of rendering all thinking up front

1 participant