Skip to content

feat: deepchat sub agent#1412

Open
zerob13 wants to merge 3 commits intodevfrom
codex/deepchat-sub-agent
Open

feat: deepchat sub agent#1412
zerob13 wants to merge 3 commits intodevfrom
codex/deepchat-sub-agent

Conversation

@zerob13
Copy link
Copy Markdown
Collaborator

@zerob13 zerob13 commented Mar 30, 2026

Summary by CodeRabbit

Release Notes

New Features

  • Introduced Subagent Orchestrator tool for delegating parallel or sequential tasks to configured subagent slots
  • Added subagent slot configuration in agent settings (target, display name, description)
  • Real-time progress tracking and visual cards for subagent task execution
  • Parent-child session navigation with back-to-parent button and subagents panel
  • Session-level toggle to enable/disable subagent capabilities
  • Comprehensive multi-language support for subagent UI

Documentation

  • Added implementation specifications and task breakdown for subagent orchestrator system

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

Introduces Subagent Orchestrator V1 for DeepChat, enabling parent agents to delegate tasks to configured child subagent sessions. Adds comprehensive spec/plan documentation, session database schema extensions for parent-child relationships, a new subagent_orchestrator MCP tool supporting parallel/chain task execution modes, internal progress event bridging between parent and child sessions, and renderer UI for managing subagent configuration, viewing child session status, and navigating between parent/child contexts.

Changes

