Repository navigation
v2.2.0 - feat:thinking/reasoning stream for chat + agent - #60
Conversation
Fixes CodeRabbit ESLint install failure — the eslint.config.mjs imports from 'eslint/config' (@eslint/core) which was only available transitively through eslint@9 and not resolvable in clean installs.
Markdown: - Install rehype-raw and add as first rehype plugin in all three pipelines (NoteMarkdownPreview, note-editor, MarkdownContent). This preserves GitHub-style raw HTML like <p align="center">, <img>, <details>, <summary> instead of stripping them. Chat sessions: - SessionPane: handleNewChatThread now calls setActiveChatThreadId with the newly created thread's id, so the chat panel immediately switches to the new session instead of keeping the old one. - ChatPanel: the init effect now detects when activeProjectId changes and switches to a thread scoped to the new project (via getOrCreateThread). Previously the effect short-circuited whenever activeChatThreadId was non-null, so switching projects kept showing the old project's chat thread.
Thinking / reasoning support for AI chat and Cairn Agent: - Parse delta.reasoning from OpenAI-compatible SSE on both chat (electron/ipc/chat.ts) and pi-agent (electron/lib/pi-agent-loop.ts) streaming paths; emit chat:thought / pi-agent:thought IPC channels - New shared ThinkingPanel component: expanded while reasoning streams, auto-collapses on first content token, re-expandable via chevron, persisted state collapsed by default - Persist reasoning text to chat_messages.reasoning and pi_agent_messages.reasoning (DB migration v20) - Parse completion_tokens_details.reasoning_tokens from usage; surface Answer / Thinking / Total breakdown in ContextRing popover - New preload listeners: chat.onThought, piAgent.onThought - Reasoning stripped from compaction summaries and LLM history replay Also includes: - GitHub-style raw HTML in markdown (rehype-raw) [from earlier branch commit] - Chat thread selection + project switching fix [from earlier branch commit] - @eslint/core explicit devDependency [from earlier branch commit] - v2.2.0 changelog - Version bump to 2.2.0
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
🚧 Files skipped from review as they are similar to previous changes (6)
📝 WalkthroughWalkthroughAdds end-to-end "thinking/reasoning stream" support: a DB migration adds ChangesReasoning/Thinking Stream Feature
Sequence Diagram(s)sequenceDiagram
participant Model
participant AgentLoop as Agent/Tool Loop
participant EventEmitter as IPC Emitter
participant Preload
participant RendererStore
participant UI
Model->>AgentLoop: stream delta.reasoning
AgentLoop->>AgentLoop: accumulate in reasoningBuffer
AgentLoop->>EventEmitter: emit thought event
EventEmitter->>Preload: forward thought event
Preload->>RendererStore: onThought callback
RendererStore->>UI: update streamingThought
Model->>AgentLoop: stream usage with reasoning_tokens
AgentLoop->>EventEmitter: emit usage event
EventEmitter->>Preload: forward usage event
Preload->>RendererStore: onUsage callback
RendererStore->>UI: update reasoningTokens
AgentLoop->>AgentLoop: finalize turn with accumulated reasoning
EventEmitter->>Preload: emit done event
Preload->>RendererStore: onDone callback
RendererStore->>UI: render ThinkingPanel with persisted reasoning
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed: one or more packages not found in the registry. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 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 `@electron/ipc/chat.ts`:
- Around line 146-152: In the catch block handling the Local LLM Engine error on
the return statement, change the reasoning field from an empty string to
accumulatedReasoning to maintain consistency with abort path returns (found at
lines 97, 174, 243). This ensures that any reasoning accumulated across multiple
rounds is preserved in the chat:done payload instead of being discarded when an
error occurs in a later round.
- Around line 255-268: The assistantMsg object includes a reasoning field that
will cause API validation errors when sent to OpenAI or Anthropic. Either remove
the reasoning field from the assistantMsg object before it is pushed to the
messages array, or implement provider-specific handling that removes the
reasoning field for OpenAI messages while preserving the original signed
thinking blocks for Anthropic. The reasoning field should not be included in the
messages array that gets sent to the API endpoint since both providers have
strict schema validation and handle reasoning differently internally.
In `@electron/lib/pi-agent-loop.ts`:
- Line 63: The AgentAssistantMsg interface on Line 63 includes an optional
reasoning field that should be preserved in session.messages for UI and database
persistence, but must not be sent to the LLM. To fix this, create a utility
function that strips the reasoning field from assistant messages when they are
prepared for outbound LLM requests. Apply this stripping function at the points
where contextMessages are populated (around Line 462) and where messages are
sent to the LLM (around Line 484), as well as in any other locations mentioned
in the PR comments (630-647). Keep the reasoning field in the interface
definition and in session.messages itself, but ensure it is removed only when
creating the messages array for LLM communication.
In `@src/components/chat/chat-panel/ThinkingPanel.tsx`:
- Around line 41-45: The useEffect at lines 41-45 includes `open` in its
dependency array, which causes the effect to re-run whenever `open` changes.
Since this effect sets `open` to false, it creates a circular dependency that
can conflict with the second effect at lines 48-52 which opens the panel. Remove
`open` from the dependency array of the first useEffect to prevent it from being
triggered by its own state changes, ensuring the closing logic in the first
effect does not create a ping-pong with the opening logic in the second effect.
- Line 91: In the ThinkingPanel component, the border styling on line 91
currently uses the Tailwind alpha variant syntax with border-[var(--border)]/50,
which does not follow the repo's styling guidelines for alpha colors. Replace
the border-[var(--border)]/50 class with the proper color-mix syntax that uses
color-mix(in srgb, var(--border) 50%, transparent) to apply the 50% alpha
transparency to the border color variable, ensuring compliance with the
repository's CSS variable and color-mix requirements.
In `@src/store/slices/terminal-sessions.ts`:
- Around line 182-214: The appendPiThought function creates streaming assistant
messages that can have empty content but non-empty reasoning. The blank-message
cleanup logic (referenced at Line 224) removes messages with empty content
without checking if reasoning exists. Update the cleanup condition that filters
out blank messages to also preserve messages when they have non-empty reasoning.
Specifically, modify the condition that determines whether to drop a piMessage
to check if either content or reasoning is non-empty, so reasoning-only
streaming turns are not lost during finalization.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 63451782-e0f8-409d-8e63-82a8ad386c96
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (25)
changelogs/v2.2.0.mdelectron/db/queries.tselectron/db/schema.tselectron/ipc/chat.tselectron/ipc/pi-agent.tselectron/lib/pi-agent-loop.tselectron/preload.tselectron/shared/db-mappers.tspackage.jsonsrc/app/page.tsxsrc/components/agent/AgentChatPane.tsxsrc/components/agent/AgentMessageBubble.tsxsrc/components/agent/ContextRing.tsxsrc/components/agent/SessionPane.tsxsrc/components/chat/chat-panel/ChatMessageBubble.tsxsrc/components/chat/chat-panel/MarkdownContent.tsxsrc/components/chat/chat-panel/ThinkingPanel.tsxsrc/components/chat/chat-panel/ToolCallIndicator.tsxsrc/components/chat/chat-panel/index.tsxsrc/components/notes/NoteMarkdownPreview.tsxsrc/components/notes/note-editor.tsxsrc/hooks/useChatStream.tssrc/store/slices/chat.tssrc/store/slices/terminal-sessions.tssrc/types/index.ts
- Preserve accumulatedReasoning on local LLM error return - Strip reasoning field from assistant messages before sending to API (chat.ts streaming + localllm paths, pi-agent-loop contextMessages) - Remove circular useEffect dep in ThinkingPanel auto-collapse - Use color-mix for border alpha per styling guidelines - Preserve reasoning-only pi messages during finalisation
…ition Use a ref to track previous text presence so the re-expand effect doesn't reopen the panel after auto-collapse while text remains non-empty (ping-pong between the two effects).
…chema Zod's .positive() generates exclusiveMinimum: 0 which many OpenAI-compatible proxies reject. Using .min(1) produces minimum: 1 instead — semantically equivalent and universally accepted.
Capture thought_signature from SSE delta.tool_calls[] and persist on assistant tool_calls[] in both runToolLoop and runAgentLoop so Gemini 3.x with thinking can resume its reasoning state across multi-turn tool calls. Extended OpenAIMessage.tool_calls[] and ToolCallSpec with an optional thought_signature field — opaque to Cairn, only meaningful when round-tripped back to the proxy/Vertex. Other providers ignore it. The reasoning-stripper in pi-agent contextMessages only destructures reasoning — thought_signature passes through untouched.
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
|
@coderabbitai please review we had an error with your last comment |
🧩 Analysis chain🏁 Script executed: #!/bin/bash
gh pr view 60 --repo ddutchie/cairn --json comments --jq '.comments[] | {author: .author.login, body: .body[0:500], createdAt: .createdAt}'Length of output: 1464
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 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 `@electron/ipc/chat.ts`:
- Around line 440-442: When abortCtrl.signal.aborted is true, the chat:done
message being sent hardcodes reasoning to an empty string, which discards the
partial reasoning that was accumulated by runToolLoop(...) before the abort
occurred. Replace the hardcoded `reasoning: ""` in the send call with the actual
accumulated reasoning variable that contains the partial reasoning from the
aborted runToolLoop(...) execution.
In `@electron/lib/pi-agent-loop.ts`:
- Around line 469-475: The reasoning field is being stripped from assistant
messages after the pruner function is called, but transformContext may process
messages while they still contain reasoning data, potentially leaking thinking
text into summaries or LLM history. Move the reasoning stripping to occur before
invoking the pruner by creating a copy of session.messages, removing the
reasoning field from any assistant messages in that copy, then passing the
cleaned copy to the pruner function instead of the raw messages. Optionally keep
the existing post-pruner map strip as a safety net to ensure custom context
transforms cannot access reasoning data.
In `@electron/preload.ts`:
- Around line 141-143: The onUsage callback parameter type in the preload is
missing the breakdown field that the main process emits in its IPC payload.
Locate the onUsage function handler definition around lines 141-143 and add the
missing breakdown property to the callback parameter object type to match the
actual IPC payload structure being emitted from the main process. Also apply the
same type fix to the other onUsage callback location mentioned in the comment.
In `@src/components/agent/AgentChatPane.tsx`:
- Around line 181-184: The onThought listener currently only handles parent
sessionIds and ignores subagent session IDs with the pattern ${sessionId}:sub:*,
while the token handling on lines 301-304 includes dedicated subagent
subscription logic. Add a matching subagent thought subscription and cleanup
alongside the parent thought subscription in the electron.piAgent.onThought
listener block, ensuring that subagent thought events matching the
${sessionId}:sub:* pattern are properly routed to appendPiThought with their
respective subagent sessionIds, just as token subscriptions are handled
separately for subagents.
- Around line 186-192: The usage breakdown information from the event object is
not being passed to the update functions, which causes the breakdown data to be
lost. Add the `e.breakdown` parameter to both the `updatePiUsage` call (for the
parent step branch) and the `updatePiSubagentUsage` call (for the subagent step
branch), placing it after the `reasoningTokens ?? 0` parameter in each function
call to preserve the usage breakdown.
In `@src/components/chat/chat-panel/index.tsx`:
- Around line 283-303: The useEffect in the chat-panel component tracks project
changes via prevProjectIdRef but does not track workspace changes, even though
activeWorkspaceId is a dependency. When the workspace changes while
activeProjectId remains the same (particularly when undefined), the old
workspace's active thread may be retained. Create a ref to track the previous
workspace ID similar to prevProjectIdRef, update prevWorkspaceIdRef at the start
of the effect, and add workspace change detection logic that invalidates the
active thread when activeWorkspaceId changes, ensuring threads are not retained
across workspace boundaries.
In `@src/components/chat/chat-panel/MarkdownContent.tsx`:
- Line 7: Add HTML sanitization to prevent injection attacks when parsing raw
HTML in markdown. First, add rehype-sanitize to the project dependencies, then
import it alongside the existing rehypeRaw import in MarkdownContent.tsx (line
7). In the rehype plugins array (around line 101 in MarkdownContent.tsx), ensure
rehypeSanitize is applied immediately after rehypeRaw to sanitize the parsed
HTML before rendering. Apply the same pattern to NoteMarkdownPreview.tsx (line
48) and note-editor.tsx (line 1007) by adding the import and placing the
sanitization plugin after rehypeRaw in their respective rehype plugin
configurations.
In `@src/components/chat/chat-panel/ThinkingPanel.tsx`:
- Around line 49-56: The useEffect hook that handles re-expanding the thinking
panel on new reasoning tokens needs to also check whether companion content (the
answer) has already started streaming. Currently, the condition only checks for
`streaming`, `text`, `hadText`, and `userOverride`, but doesn't account for late
reasoning deltas arriving after answer streaming has commenced. Add a check to
verify that `companionContent` is empty before allowing the panel to reopen when
the first reasoning token arrives, ensuring the panel won't reopen if an answer
is already being streamed and was previously auto-collapsed.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: afb5c177-9ace-4a5a-b2e7-ffd73d07cd1a
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (27)
changelogs/v2.2.0.mdelectron/db/queries.tselectron/db/schema.tselectron/ipc/chat.tselectron/ipc/pi-agent.tselectron/lib/llm.tselectron/lib/pi-agent-loop.tselectron/lib/tool-schemas.tselectron/preload.tselectron/shared/db-mappers.tspackage.jsonsrc/app/page.tsxsrc/components/agent/AgentChatPane.tsxsrc/components/agent/AgentMessageBubble.tsxsrc/components/agent/ContextRing.tsxsrc/components/agent/SessionPane.tsxsrc/components/chat/chat-panel/ChatMessageBubble.tsxsrc/components/chat/chat-panel/MarkdownContent.tsxsrc/components/chat/chat-panel/ThinkingPanel.tsxsrc/components/chat/chat-panel/ToolCallIndicator.tsxsrc/components/chat/chat-panel/index.tsxsrc/components/notes/NoteMarkdownPreview.tsxsrc/components/notes/note-editor.tsxsrc/hooks/useChatStream.tssrc/store/slices/chat.tssrc/store/slices/terminal-sessions.tssrc/types/index.ts
…threads - Preserve loopResult.reasoning on abort in chat:done payload - Strip reasoning before pruner in pi-agent contextMessages to prevent leaking thinking text into summaries or transforms; keep post-pruner map as safety net - Add breakdown field to onUsage callback types in preload - Pass e.breakdown to updatePiUsage/updatePiSubagentUsage - Add appendPiSubagentThought store action + subagent thought subscription/cleanup in AgentChatPane - Track workspace changes via prevWorkspaceIdRef in chat-panel to invalidate active thread across workspace boundaries - ThinkingPanel re-expand checks companionContent so panel won't reopen if answer streaming has already started
What does this PR do?
Adds thinking/reasoning stream support to AI chat and the Cairn Agent — model reasoning text (Claude
thinking_delta, OpenAIdelta.reasoning) now streams through a separate IPC channel and renders in a collapsible Thinking panel above the assistant's reply, auto-collapsing when content starts streaming. Reasoning is persisted to DB so past messages retain their thinking panel across restarts. Also ships GitHub-style raw HTML in markdown, a chat thread selection fix, and the v2.2.0 changelog.Type of change
Screenshots / recording
Checklist
npm run type-check:allpassesnpm run lintpassesnpm testpasses (runsnpm run compilefirst soelectron/bundle-guard.test.tsactually executes —npm run test:bundleto run just that)npm run test:e2epasses (run before merging UI changes or cutting a release)var(--accent),var(--text-primary), etc.)text-[Npx]pixel font classes — rem equivalents only (text-[0.714rem],text-xs, etc.)handle()and returnIpcResult<T>schema.tselectron/db/queries.ts— single source of truth (imported by both Electron main process and MCP server); never construct aDatabaseinstance outsidedb/client.ts(Electron) ormcp-server.ts(MCP runtime)dependenciesordevDependenciesadded toROLE_MAPinscripts/generate-licenses.jsandlicenses.jsonregeneratedelectron/mcp/tools/index.tsdispatch +electron/lib/tool-schemas.tsZod schema--external:<pkg>flag in thecompilescript: updated the appropriate allowlist group (RUNTIME_PROVIDED/OPTIONAL_TRANSITIVE/SUBPROCESS_ONLY/SHIPPED_NATIVE) inelectron/bundle-guard.test.tsand (ifSHIPPED_NATIVE) shipped the package viaelectron-builder.ymlNotes for reviewer
reasoningTokens) remain tracked for metrics. See the guidance doc in PR discussion for the full rationale.chat:thought/pi-agent:thoughtIPC channels mirror the existingchat:token/pi-agent:tokenpattern — same payload shape, same listener cleanup pattern.max_tokenschanges — existing 4096 is sufficient for most reasoning models. Gemini may need ≥256; flagged in changelog.rehype-rawdependency was added in an earlier branch commit (not in this PR's diff) but is covered in the changelog for completeness.Summary by CodeRabbit