Cohort / File(s) Summary
Documentation
docs/specs/subagent-orchestrator/*
Adds spec, plan, and tasks documentation defining Subagent Orchestrator V1 end-to-end implementation, including data model, runtime flow, UI requirements, testing strategy, and phased rollout.
Shared Types & Constants
src/shared/types/agent-interface.d.ts, src/shared/types/presenters/*.d.ts, src/shared/chat.d.ts, src/shared/lib/deepchatSubagents.ts
Introduces SessionKind, DeepChatSubagentSlot, DeepChatSubagentMeta, AgentToolProgressUpdate types; extends session/config types with subagent fields; adds normalization utilities for subagent slots/config; updates tool presenter signatures to support onProgress callbacks and abort signals.
Database Schema
src/main/presenter/sqlitePresenter/tables/newSessions.ts, src/main/presenter/sqlitePresenter/tables/baseTable.ts
Adds subagent_enabled, session_kind, parent_session_id, subagent_meta_json columns to new_sessions table with schema versioning and migration; adds hasColumn helper to BaseTable.
Session Management
src/main/presenter/newAgentPresenter/sessionManager.ts, src/main/presenter/newAgentPresenter/index.ts
Extends NewSessionManager to handle subagent session lifecycle: creates regular/subagent sessions with parent-child linkage, persists subagent metadata, supports filtering by subagent inclusion/parent ID, manages cascade deletion; adds createSubagentSession, setSessionSubagentEnabled, deleteSessionInternal methods.
Subagent Tool Implementation
src/main/presenter/toolPresenter/agentTools/subagentOrchestratorTool.ts, src/main/presenter/toolPresenter/agentTools/agentToolManager.ts, src/main/presenter/toolPresenter/index.ts, src/main/presenter/toolPresenter/runtimePorts.ts
Implements SubagentOrchestratorTool for parallel/chain task delegation with slot validation, child session creation, handoff messaging, progress monitoring, and cancellation support; updates tool manager to route subagent_orchestrator calls; extends AgentToolRuntimePort with subagent session creation, messaging, and update subscription; adds tool presenter options for progress callbacks and signals.
DeepChat Agent Presenter Core
src/main/presenter/deepchatAgentPresenter/internalSessionEvents.ts, src/main/presenter/deepchatAgentPresenter/dispatch.ts, src/main/presenter/deepchatAgentPresenter/index.ts
Introduces internal session event emission infrastructure; adds updateSubagentToolCallBlock, extractSubagentToolState for progress/final result handling; integrates progress callbacks into executeTools and executeDeferredToolCall; extends status/message refresh to emit internal events; provides markdown builders for response/preview extraction.
Agent Repository & Configuration
src/main/presenter/agentRepository/index.ts, src/main/presenter/configPresenter/index.ts, src/main/presenter/index.ts
Normalizes DeepChat subagent config via normalizeDeepChatSubagentConfig during config resolution; extends resolveConversationSessionInfo with session/agent metadata including sessionKind, parentSessionId, subagentEnabled, availableSubagentSlots; adds runtime methods for subagent session creation, messaging, and subscription.
Renderer Settings
src/renderer/settings/components/DeepChatAgentsSettings.vue
Adds "Subagents" settings section with enable toggle, per-slot configuration UI (target type/agent selection, display name, description), slot management (add/delete/limit enforcement), and target agent filtering logic.
Renderer Chat UI Components
src/renderer/src/components/chat-input/McpIndicator.vue, src/renderer/src/components/chat/ChatStatusBar.vue, src/renderer/src/components/chat/ChatTopBar.vue, src/renderer/src/components/sidepanel/WorkspacePanel.vue
Refactors McpIndicator to model tools/subagents as items in groups; adds subagent toggle props/events to ChatStatusBar with persistent state sync; adds "Back to parent" navigation to ChatTopBar; adds "Subagents" section to WorkspacePanel showing child sessions with status and navigation.
Renderer Message Rendering
src/renderer/src/components/message/MessageBlockToolCall.vue, src/renderer/src/components/chat/messageListItems.ts
Extends DisplayAssistantMessageExtra with subagentProgress/subagentFinal fields; adds subagent-orchestrator-specific rendering for task list display, progress parsing, auto-expand behavior, and child session navigation.
Renderer Store & Page Logic
src/renderer/src/stores/ui/draft.ts, src/renderer/src/stores/ui/session.ts, src/renderer/src/stores/ui/sidepanel.ts, src/renderer/src/pages/NewThreadPage.vue, src/renderer/src/pages/ChatPage.vue
Adds subagentEnabled draft state; extends UISession with session kind/parent/subagent metadata and setSessionSubagentEnabled action; enables subagents sidepanel section by default; passes subagentEnabled to session creation; extracts subagent waiting interactions from progress payloads for permission routing; adds parentSessionId to pending interaction view.
Internationalization
src/renderer/src/i18n/*/chat.json, src/renderer/src/i18n/*/common.json, src/renderer/src/i18n/*/settings.json
Adds localized strings for subagent UI across 15 languages: tool call summary/mode/status labels, "Back to parent" navigation, workspace subagents section, settings configuration UI (enable toggle, target type, display name, slot limit, add action), and common "open" action.
Tests
test/main/presenter/.../*.test.ts, test/renderer/components/.../*.test.ts
Adds/updates tests for session creation with subagent metadata, schema migration, SubagentOrchestratorTool handoff message construction, AgentToolManager slot resolution, ChatStatusBar/McpIndicator/MessageBlockToolCall subagent UI behavior, DeepChatAgentsSettings slot configuration, and sidebar opening with sessionId.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • fix(agent): guard large tool outputs #1333 — Both modify deepchatAgentPresenter/dispatch.ts executeTools flow to inject new handlers (ToolOutputGuard vs subagent progress); changes to same code paths may conflict.
  • feat: support rtk in tool call #1356 — Both modify tool_call block handling and dispatch.ts tool execution logic; overlapping changes to presenter internals.
  • refactor: full agent loop mode #1208 — Both extend ToolPresenter/IToolPresenter interface with callTool options; main PR adds progress callbacks/signals to existing API while retrieved PR introduces larger unified tool routing refactor.

Suggested labels

codex, feature, scope:main, scope:renderer


🐰 Hops with delight at orchestrated harmony—
Five slots of wisdom dance in sync,
Parent guides child agents in chain and parallel rhythm,
Progress flows home like whispered echoes,
Subagents bloom! 🌱

✨ 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 codex/deepchat-sub-agent

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (10)
test/renderer/components/ChatSidePanel.test.ts (1)

79-81: Strengthen session-scoping coverage in this test.

Line 79 validates the matching session payload, but Line 81 only checks total call count.
Consider adding a mismatch case to ensure a different sessionId does not open the browser panel.

🧪 Suggested test hardening
     handler?.({}, { windowId: 7, sessionId: 'session-1' })
     expect(sidepanelStore.openBrowser).toHaveBeenCalledTimes(1)
+
+    handler?.({}, { windowId: 7, sessionId: 'session-2' })
+    expect(sidepanelStore.openBrowser).toHaveBeenCalledTimes(1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/renderer/components/ChatSidePanel.test.ts` around lines 79 - 81, Add a
negative test case that calls handler with a non-matching sessionId and assert
it does not trigger sidepanelStore.openBrowser again: after the existing call
handler?.({}, { windowId: 7, sessionId: 'session-1' }) and
expect(sidepanelStore.openBrowser).toHaveBeenCalledTimes(1), invoke
handler?.({}, { windowId: 7, sessionId: 'other-session' }) and assert
expect(sidepanelStore.openBrowser).toHaveBeenCalledTimes(1) (or assert not
called with the mismatched payload); use the existing handler and
sidepanelStore.openBrowser mocks to validate session-scoping behavior.
test/main/presenter/newAgentPresenter/sessionManager.test.ts (1)

79-82: Add one non-default mapping test for new subagent fields.

Current assertions validate defaults well; add a case where DB fields are explicitly populated to protect mapping logic from regressions.

🧪 Suggested test addition
+    it('maps explicit subagent fields from DB record', () => {
+      sqlitePresenter.newSessionsTable.get.mockReturnValue({
+        id: 's2',
+        agent_id: 'deepchat',
+        title: 'Child',
+        project_dir: '/tmp/proj',
+        is_pinned: 0,
+        is_draft: 0,
+        session_kind: 'subagent',
+        parent_session_id: 'p1',
+        subagent_enabled: 1,
+        subagent_meta_json: '{"slotId":"slot-1"}',
+        created_at: 1,
+        updated_at: 2
+      })
+
+      const record = manager.get('s2')
+      expect(record?.sessionKind).toBe('subagent')
+      expect(record?.parentSessionId).toBe('p1')
+      expect(record?.subagentEnabled).toBe(true)
+      expect(record?.subagentMeta).toEqual({ slotId: 'slot-1' })
+    })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/newAgentPresenter/sessionManager.test.ts` around lines 79
- 82, Add a new unit test in sessionManager.test.ts that covers a non-default
mapping of the new subagent fields: construct a DB session object with
sessionKind: 'regular', parentSessionId set to a non-null value,
subagentEnabled: true, and subagentMeta populated with a sample object, then
call the same mapping function used in the existing tests (the presenter/session
mapping helper used in this file) and assert the mapped result exposes the
non-default values for parentSessionId, subagentEnabled, and subagentMeta; name
the test something like "maps non-default subagent fields" and place it
alongside the other mapping tests to guard against regressions.
src/main/presenter/sqlitePresenter/tables/newSessions.ts (1)

67-73: Add an index for parent-child session lookups.

The new parent_session_id filter will otherwise scan and sort the full new_sessions table whenever the subagents view refreshes. An index on parent_session_id — ideally paired with updated_at — will keep this query predictable as session history grows. You'll want the same CREATE INDEX in the migration path too, not just fresh-table creation.

📈 Suggested index
       CREATE TABLE IF NOT EXISTS new_sessions (
         ${columns.join(',\n        ')}
       );
       CREATE INDEX IF NOT EXISTS idx_new_sessions_agent ON new_sessions(agent_id);
+      CREATE INDEX IF NOT EXISTS idx_new_sessions_parent_updated
+        ON new_sessions(parent_session_id, updated_at DESC);
       CREATE INDEX IF NOT EXISTS idx_new_sessions_updated ON new_sessions(updated_at DESC);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/sqlitePresenter/tables/newSessions.ts` around lines 67 -
73, Add a composite index on parent_session_id (and include updated_at for
ordering) to the new_sessions schema: update the CREATE TABLE/INDEX block that
returns the SQL in newSessions (the template that currently creates
idx_new_sessions_agent and idx_new_sessions_updated) to also CREATE INDEX IF NOT
EXISTS idx_new_sessions_parent_updated ON new_sessions(parent_session_id,
updated_at DESC); and mirror that same CREATE INDEX statement in the migration
path that applies schema changes so existing databases get the new index as
well.
src/renderer/src/pages/NewThreadPage.vue (1)

365-377: Normalize both resolveDeepChatAgentConfig() branches.

Only the fallback branch currently passes through normalizeDeepChatSubagentConfig(). Keeping the presenter branch raw makes this page depend on another layer already having expanded the new subagent fields, which is easy for mocks and alternate implementations to miss.

♻️ Keep a single config invariant here
 const resolveDeepChatAgentConfig = async (agentId: string): Promise<DeepChatAgentConfig> => {
   if (typeof configPresenter.resolveDeepChatAgentConfig === 'function') {
-    return configPresenter.resolveDeepChatAgentConfig(agentId)
+    return normalizeDeepChatSubagentConfig(
+      await configPresenter.resolveDeepChatAgentConfig(agentId)
+    )
   }

   const systemPrompt = await configPresenter.getSetting?.('default_system_prompt')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/pages/NewThreadPage.vue` around lines 365 - 377, The two
code paths in resolveDeepChatAgentConfig must return the same normalized shape:
when configPresenter.resolveDeepChatAgentConfig exists, await its result and
pass it through normalizeDeepChatSubagentConfig before returning (instead of
returning the raw presenter value), so both branches produce a
DeepChatAgentConfig; keep the fallback branch as-is (using getSetting) and
ensure you preserve typing/async behavior and any default fields when calling
normalizeDeepChatSubagentConfig.
src/renderer/settings/components/DeepChatAgentsSettings.vue (1)

440-452: Consider using a consistent Select component instead of native <select>.

The subagent target dropdown uses a native <select> element while other dropdowns in this component use shadcn's DropdownMenu or Popover with Select. This creates visual inconsistency in the UI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/settings/components/DeepChatAgentsSettings.vue` around lines 440
- 452, The native <select> for subagent targets should be replaced with the
project's consistent Select component (used elsewhere) to match visual style;
locate the block rendering getSubagentTargetValue(slot),
handleSubagentTargetChange(slot, $event) and iterating subagentTargetOptions,
and render those options via the Select/SelectTrigger/SelectContent pattern used
in this file (keeping the same value binding from getSubagentTargetValue and
invoking handleSubagentTargetChange with the selected value), ensure keys use
agentOption.value and labels use agentOption.label so behavior and data flow
remain identical.
src/renderer/src/components/sidepanel/WorkspacePanel.vue (1)

100-125: The highlight condition child.id === props.sessionId is always false.

In childSessions, all sessions are filtered to have parentSessionId === props.sessionId. Therefore, no child session's id can equal props.sessionId, making the highlight conditional always evaluate to the "not selected" branch. This is likely copy-paste from similar patterns.

Consider removing the dead highlight logic or replacing it with actual selection tracking if child selection highlighting is intended.

♻️ Simplify by removing unused highlight logic
             <button
               v-for="child in childSessions"
               :key="child.id"
               class="flex w-full items-start gap-2 px-3 py-2 text-left text-xs transition-colors"
-              :class="
-                child.id === props.sessionId
-                  ? 'bg-accent text-accent-foreground'
-                  : 'text-muted-foreground hover:bg-accent/60 hover:text-foreground'
-              "
+              class="text-muted-foreground hover:bg-accent/60 hover:text-foreground"
               type="button"
               `@click`="handleSubagentOpen(child.id)"
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/components/sidepanel/WorkspacePanel.vue` around lines 100 -
125, The highlight conditional child.id === props.sessionId is dead because
childSessions are filtered by parentSessionId === props.sessionId; remove or
replace it: either drop the ternary and always apply the "not selected" class,
or implement proper child selection tracking (introduce a selectedChildId
ref/prop or use existing selected state) and change the condition to child.id
=== selectedChildId; update the click handler (handleSubagentOpen) to set
selectedChildId if you choose selection tracking so the button highlights the
clicked child correctly.
src/renderer/src/components/message/MessageBlockToolCall.vue (1)

351-362: Consider adding basic property validation after JSON parsing.

The parseSubagentProgress function casts the parsed JSON directly to SubagentProgressPayload with only an Array.isArray(parsed?.tasks) check. If the JSON structure doesn't fully match the expected type (e.g., mode is missing or invalid), downstream code accessing progress.mode could produce unexpected results.

♻️ Optional: Add minimal schema validation
 const parseSubagentProgress = (value: unknown): SubagentProgressPayload | null => {
   if (typeof value !== 'string' || !value.trim()) {
     return null
   }

   try {
     const parsed = JSON.parse(value) as SubagentProgressPayload
-    return Array.isArray(parsed?.tasks) ? parsed : null
+    if (!Array.isArray(parsed?.tasks)) {
+      return null
+    }
+    if (parsed.mode !== 'parallel' && parsed.mode !== 'chain') {
+      return null
+    }
+    return parsed
   } catch {
     return null
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/components/message/MessageBlockToolCall.vue` around lines
351 - 362, The parseSubagentProgress function currently only checks that
parsed?.tasks is an array; update it to perform minimal schema validation after
JSON.parse: ensure parsed is an object, parsed.mode exists and is a string (and
optionally one of the allowed mode values used elsewhere), parsed.tasks is an
array, and each task item has the expected basic properties (e.g., id and status
as strings or the minimal types your code expects). If any of these checks fail
return null; keep the existing try/catch around JSON.parse and only cast/return
the parsed object when all validations pass. Use the function name
parseSubagentProgress and the SubagentProgressPayload shape to guide which
fields to validate.
docs/specs/subagent-orchestrator/spec.md (1)

78-83: Add an index for parent_session_id in the migration plan.

The workspace list, restart recovery, and parent→child lookup paths will all query by this field. Without an index, those flows degrade to full scans as new_sessions grows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/specs/subagent-orchestrator/spec.md` around lines 78 - 83, Add a DB
index on parent_session_id to avoid full-table scans: in the migration that
alters the new_sessions table (where you added subagent_enabled, session_kind,
parent_session_id, subagent_meta_json) add an index creation for
parent_session_id (e.g. CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_new_sessions_parent_session_id ON new_sessions(parent_session_id)) in the up
path and drop it in the down path; ensure the migration uses CONCURRENTLY if
supported and update any schema/verification tests or CI migration checks to
expect the new index.
src/main/presenter/toolPresenter/agentTools/subagentOrchestratorTool.ts (1)

70-80: Deferred promise pattern works but could use standard approach.

The pattern is functional, but the empty initial resolve function assignment could be confusing. Consider using TypeScript's non-null assertion or a well-known deferred utility.

Alternative using non-null assertion
 const createDeferred = (): MutableTaskState['completion'] => {
-  let resolve = () => {}
+  let resolve!: () => void
   const promise = new Promise<void>((innerResolve) => {
     resolve = innerResolve
   })

   return {
     promise,
     resolve
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/toolPresenter/agentTools/subagentOrchestratorTool.ts`
around lines 70 - 80, The createDeferred function currently initializes resolve
to a no-op which is confusing; change it to use TypeScript's non-null assertion
so resolve is declared but assigned inside the Promise: replace "let resolve =
() => {}" with "let resolve!: () => void" (keeping the Promise initializer that
assigns innerResolve to resolve) so MutableTaskState['completion'] returns a
properly-typed promise and resolve without a placeholder no-op; alternatively
import/use a shared deferred utility and return its completion shape to avoid
manual boilerplate.
src/main/presenter/newAgentPresenter/index.ts (1)

421-427: Unsafe cast after buildSessionWithState.

buildSessionWithState can throw if resolveAgentImplementation fails (e.g., agent not found). While the session record check is present, the subsequent call isn't wrapped in try-catch, so any error propagates. Consider using tryBuildSessionWithState with a null guard or keeping the current pattern but documenting that errors propagate intentionally.

Optional: Use try-catch for consistent error messaging
     const record = this.sessionManager.get(sessionId)
     if (!record) {
       throw new Error(`Subagent session not found after creation: ${sessionId}`)
     }

-    return (await this.buildSessionWithState(record)) as SessionWithState
+    try {
+      return await this.buildSessionWithState(record)
+    } catch (error) {
+      throw new Error(`Failed to build subagent session state: ${sessionId}`)
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/newAgentPresenter/index.ts` around lines 421 - 427, The
code unsafely casts the result of buildSessionWithState to SessionWithState
without handling errors that buildSessionWithState may throw (e.g., from
resolveAgentImplementation); wrap the call in a try-catch and rethrow or return
a clearer error, or call tryBuildSessionWithState and guard the null result
before casting, while keeping the existing sessionManager.get check;
specifically update the block that uses sessionManager.get(sessionId) so it
calls tryBuildSessionWithState(record) and checks for null (or wrap await
this.buildSessionWithState(record) in try-catch) and produce a consistent error
message referencing sessionId and the underlying failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/presenter/deepchatAgentPresenter/dispatch.ts`:
- Around line 784-795: The current guard only updates the UI when
extractSubagentToolState returns subagentProgress, so a final-only payload
(subagentFinal) is dropped; change the conditional in dispatch.ts to run
updateSubagentToolCallBlock whenever either subagentState.subagentProgress or
subagentState.subagentFinal is present (e.g., if (subagentState.subagentProgress
|| subagentState.subagentFinal)), keep the same content conversion logic using
toolResponseToText for non-string content, and pass both
subagentState.subagentProgress and subagentState.subagentFinal to
updateSubagentToolCallBlock so final-only updates are persisted.

In `@src/main/presenter/deepchatAgentPresenter/index.ts`:
- Around line 2941-2959: The call to this.toolPresenter.callTool is using
this.getAbortSignalForSession(sessionId) which is often undefined because the
paused-generation controller was cleared; create and register a dedicated
AbortController for this deferred subagent tool run before invoking callTool
(keyed by sessionId and toolCall.id), pass its signal into callTool, and ensure
you unregister/abort that controller in the finally/cleanup path so
cancelGeneration(sessionId) (and cancel logic) can find and cancel this running
subagent_orchestrator; updateSubagentToolCallProgress remains unchanged but use
toolCall.id when registering/removing the controller so cancelGeneration can
reference it.

In `@src/main/presenter/deepchatAgentPresenter/internalSessionEvents.ts`:
- Around line 72-106: extractWaitingInteraction currently scans blocks backwards
and returns the last pending action, but respondToolInteraction expects the
earliest unresolved action for a message; change the scan order to iterate from
the start (index 0 to blocks.length-1) so the function returns the earliest
pending interaction. Keep the same matching conditions (block.type !== 'action',
block.status !== 'pending', block.extra?.needsUserAction !== true,
!block.tool_call?.id) and return shapes (type 'permission' or 'question',
messageId, toolCallId, actionBlock as JSON clone) when the first matching block
is found so the resume path and respondToolInteraction receive the same pending
entry.
- Around line 51-61: The current toDisplayLines used by
buildAssistantResponseMarkdown trims each line and removes blank lines, which
destroys leading indentation and empty-line formatting (breaking fenced code,
nested lists, YAML) when mirroring child responses; change toDisplayLines to
only split on CRLF/newline and return lines as-is (do not .trim() and do not
.filter(Boolean)) so extractBlockText -> toDisplayLines ->
buildAssistantResponseMarkdown preserves original whitespace and blank lines.
- Around line 109-117: The emitDeepChatInternalSessionUpdate function currently
calls emitter.emit('update', update) synchronously which lets exceptions thrown
by subscribers bubble up; modify emitDeepChatInternalSessionUpdate to wrap the
emitter.emit call in a try-catch so subscriber failures are isolated, catch any
thrown error and log it (use the existing logger if available or console.error)
and swallow or handle the error so it does not propagate to callers like
setSessionStatus or emitMessageRefresh; keep
subscribeDeepChatInternalSessionUpdates unchanged and reference emitter.emit and
emitDeepChatInternalSessionUpdate when making the change.

In `@src/main/presenter/newAgentPresenter/index.ts`:
- Around line 1229-1251: The method setSessionSubagentEnabled currently
force-casts the result of tryBuildSessionWithState to SessionWithState even
though tryBuildSessionWithState can return null; change this by removing the
unsafe cast, capturing the result into a variable, checking if it's null, and
throwing a descriptive error (e.g., "Failed to build session state for
sessionId") before returning; ensure this check happens after
sessionManager.update and emitSessionListUpdated so callers never receive a null
value from setSessionSubagentEnabled.

In `@src/main/presenter/sqlitePresenter/tables/newSessions.ts`:
- Around line 209-215: The current logic adds "session_kind = 'regular'"
whenever filters.includeSubagents !== true, which unintentionally excludes child
sessions when filters.parentSessionId is provided; change the condition so that
session_kind='regular' is only added when includeSubagents is explicitly false
(or when parentSessionId is not provided). Concretely, update the check around
conditions.push("session_kind = 'regular'") to skip adding that condition if
filters.parentSessionId is set (or use filters.includeSubagents === false
instead of !== true), so that queries with filters.parentSessionId return child
sessions as expected; adjust any affected params handling accordingly
(references: filters.includeSubagents, filters.parentSessionId,
conditions.push("session_kind = 'regular'")).

In `@src/renderer/src/components/chat/ChatStatusBar.vue`:
- Around line 982-992: The computed showSubagentToggle currently allows the
toggle for any non-ACP active session with sessionKind === 'regular'; update the
active-session branch to also require the active session's agentType to be
'deepchat' so the toggle is only shown for DeepChat sessions. Specifically, in
the computed showing logic (showSubagentToggle) change the hasActiveSession
branch to return true only when sessionStore.activeSession?.sessionKind ===
'regular' AND sessionStore.activeSession?.agentType === 'deepchat' (keep
existing isAcpAgent and selectedAgentType checks and use null-safe access).

In `@src/renderer/src/i18n/ru-RU/chat.json`:
- Line 172: Translate the English value for the key workspace.sections.artifacts
in the ru-RU bundle to Russian (replace "Artifacts" with an appropriate Russian
translation like "Артефакты") so the ru-RU chat.json no longer contains
mixed-language UI; update the value for the key "artifacts" in the existing
object entry to the Russian string.

In `@src/renderer/src/i18n/zh-HK/settings.json`:
- Line 1630: The value for the i18n key subagentsTitle is still in English;
update the value for "subagentsTitle" to a Traditional Chinese translation
(e.g., "子代理") so it matches the surrounding zh-HK strings, keeping the key name
unchanged and preserving JSON formatting in settings.json.

In
`@test/main/presenter/toolPresenter/agentTools/subagentOrchestratorTool.test.ts`:
- Around line 43-55: The test's workdir source is ambiguous because
resolveConversationWorkdir, parentSession.projectDir, and
childSession.projectDir currently all equal the same path, so the assertion
doesn't confirm the tool actually uses the resolver; update the fixture so these
three values differ (e.g., give parentSession.projectDir and
childSession.projectDir distinct paths and have
resolveConversationWorkdir.mockResolvedValue return the expected handoff path)
and assert that SubagentOrchestratorTool uses resolveConversationWorkdir (the
mocked function) rather than copying either session.projectDir; change the
buildSessionInfo calls for parentSession and childSession to set unique
projectDir values and adjust the resolveConversationWorkdir mock return
accordingly.

---

Nitpick comments:
In `@docs/specs/subagent-orchestrator/spec.md`:
- Around line 78-83: Add a DB index on parent_session_id to avoid full-table
scans: in the migration that alters the new_sessions table (where you added
subagent_enabled, session_kind, parent_session_id, subagent_meta_json) add an
index creation for parent_session_id (e.g. CREATE INDEX CONCURRENTLY IF NOT
EXISTS idx_new_sessions_parent_session_id ON new_sessions(parent_session_id)) in
the up path and drop it in the down path; ensure the migration uses CONCURRENTLY
if supported and update any schema/verification tests or CI migration checks to
expect the new index.

In `@src/main/presenter/newAgentPresenter/index.ts`:
- Around line 421-427: The code unsafely casts the result of
buildSessionWithState to SessionWithState without handling errors that
buildSessionWithState may throw (e.g., from resolveAgentImplementation); wrap
the call in a try-catch and rethrow or return a clearer error, or call
tryBuildSessionWithState and guard the null result before casting, while keeping
the existing sessionManager.get check; specifically update the block that uses
sessionManager.get(sessionId) so it calls tryBuildSessionWithState(record) and
checks for null (or wrap await this.buildSessionWithState(record) in try-catch)
and produce a consistent error message referencing sessionId and the underlying
failure.

In `@src/main/presenter/sqlitePresenter/tables/newSessions.ts`:
- Around line 67-73: Add a composite index on parent_session_id (and include
updated_at for ordering) to the new_sessions schema: update the CREATE
TABLE/INDEX block that returns the SQL in newSessions (the template that
currently creates idx_new_sessions_agent and idx_new_sessions_updated) to also
CREATE INDEX IF NOT EXISTS idx_new_sessions_parent_updated ON
new_sessions(parent_session_id, updated_at DESC); and mirror that same CREATE
INDEX statement in the migration path that applies schema changes so existing
databases get the new index as well.

In `@src/main/presenter/toolPresenter/agentTools/subagentOrchestratorTool.ts`:
- Around line 70-80: The createDeferred function currently initializes resolve
to a no-op which is confusing; change it to use TypeScript's non-null assertion
so resolve is declared but assigned inside the Promise: replace "let resolve =
() => {}" with "let resolve!: () => void" (keeping the Promise initializer that
assigns innerResolve to resolve) so MutableTaskState['completion'] returns a
properly-typed promise and resolve without a placeholder no-op; alternatively
import/use a shared deferred utility and return its completion shape to avoid
manual boilerplate.

In `@src/renderer/settings/components/DeepChatAgentsSettings.vue`:
- Around line 440-452: The native <select> for subagent targets should be
replaced with the project's consistent Select component (used elsewhere) to
match visual style; locate the block rendering getSubagentTargetValue(slot),
handleSubagentTargetChange(slot, $event) and iterating subagentTargetOptions,
and render those options via the Select/SelectTrigger/SelectContent pattern used
in this file (keeping the same value binding from getSubagentTargetValue and
invoking handleSubagentTargetChange with the selected value), ensure keys use
agentOption.value and labels use agentOption.label so behavior and data flow
remain identical.

In `@src/renderer/src/components/message/MessageBlockToolCall.vue`:
- Around line 351-362: The parseSubagentProgress function currently only checks
that parsed?.tasks is an array; update it to perform minimal schema validation
after JSON.parse: ensure parsed is an object, parsed.mode exists and is a string
(and optionally one of the allowed mode values used elsewhere), parsed.tasks is
an array, and each task item has the expected basic properties (e.g., id and
status as strings or the minimal types your code expects). If any of these
checks fail return null; keep the existing try/catch around JSON.parse and only
cast/return the parsed object when all validations pass. Use the function name
parseSubagentProgress and the SubagentProgressPayload shape to guide which
fields to validate.

In `@src/renderer/src/components/sidepanel/WorkspacePanel.vue`:
- Around line 100-125: The highlight conditional child.id === props.sessionId is
dead because childSessions are filtered by parentSessionId === props.sessionId;
remove or replace it: either drop the ternary and always apply the "not
selected" class, or implement proper child selection tracking (introduce a
selectedChildId ref/prop or use existing selected state) and change the
condition to child.id === selectedChildId; update the click handler
(handleSubagentOpen) to set selectedChildId if you choose selection tracking so
the button highlights the clicked child correctly.

In `@src/renderer/src/pages/NewThreadPage.vue`:
- Around line 365-377: The two code paths in resolveDeepChatAgentConfig must
return the same normalized shape: when
configPresenter.resolveDeepChatAgentConfig exists, await its result and pass it
through normalizeDeepChatSubagentConfig before returning (instead of returning
the raw presenter value), so both branches produce a DeepChatAgentConfig; keep
the fallback branch as-is (using getSetting) and ensure you preserve
typing/async behavior and any default fields when calling
normalizeDeepChatSubagentConfig.

In `@test/main/presenter/newAgentPresenter/sessionManager.test.ts`:
- Around line 79-82: Add a new unit test in sessionManager.test.ts that covers a
non-default mapping of the new subagent fields: construct a DB session object
with sessionKind: 'regular', parentSessionId set to a non-null value,
subagentEnabled: true, and subagentMeta populated with a sample object, then
call the same mapping function used in the existing tests (the presenter/session
mapping helper used in this file) and assert the mapped result exposes the
non-default values for parentSessionId, subagentEnabled, and subagentMeta; name
the test something like "maps non-default subagent fields" and place it
alongside the other mapping tests to guard against regressions.

In `@test/renderer/components/ChatSidePanel.test.ts`:
- Around line 79-81: Add a negative test case that calls handler with a
non-matching sessionId and assert it does not trigger sidepanelStore.openBrowser
again: after the existing call handler?.({}, { windowId: 7, sessionId:
'session-1' }) and expect(sidepanelStore.openBrowser).toHaveBeenCalledTimes(1),
invoke handler?.({}, { windowId: 7, sessionId: 'other-session' }) and assert
expect(sidepanelStore.openBrowser).toHaveBeenCalledTimes(1) (or assert not
called with the mismatched payload); use the existing handler and
sidepanelStore.openBrowser mocks to validate session-scoping behavior.
🪄 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

Run ID: ba710e02-f220-4cd4-a6b6-df9b1719ce30

📥 Commits

Reviewing files that changed from the base of the PR and between b3644b6 and bd3696a.

📒 Files selected for processing (82)
  • docs/specs/subagent-orchestrator/plan.md
  • docs/specs/subagent-orchestrator/spec.md
  • docs/specs/subagent-orchestrator/tasks.md
  • src/main/presenter/agentRepository/index.ts
  • src/main/presenter/configPresenter/index.ts
  • src/main/presenter/deepchatAgentPresenter/dispatch.ts
  • src/main/presenter/deepchatAgentPresenter/index.ts
  • src/main/presenter/deepchatAgentPresenter/internalSessionEvents.ts
  • src/main/presenter/index.ts
  • src/main/presenter/newAgentPresenter/index.ts
  • src/main/presenter/newAgentPresenter/sessionManager.ts
  • src/main/presenter/sqlitePresenter/tables/baseTable.ts
  • src/main/presenter/sqlitePresenter/tables/newSessions.ts
  • src/main/presenter/toolPresenter/agentTools/agentToolManager.ts
  • src/main/presenter/toolPresenter/agentTools/subagentOrchestratorTool.ts
  • src/main/presenter/toolPresenter/index.ts
  • src/main/presenter/toolPresenter/runtimePorts.ts
  • src/renderer/settings/components/DeepChatAgentsSettings.vue
  • src/renderer/src/components/chat-input/McpIndicator.vue
  • src/renderer/src/components/chat/ChatStatusBar.vue
  • src/renderer/src/components/chat/ChatTopBar.vue
  • src/renderer/src/components/chat/messageListItems.ts
  • src/renderer/src/components/message/MessageBlockToolCall.vue
  • src/renderer/src/components/sidepanel/WorkspacePanel.vue
  • src/renderer/src/i18n/da-DK/chat.json
  • src/renderer/src/i18n/da-DK/common.json
  • src/renderer/src/i18n/da-DK/settings.json
  • src/renderer/src/i18n/en-US/chat.json
  • src/renderer/src/i18n/en-US/common.json
  • src/renderer/src/i18n/en-US/settings.json
  • src/renderer/src/i18n/fa-IR/chat.json
  • src/renderer/src/i18n/fa-IR/common.json
  • src/renderer/src/i18n/fa-IR/settings.json
  • src/renderer/src/i18n/fr-FR/chat.json
  • src/renderer/src/i18n/fr-FR/common.json
  • src/renderer/src/i18n/fr-FR/settings.json
  • src/renderer/src/i18n/he-IL/chat.json
  • src/renderer/src/i18n/he-IL/common.json
  • src/renderer/src/i18n/he-IL/settings.json
  • src/renderer/src/i18n/ja-JP/chat.json
  • src/renderer/src/i18n/ja-JP/common.json
  • src/renderer/src/i18n/ja-JP/settings.json
  • src/renderer/src/i18n/ko-KR/chat.json
  • src/renderer/src/i18n/ko-KR/common.json
  • src/renderer/src/i18n/ko-KR/settings.json
  • src/renderer/src/i18n/pt-BR/chat.json
  • src/renderer/src/i18n/pt-BR/common.json
  • src/renderer/src/i18n/pt-BR/settings.json
  • src/renderer/src/i18n/ru-RU/chat.json
  • src/renderer/src/i18n/ru-RU/common.json
  • src/renderer/src/i18n/ru-RU/settings.json
  • src/renderer/src/i18n/zh-CN/chat.json
  • src/renderer/src/i18n/zh-CN/common.json
  • src/renderer/src/i18n/zh-CN/settings.json
  • src/renderer/src/i18n/zh-HK/chat.json
  • src/renderer/src/i18n/zh-HK/common.json
  • src/renderer/src/i18n/zh-HK/settings.json
  • src/renderer/src/i18n/zh-TW/chat.json
  • src/renderer/src/i18n/zh-TW/common.json
  • src/renderer/src/i18n/zh-TW/settings.json
  • src/renderer/src/pages/ChatPage.vue
  • src/renderer/src/pages/NewThreadPage.vue
  • src/renderer/src/stores/ui/draft.ts
  • src/renderer/src/stores/ui/session.ts
  • src/renderer/src/stores/ui/sidepanel.ts
  • src/shared/chat.d.ts
  • src/shared/lib/deepchatSubagents.ts
  • src/shared/types/agent-interface.d.ts
  • src/shared/types/presenters/legacy.presenters.d.ts
  • src/shared/types/presenters/new-agent.presenter.d.ts
  • src/shared/types/presenters/tool.presenter.d.ts
  • src/shared/types/presenters/workspace.d.ts
  • test/main/presenter/newAgentPresenter/newAgentPresenter.test.ts
  • test/main/presenter/newAgentPresenter/sessionManager.test.ts
  • test/main/presenter/sqlitePresenter.test.ts
  • test/main/presenter/toolPresenter/agentTools/agentToolManagerSettings.test.ts
  • test/main/presenter/toolPresenter/agentTools/subagentOrchestratorTool.test.ts
  • test/renderer/components/ChatSidePanel.test.ts
  • test/renderer/components/ChatStatusBar.test.ts
  • test/renderer/components/DeepChatAgentsSettings.test.ts
  • test/renderer/components/McpIndicator.test.ts
  • test/renderer/components/message/MessageBlockToolCall.test.ts

Comment on lines +784 to +795
const subagentState = extractSubagentToolState(toolRawData)
if (subagentState.subagentProgress) {
updateSubagentToolCallBlock(
state.blocks,
tc.id,
typeof toolRawData.content === 'string'
? toolRawData.content
: toolResponseToText(toolRawData.content),
subagentState.subagentProgress,
subagentState.subagentFinal
)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Persist subagentFinal even when the tool omits a progress snapshot.

The guard on Line 785 only runs when subagentProgress exists. If the tool returns a final-only payload, extra.subagentFinal is dropped and the renderer never sees the completed subagent state.

🩹 Suggested fix
-      if (subagentState.subagentProgress) {
+      if (subagentState.subagentProgress || subagentState.subagentFinal) {
         updateSubagentToolCallBlock(
           state.blocks,
           tc.id,
           typeof toolRawData.content === 'string'
             ? toolRawData.content
             : toolResponseToText(toolRawData.content),
-          subagentState.subagentProgress,
+          subagentState.subagentProgress ?? subagentState.subagentFinal ?? '',
           subagentState.subagentFinal
         )
       }
📝 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
const subagentState = extractSubagentToolState(toolRawData)
if (subagentState.subagentProgress) {
updateSubagentToolCallBlock(
state.blocks,
tc.id,
typeof toolRawData.content === 'string'
? toolRawData.content
: toolResponseToText(toolRawData.content),
subagentState.subagentProgress,
subagentState.subagentFinal
)
}
const subagentState = extractSubagentToolState(toolRawData)
if (subagentState.subagentProgress || subagentState.subagentFinal) {
updateSubagentToolCallBlock(
state.blocks,
tc.id,
typeof toolRawData.content === 'string'
? toolRawData.content
: toolResponseToText(toolRawData.content),
subagentState.subagentProgress ?? subagentState.subagentFinal ?? '',
subagentState.subagentFinal
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/deepchatAgentPresenter/dispatch.ts` around lines 784 -
795, The current guard only updates the UI when extractSubagentToolState returns
subagentProgress, so a final-only payload (subagentFinal) is dropped; change the
conditional in dispatch.ts to run updateSubagentToolCallBlock whenever either
subagentState.subagentProgress or subagentState.subagentFinal is present (e.g.,
if (subagentState.subagentProgress || subagentState.subagentFinal)), keep the
same content conversion logic using toolResponseToText for non-string content,
and pass both subagentState.subagentProgress and subagentState.subagentFinal to
updateSubagentToolCallBlock so final-only updates are persisted.

Comment on lines +2941 to +2959
const result = await this.toolPresenter.callTool(request, {
onProgress: (update) => {
if (
update.kind !== 'subagent_orchestrator' ||
update.toolCallId !== (toolCall.id || '')
) {
return
}

this.updateSubagentToolCallProgress(
sessionId,
messageId,
toolCall.id || '',
update.responseMarkdown,
update.progressJson
)
},
signal: this.getAbortSignalForSession(sessionId)
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Register an abort controller for deferred tool execution.

By the time Line 2958 runs, the paused generation controller has already been cleared, so getAbortSignalForSession() is usually undefined here. A long-running subagent_orchestrator started after permission approval therefore cannot be canceled via cancelGeneration(sessionId) until resume begins again.

💡 Suggested fix
-    try {
-      const result = await this.toolPresenter.callTool(request, {
+    const deferredAbortController = new AbortController()
+    this.abortControllers.set(sessionId, deferredAbortController)
+    try {
+      const result = await this.toolPresenter.callTool(request, {
         onProgress: (update) => {
           if (
             update.kind !== 'subagent_orchestrator' ||
             update.toolCallId !== (toolCall.id || '')
           ) {
             return
           }
 
           this.updateSubagentToolCallProgress(
             sessionId,
             messageId,
             toolCall.id || '',
             update.responseMarkdown,
             update.progressJson
           )
         },
-        signal: this.getAbortSignalForSession(sessionId)
+        signal: deferredAbortController.signal
       })
       const rawData = result.rawData as MCPToolResponse
       if (rawData.requiresPermission) {
         return {
           responseText: this.toolContentToText(rawData.content),
@@
     } catch (error) {
       const errorText = error instanceof Error ? error.message : String(error)
       return {
         responseText: `Error: ${errorText}`,
         isError: true
       }
+    } finally {
+      if (this.abortControllers.get(sessionId) === deferredAbortController) {
+        this.abortControllers.delete(sessionId)
+      }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/deepchatAgentPresenter/index.ts` around lines 2941 - 2959,
The call to this.toolPresenter.callTool is using
this.getAbortSignalForSession(sessionId) which is often undefined because the
paused-generation controller was cleared; create and register a dedicated
AbortController for this deferred subagent tool run before invoking callTool
(keyed by sessionId and toolCall.id), pass its signal into callTool, and ensure
you unregister/abort that controller in the finally/cleanup path so
cancelGeneration(sessionId) (and cancel logic) can find and cancel this running
subagent_orchestrator; updateSubagentToolCallProgress remains unchanged but use
toolCall.id when registering/removing the controller so cancelGeneration can
reference it.

Comment on lines +51 to +61
const toDisplayLines = (text: string): string[] =>
text
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)

export const buildAssistantResponseMarkdown = (blocks: AssistantMessageBlock[]): string =>
blocks
.flatMap((block) => extractBlockText(block))
.flatMap((text) => toDisplayLines(text))
.join('\n')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve markdown whitespace in responseMarkdown.

Line 54 trims leading indentation and Line 55 drops blank lines before the text is mirrored into the parent tool-call response. Child answers with fenced code, nested lists, or YAML will lose formatting in the parent card.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/deepchatAgentPresenter/internalSessionEvents.ts` around
lines 51 - 61, The current toDisplayLines used by buildAssistantResponseMarkdown
trims each line and removes blank lines, which destroys leading indentation and
empty-line formatting (breaking fenced code, nested lists, YAML) when mirroring
child responses; change toDisplayLines to only split on CRLF/newline and return
lines as-is (do not .trim() and do not .filter(Boolean)) so extractBlockText ->
toDisplayLines -> buildAssistantResponseMarkdown preserves original whitespace
and blank lines.

Comment on lines +72 to +106
export const extractWaitingInteraction = (
blocks: AssistantMessageBlock[],
messageId: string
): DeepChatInternalSessionWaitingInteraction | null => {
for (let index = blocks.length - 1; index >= 0; index -= 1) {
const block = blocks[index]
if (
block.type !== 'action' ||
block.status !== 'pending' ||
block.extra?.needsUserAction !== true ||
!block.tool_call?.id
) {
continue
}

if (block.action_type === 'tool_call_permission') {
return {
type: 'permission',
messageId,
toolCallId: block.tool_call.id,
actionBlock: JSON.parse(JSON.stringify(block)) as AssistantMessageBlock
}
}

if (block.action_type === 'question_request') {
return {
type: 'question',
messageId,
toolCallId: block.tool_call.id,
actionBlock: JSON.parse(JSON.stringify(block)) as AssistantMessageBlock
}
}
}

return null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Surface the same pending interaction that the resume path expects.

Line 76 walks backward, but respondToolInteraction() only accepts the earliest unresolved entry from the message. If a child message contains more than one pending action, the parent overlay can advertise a later interaction that the user still cannot answer yet.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/deepchatAgentPresenter/internalSessionEvents.ts` around
lines 72 - 106, extractWaitingInteraction currently scans blocks backwards and
returns the last pending action, but respondToolInteraction expects the earliest
unresolved action for a message; change the scan order to iterate from the start
(index 0 to blocks.length-1) so the function returns the earliest pending
interaction. Keep the same matching conditions (block.type !== 'action',
block.status !== 'pending', block.extra?.needsUserAction !== true,
!block.tool_call?.id) and return shapes (type 'permission' or 'question',
messageId, toolCallId, actionBlock as JSON clone) when the first matching block
is found so the resume path and respondToolInteraction receive the same pending
entry.

Comment on lines +109 to +117
export const emitDeepChatInternalSessionUpdate = (update: DeepChatInternalSessionUpdate): void => {
emitter.emit('update', update)
}

export const subscribeDeepChatInternalSessionUpdates = (
listener: (update: DeepChatInternalSessionUpdate) => void
): (() => void) => {
emitter.on('update', listener)
return () => emitter.off('update', listener)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does Node.js EventEmitter.emit() invoke listeners synchronously, and do exceptions thrown inside a listener propagate back to the caller of emit()?

💡 Result:

Yes.

  • EventEmitter.emit() invokes listeners synchronously (it “synchronously calls each of the listeners … in the order they were registered”). [1]
  • Exceptions thrown inside a listener do propagate back to the caller of emit() (Node’s emit() directly calls each listener—e.g., via ReflectApply(...)—without wrapping the call in a try/catch, so a thrown error will bubble out of emit() and can be caught by the emit() caller). [2]

Notes:

  • Emitting 'error' with no 'error' listener is a special case: emit('error', err) will throw (and typically crash the process if uncaught). [1]

Sources: [1] Node.js Events docs (EventEmitter.emit) • [2] Node.js core lib/events.js implementation


Isolate subscriber failures from the internal event bridge.

The emitter.emit() call at line 110 is synchronous and will propagate exceptions thrown by listeners directly back to the caller (emitDeepChatInternalSessionUpdate). A faulty subscriber could throw an exception, breaking chat processing in setSessionStatus() / emitMessageRefresh() and affecting unrelated sessions. Wrap the emit call in a try-catch block to prevent subscriber failures from cascading upstream.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/deepchatAgentPresenter/internalSessionEvents.ts` around
lines 109 - 117, The emitDeepChatInternalSessionUpdate function currently calls
emitter.emit('update', update) synchronously which lets exceptions thrown by
subscribers bubble up; modify emitDeepChatInternalSessionUpdate to wrap the
emitter.emit call in a try-catch so subscriber failures are isolated, catch any
thrown error and log it (use the existing logger if available or console.error)
and swallow or handle the error so it does not propagate to callers like
setSessionStatus or emitMessageRefresh; keep
subscribeDeepChatInternalSessionUpdates unchanged and reference emitter.emit and
emitDeepChatInternalSessionUpdate when making the change.

Comment on lines +209 to +215
if (filters?.includeSubagents !== true) {
conditions.push("session_kind = 'regular'")
}
if (filters?.parentSessionId !== undefined) {
conditions.push('parent_session_id = ?')
params.push(filters.parentSessionId)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

parentSessionId currently filters out the very rows it's supposed to find.

When callers pass parentSessionId without also remembering includeSubagents: true, Lines 209-210 still add session_kind = 'regular', so child-session lookups return []. That makes the new filter very easy to misuse and breaks the obvious list({ parentSessionId }) call shape.

🐛 Treat parent-scoped queries as subagent queries
-    if (filters?.includeSubagents !== true) {
-      conditions.push("session_kind = 'regular'")
-    }
     if (filters?.parentSessionId !== undefined) {
+      conditions.push("session_kind = 'subagent'")
       conditions.push('parent_session_id = ?')
       params.push(filters.parentSessionId)
+    } else if (filters?.includeSubagents !== true) {
+      conditions.push("session_kind = 'regular'")
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/sqlitePresenter/tables/newSessions.ts` around lines 209 -
215, The current logic adds "session_kind = 'regular'" whenever
filters.includeSubagents !== true, which unintentionally excludes child sessions
when filters.parentSessionId is provided; change the condition so that
session_kind='regular' is only added when includeSubagents is explicitly false
(or when parentSessionId is not provided). Concretely, update the check around
conditions.push("session_kind = 'regular'") to skip adding that condition if
filters.parentSessionId is set (or use filters.includeSubagents === false
instead of !== true), so that queries with filters.parentSessionId return child
sessions as expected; adjust any affected params handling accordingly
(references: filters.includeSubagents, filters.parentSessionId,
conditions.push("session_kind = 'regular'")).

Comment on lines +982 to +992
const showSubagentToggle = computed(() => {
if (isAcpAgent.value) {
return false
}

if (hasActiveSession.value) {
return sessionStore.activeSession?.sessionKind === 'regular'
}

return selectedAgentType.value === 'deepchat'
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Gate the subagent toggle on the active session's agent type.

For active sessions, this currently shows the toggle for any non-ACP regular session. The feature is supposed to be DeepChat-only, so an ACP/custom regular session can expose a control that writes subagentEnabled on an unsupported session.

🩹 Suggested fix
+const activeSessionAgentType = computed(() =>
+  inferAgentType(sessionStore.activeSession?.agentId)
+)
+
 const showSubagentToggle = computed(() => {
   if (isAcpAgent.value) {
     return false
   }

   if (hasActiveSession.value) {
-    return sessionStore.activeSession?.sessionKind === 'regular'
+    return (
+      activeSessionAgentType.value === 'deepchat' &&
+      sessionStore.activeSession?.sessionKind === 'regular'
+    )
   }

   return selectedAgentType.value === 'deepchat'
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/components/chat/ChatStatusBar.vue` around lines 982 - 992,
The computed showSubagentToggle currently allows the toggle for any non-ACP
active session with sessionKind === 'regular'; update the active-session branch
to also require the active session's agentType to be 'deepchat' so the toggle is
only shown for DeepChat sessions. Specifically, in the computed showing logic
(showSubagentToggle) change the hasActiveSession branch to return true only when
sessionStore.activeSession?.sessionKind === 'regular' AND
sessionStore.activeSession?.agentType === 'deepchat' (keep existing isAcpAgent
and selectedAgentType checks and use null-safe access).

"files": "Файлы",
"git": "Git",
"artifacts": "Artifacts"
"artifacts": "Artifacts",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Localize workspace.sections.artifacts in the Russian bundle.

Line 172 is still English, so the new workspace section renders mixed-language UI in ru-RU.

🌐 Suggested fix
-      "artifacts": "Artifacts",
+      "artifacts": "Артефакты",
📝 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
"artifacts": "Artifacts",
"artifacts": "Артефакты",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/i18n/ru-RU/chat.json` at line 172, Translate the English
value for the key workspace.sections.artifacts in the ru-RU bundle to Russian
(replace "Artifacts" with an appropriate Russian translation like "Артефакты")
so the ru-RU chat.json no longer contains mixed-language UI; update the value
for the key "artifacts" in the existing object entry to the Russian string.

"compactionRetainPairs": "保留最近訊息對",
"deleteConfirm": "確定要刪除智能體「{name}」嗎?它的歷史會話會自動回退到內建 DeepChat 智能體。"
"deleteConfirm": "確定要刪除智能體「{name}」嗎?它的歷史會話會自動回退到內建 DeepChat 智能體。",
"subagentsTitle": "subagent",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Localize subagentsTitle for zh-HK consistency.

"subagentsTitle": "subagent" is not localized while adjacent labels are Traditional Chinese. This will look inconsistent in the settings UI.

💡 Suggested change
-    "subagentsTitle": "subagent",
+    "subagentsTitle": "子智能體",
📝 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
"subagentsTitle": "subagent",
"subagentsTitle": "子智能體",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/i18n/zh-HK/settings.json` at line 1630, The value for the
i18n key subagentsTitle is still in English; update the value for
"subagentsTitle" to a Traditional Chinese translation (e.g., "子代理") so it
matches the surrounding zh-HK strings, keeping the key name unchanged and
preserving JSON formatting in settings.json.

Comment on lines +43 to +55
const parentSession = buildSessionInfo()
const childSession = buildSessionInfo({
sessionId: 'child-session',
agentName: 'Reviewer Clone',
sessionKind: 'subagent',
parentSessionId: parentSession.sessionId,
subagentEnabled: false,
availableSubagentSlots: []
})

const tool = new SubagentOrchestratorTool({
resolveConversationWorkdir: vi.fn().mockResolvedValue(parentSession.projectDir),
resolveConversationSessionInfo: vi
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make the workdir source unambiguous in this test.

resolveConversationWorkdir, parentSession.projectDir, and childSession.projectDir all resolve to the same /workspace/parent-app value right now. This still passes if the tool skips the resolver and copies either session field instead, so the test doesn't actually pin the handoff behavior it describes.

🧪 Tighten the fixture
-    const parentSession = buildSessionInfo()
+    const parentSession = buildSessionInfo({
+      projectDir: '/workspace/parent-session'
+    })
     const childSession = buildSessionInfo({
       sessionId: 'child-session',
       agentName: 'Reviewer Clone',
+      projectDir: '/workspace/child-session',
       sessionKind: 'subagent',
       parentSessionId: parentSession.sessionId,
       subagentEnabled: false,
       availableSubagentSlots: []
     })

     const tool = new SubagentOrchestratorTool({
-      resolveConversationWorkdir: vi.fn().mockResolvedValue(parentSession.projectDir),
+      resolveConversationWorkdir: vi.fn().mockResolvedValue('/workspace/resolved-parent'),
-    expect(handoffMessage).toContain('/workspace/parent-app')
+    expect(handoffMessage).toContain('/workspace/resolved-parent')

Also applies to: 120-121

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@test/main/presenter/toolPresenter/agentTools/subagentOrchestratorTool.test.ts`
around lines 43 - 55, The test's workdir source is ambiguous because
resolveConversationWorkdir, parentSession.projectDir, and
childSession.projectDir currently all equal the same path, so the assertion
doesn't confirm the tool actually uses the resolver; update the fixture so these
three values differ (e.g., give parentSession.projectDir and
childSession.projectDir distinct paths and have
resolveConversationWorkdir.mockResolvedValue return the expected handoff path)
and assert that SubagentOrchestratorTool uses resolveConversationWorkdir (the
mocked function) rather than copying either session.projectDir; change the
buildSessionInfo calls for parentSession and childSession to set unique
projectDir values and adjust the resolveConversationWorkdir mock return
accordingly.

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.

1 participant