@@ -102,13 +197,12 @@ function ToolCallGroupMulti({ toolCalls, resultMap, isStreaming }: Props) {
{expanded && (
{toolCalls.map((tc) => {
- const result = resultMap.get(tc.toolUseId)
return (
-
)
@@ -118,3 +212,311 @@ function ToolCallGroupMulti({ toolCalls, resultMap, isStreaming }: Props) {
)
}
+
+function AgentCallCard({
+ toolCall,
+ resultMap,
+ childToolCallsByParent,
+ isStreaming = false,
+}: {
+ toolCall: ToolCall
+ resultMap: Map
+ childToolCallsByParent: Map
+ isStreaming?: boolean
+}) {
+ const [expanded, setExpanded] = useState(false)
+ const [previewOpen, setPreviewOpen] = useState(false)
+ const t = useTranslation()
+ const input = toolCall.input && typeof toolCall.input === 'object'
+ ? toolCall.input as Record
+ : {}
+ const result = resultMap.get(toolCall.toolUseId)
+ const childToolCalls = childToolCallsByParent.get(toolCall.toolUseId) ?? []
+ const recentToolCalls = childToolCalls.slice(-2)
+ const status = getAgentStatus({
+ hasResult: !!result,
+ isError: !!result?.isError,
+ isStreaming,
+ childCount: childToolCalls.length,
+ })
+ const statusClassName = getAgentStatusClassName(status)
+ const statusLabel = getAgentStatusLabel(status, t)
+ const errorText = result?.isError ? getAgentErrorSummary(result.content) : ''
+ const fullOutputText = result && !result.isError ? extractTextContent(result.content).trim() : ''
+ const outputSummary = fullOutputText ? getAgentOutputSummary(fullOutputText) : ''
+ const description = typeof input.description === 'string' ? input.description : ''
+
+ return (
+
+
+
smart_toy
+
+
+ Agent
+ {description && (
+
+ {description}
+
+ )}
+
+ {!expanded && recentToolCalls.length > 0 && (
+
+ {recentToolCalls.map((recentToolCall) => (
+
+ {formatRecentToolUseSummary(recentToolCall, resultMap)}
+
+ ))}
+
+ )}
+ {!expanded && !recentToolCalls.length && errorText && (
+
+ {errorText}
+
+ )}
+ {!expanded && !recentToolCalls.length && !errorText && outputSummary && (
+
+ {outputSummary}
+
+ )}
+
+ {outputSummary && (
+
+ )}
+
+ {statusLabel}
+
+
+
+
+ {expanded && (
+
+ {errorText && (
+
+ {errorText}
+
+ )}
+ {childToolCalls.length > 0 ? (
+
+ {childToolCalls.map((childToolCall) => (
+
+ ))}
+
+ ) : outputSummary ? (
+
+
+ {outputSummary}
+
+
+
+ {t('agentStatus.viewResult')}
+
+
+
+ ) : (
+
+ {status === 'starting' ? t('agentStatus.starting') : t('agentStatus.noActivity')}
+
+ )}
+
+ )}
+
setPreviewOpen(false)}
+ title={description || t('agentStatus.resultTitle')}
+ width={900}
+ >
+
+
+
+
+
+ )
+}
+
+function ToolCallTree({
+ toolCall,
+ resultMap,
+ childToolCallsByParent,
+ compact = false,
+}: {
+ toolCall: ToolCall
+ resultMap: Map
+ childToolCallsByParent: Map
+ compact?: boolean
+}) {
+ const result = resultMap.get(toolCall.toolUseId)
+ const childToolCalls = childToolCallsByParent.get(toolCall.toolUseId) ?? []
+
+ return (
+
+
+ {childToolCalls.length > 0 && (
+
+
+ {childToolCalls.map((childToolCall) => (
+
+ ))}
+
+
+ )}
+
+ )
+}
+
+type AgentStatus = 'starting' | 'running' | 'done' | 'failed'
+
+function getAgentStatus({
+ hasResult,
+ isError,
+ isStreaming,
+ childCount,
+}: {
+ hasResult: boolean
+ isError: boolean
+ isStreaming: boolean
+ childCount: number
+}): AgentStatus {
+ if (hasResult && isError) return 'failed'
+ if (hasResult) return 'done'
+ if (isStreaming || childCount > 0) return 'running'
+ return 'starting'
+}
+
+function getAgentStatusLabel(
+ status: AgentStatus,
+ t: (key: any, params?: any) => string,
+): string {
+ switch (status) {
+ case 'failed':
+ return t('agentStatus.failed')
+ case 'done':
+ return t('agentStatus.done')
+ case 'running':
+ return t('agentStatus.running')
+ case 'starting':
+ default:
+ return t('agentStatus.starting')
+ }
+}
+
+function getAgentStatusClassName(status: AgentStatus): string {
+ switch (status) {
+ case 'failed':
+ return 'bg-[var(--color-error)]/10 text-[var(--color-error)]'
+ case 'done':
+ return 'bg-[var(--color-success)]/10 text-[var(--color-success)]'
+ case 'running':
+ return 'bg-[var(--color-warning)]/10 text-[var(--color-warning)]'
+ case 'starting':
+ default:
+ return 'bg-[var(--color-surface-container-high)] text-[var(--color-text-secondary)]'
+ }
+}
+
+function formatRecentToolUseSummary(
+ toolCall: ToolCall,
+ resultMap: Map,
+): string {
+ const input = toolCall.input && typeof toolCall.input === 'object'
+ ? toolCall.input as Record
+ : {}
+ const result = resultMap.get(toolCall.toolUseId)
+ const suffix = result?.isError ? ' • failed' : result ? ' • done' : ' • running'
+
+ switch (toolCall.toolName) {
+ case 'Bash':
+ return `Bash · ${typeof input.command === 'string' ? input.command : ''}${suffix}`
+ case 'Read':
+ return `Read · ${typeof input.file_path === 'string' ? input.file_path.split('/').pop() : 'file'}${suffix}`
+ case 'Glob':
+ return `Glob · ${typeof input.pattern === 'string' ? input.pattern : ''}${suffix}`
+ case 'Grep':
+ return `Grep · ${typeof input.pattern === 'string' ? input.pattern : ''}${suffix}`
+ case 'Agent':
+ return `Agent · ${typeof input.description === 'string' ? input.description : ''}${suffix}`
+ default:
+ return `${toolCall.toolName}${suffix}`
+ }
+}
+
+function getAgentErrorSummary(content: unknown): string {
+ const text = extractTextContent(content).replace(/\s+/g, ' ').trim()
+ if (!text) return ''
+ if (text.includes(`Agent type 'Explore' not found`)) {
+ return 'Explore agent unavailable in this session'
+ }
+ return text.length > 120 ? `${text.slice(0, 120)}...` : text
+}
+
+function getAgentOutputSummary(content: string): string {
+ const text = content.replace(/\s+\n/g, '\n').trim()
+ if (!text) return ''
+ return text.length > 220 ? `${text.slice(0, 220)}...` : text
+}
+
+function extractTextContent(content: unknown): string {
+ if (typeof content === 'string') return content
+ if (Array.isArray(content)) {
+ return content
+ .map((chunk) => {
+ if (typeof chunk === 'string') return chunk
+ if (chunk && typeof chunk === 'object' && 'text' in chunk) {
+ return typeof chunk.text === 'string' ? chunk.text : ''
+ }
+ return ''
+ })
+ .filter(Boolean)
+ .join('\n')
+ }
+ if (content && typeof content === 'object') {
+ if (
+ 'status' in content &&
+ (content as Record).status === 'completed' &&
+ Array.isArray((content as Record).content)
+ ) {
+ return extractTextContent((content as Record).content)
+ }
+ }
+ if (content && typeof content === 'object') {
+ return JSON.stringify(content)
+ }
+ return ''
+}
diff --git a/desktop/src/components/markdown/MarkdownRenderer.tsx b/desktop/src/components/markdown/MarkdownRenderer.tsx
index a0d2eb1d..fd26e457 100644
--- a/desktop/src/components/markdown/MarkdownRenderer.tsx
+++ b/desktop/src/components/markdown/MarkdownRenderer.tsx
@@ -90,15 +90,16 @@ export function MarkdownRenderer({ content }: Props) {
const proseClasses = `prose prose-sm max-w-none text-[var(--color-text-primary)]
prose-headings:text-[var(--color-text-primary)] prose-headings:font-semibold
prose-p:my-2 prose-p:leading-relaxed
+ prose-p:break-words
prose-code:text-[13px] prose-code:font-[var(--font-mono)] prose-code:bg-[var(--color-surface-info)] prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded
prose-pre:!bg-transparent prose-pre:!p-0 prose-pre:!shadow-none
prose-a:text-[var(--color-text-accent)] prose-a:no-underline hover:prose-a:underline
prose-strong:text-[var(--color-text-primary)]
prose-ul:my-2 prose-ol:my-2
prose-li:my-0.5
- prose-table:text-sm
- prose-th:bg-[var(--color-surface-info)] prose-th:px-3 prose-th:py-2
- prose-td:px-3 prose-td:py-2 prose-td:border-[var(--color-border)]`
+ prose-table:w-full prose-table:table-auto prose-table:text-sm
+ prose-th:bg-[var(--color-surface-info)] prose-th:px-3 prose-th:py-2 prose-th:whitespace-normal prose-th:break-words prose-th:align-top
+ prose-td:px-3 prose-td:py-2 prose-td:border-[var(--color-border)] prose-td:whitespace-normal prose-td:break-words prose-td:align-top`
if (codeBlocks.length === 0) {
return (
diff --git a/desktop/src/components/shared/Modal.tsx b/desktop/src/components/shared/Modal.tsx
index ad368308..a74a79a6 100644
--- a/desktop/src/components/shared/Modal.tsx
+++ b/desktop/src/components/shared/Modal.tsx
@@ -33,10 +33,20 @@ export function Modal({ open, onClose, title, children, width = 560, footer }: M
{title && (
-
+
{title}
+
)}
diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts
index 842ac821..7d22245a 100644
--- a/desktop/src/i18n/locales/en.ts
+++ b/desktop/src/i18n/locales/en.ts
@@ -228,6 +228,13 @@ export const en = {
'toolGroup.searchedMany': 'searched {count} patterns',
'toolGroup.agentOne': 'dispatched an agent',
'toolGroup.agentMany': 'dispatched {count} agents',
+ 'agentStatus.starting': 'Starting',
+ 'agentStatus.running': 'Running',
+ 'agentStatus.done': 'Done',
+ 'agentStatus.failed': 'Failed',
+ 'agentStatus.noActivity': 'No tool activity yet',
+ 'agentStatus.viewResult': 'View result',
+ 'agentStatus.resultTitle': 'Agent result',
'toolGroup.searchedWeb': 'searched the web',
'toolGroup.fetchedOne': 'fetched a page',
'toolGroup.fetchedMany': 'fetched {count} pages',
diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts
index 295f08bf..9f5fd3db 100644
--- a/desktop/src/i18n/locales/zh.ts
+++ b/desktop/src/i18n/locales/zh.ts
@@ -230,6 +230,13 @@ export const zh: Record
= {
'toolGroup.searchedMany': '搜索了 {count} 个模式',
'toolGroup.agentOne': '派遣了一个代理',
'toolGroup.agentMany': '派遣了 {count} 个代理',
+ 'agentStatus.starting': '开始中',
+ 'agentStatus.running': '进行中',
+ 'agentStatus.done': '完成',
+ 'agentStatus.failed': '失败',
+ 'agentStatus.noActivity': '暂时还没有工具活动',
+ 'agentStatus.viewResult': '查看结果',
+ 'agentStatus.resultTitle': 'Agent 结果',
'toolGroup.searchedWeb': '搜索了网页',
'toolGroup.fetchedOne': '获取了一个页面',
'toolGroup.fetchedMany': '获取了 {count} 个页面',
diff --git a/desktop/src/stores/chatStore.test.ts b/desktop/src/stores/chatStore.test.ts
index 01eb1d72..f7fc8a3b 100644
--- a/desktop/src/stores/chatStore.test.ts
+++ b/desktop/src/stores/chatStore.test.ts
@@ -64,6 +64,7 @@ describe('chatStore history mapping', () => {
type: 'assistant',
timestamp: '2026-04-06T00:00:00.000Z',
model: 'opus',
+ parentToolUseId: 'agent-1',
content: [
{ type: 'thinking', thinking: 'internal reasoning' },
{ type: 'text', text: '目录结构分析' },
@@ -74,6 +75,7 @@ describe('chatStore history mapping', () => {
id: 'user-1',
type: 'user',
timestamp: '2026-04-06T00:00:01.000Z',
+ parentToolUseId: 'agent-1',
content: [
{ type: 'tool_result', tool_use_id: 'tool-1', content: 'ok', is_error: false },
],
@@ -88,6 +90,39 @@ describe('chatStore history mapping', () => {
'tool_use',
'tool_result',
])
+ expect(mapped[2]).toMatchObject({ parentToolUseId: 'agent-1' })
+ expect(mapped[3]).toMatchObject({ parentToolUseId: 'agent-1' })
+ })
+
+ it('keeps parent tool linkage for live tool events', () => {
+ useChatStore.getState().handleServerMessage({
+ type: 'tool_use_complete',
+ toolName: 'Read',
+ toolUseId: 'tool-1',
+ input: { file_path: 'src/App.tsx' },
+ parentToolUseId: 'agent-1',
+ })
+
+ useChatStore.getState().handleServerMessage({
+ type: 'tool_result',
+ toolUseId: 'tool-1',
+ content: 'ok',
+ isError: false,
+ parentToolUseId: 'agent-1',
+ })
+
+ expect(useChatStore.getState().messages).toMatchObject([
+ {
+ type: 'tool_use',
+ toolUseId: 'tool-1',
+ parentToolUseId: 'agent-1',
+ },
+ {
+ type: 'tool_result',
+ toolUseId: 'tool-1',
+ parentToolUseId: 'agent-1',
+ },
+ ])
})
it('sends permission mode updates to the active session only', () => {
diff --git a/desktop/src/stores/chatStore.ts b/desktop/src/stores/chatStore.ts
index abfc172f..ba9ef9de 100644
--- a/desktop/src/stores/chatStore.ts
+++ b/desktop/src/stores/chatStore.ts
@@ -334,6 +334,7 @@ export const useChatStore = create((set, get) => ({
toolUseId: msg.toolUseId || s.activeToolUseId || '',
input: msg.input,
timestamp: Date.now(),
+ parentToolUseId: msg.parentToolUseId,
}],
activeToolUseId: null,
activeToolName: null,
@@ -361,6 +362,7 @@ export const useChatStore = create((set, get) => ({
content: msg.content,
isError: msg.isError,
timestamp: Date.now(),
+ parentToolUseId: msg.parentToolUseId,
}],
chatState: 'thinking',
activeThinkingId: null,
@@ -532,6 +534,7 @@ export function mapHistoryMessagesToUiMessages(messages: MessageEntry[]): UIMess
toolUseId: block.id ?? '',
input: block.input,
timestamp,
+ parentToolUseId: msg.parentToolUseId,
})
}
}
@@ -567,6 +570,7 @@ export function mapHistoryMessagesToUiMessages(messages: MessageEntry[]): UIMess
content: block.content,
isError: !!block.is_error,
timestamp,
+ parentToolUseId: msg.parentToolUseId,
})
}
}
diff --git a/desktop/src/types/chat.ts b/desktop/src/types/chat.ts
index d01b2a82..261d9cba 100644
--- a/desktop/src/types/chat.ts
+++ b/desktop/src/types/chat.ts
@@ -30,10 +30,10 @@ export type UIAttachment = {
export type ServerMessage =
| { type: 'connected'; sessionId: string }
- | { type: 'content_start'; blockType: 'text' | 'tool_use'; toolName?: string; toolUseId?: string }
+ | { type: 'content_start'; blockType: 'text' | 'tool_use'; toolName?: string; toolUseId?: string; parentToolUseId?: string }
| { type: 'content_delta'; text?: string; toolInput?: string }
- | { type: 'tool_use_complete'; toolName: string; toolUseId: string; input: unknown }
- | { type: 'tool_result'; toolUseId: string; content: unknown; isError: boolean }
+ | { type: 'tool_use_complete'; toolName: string; toolUseId: string; input: unknown; parentToolUseId?: string }
+ | { type: 'tool_result'; toolUseId: string; content: unknown; isError: boolean; parentToolUseId?: string }
| { type: 'permission_request'; requestId: string; toolName: string; input: unknown; description?: string }
| { type: 'message_complete'; usage: TokenUsage }
| { type: 'thinking'; text: string }
@@ -76,8 +76,8 @@ export type UIMessage =
| { id: string; type: 'user_text'; content: string; timestamp: number; attachments?: UIAttachment[] }
| { id: string; type: 'assistant_text'; content: string; timestamp: number; model?: string }
| { id: string; type: 'thinking'; content: string; timestamp: number }
- | { id: string; type: 'tool_use'; toolName: string; toolUseId: string; input: unknown; timestamp: number }
- | { id: string; type: 'tool_result'; toolUseId: string; content: unknown; isError: boolean; timestamp: number }
+ | { id: string; type: 'tool_use'; toolName: string; toolUseId: string; input: unknown; timestamp: number; parentToolUseId?: string }
+ | { id: string; type: 'tool_result'; toolUseId: string; content: unknown; isError: boolean; timestamp: number; parentToolUseId?: string }
| { id: string; type: 'system'; content: string; timestamp: number }
| { id: string; type: 'permission_request'; requestId: string; toolName: string; input: unknown; description?: string; timestamp: number }
| { id: string; type: 'error'; message: string; code: string; timestamp: number }
diff --git a/desktop/src/types/session.ts b/desktop/src/types/session.ts
index e8584e15..96a7e609 100644
--- a/desktop/src/types/session.ts
+++ b/desktop/src/types/session.ts
@@ -18,6 +18,7 @@ export type MessageEntry = {
timestamp: string
model?: string
parentUuid?: string
+ parentToolUseId?: string
isSidechain?: boolean
}
diff --git a/src/server/__tests__/sessions.test.ts b/src/server/__tests__/sessions.test.ts
index 146284bd..95991cde 100644
--- a/src/server/__tests__/sessions.test.ts
+++ b/src/server/__tests__/sessions.test.ts
@@ -246,6 +246,96 @@ describe('SessionService', () => {
expect(messages).toHaveLength(2)
})
+ it('should reconstruct parent agent tool linkage from parentUuid chains', async () => {
+ const sessionId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
+ const userUuid = crypto.randomUUID()
+ const agentAssistantUuid = crypto.randomUUID()
+ const childAssistantUuid = crypto.randomUUID()
+
+ await writeSessionFile('-tmp-project', sessionId, [
+ makeSnapshotEntry(),
+ makeUserEntry('Inspect the codebase', userUuid),
+ {
+ parentUuid: userUuid,
+ isSidechain: false,
+ type: 'assistant',
+ message: {
+ model: 'claude-opus-4-6',
+ id: `msg_${crypto.randomUUID().slice(0, 20)}`,
+ type: 'message',
+ role: 'assistant',
+ content: [
+ {
+ type: 'tool_use',
+ name: 'Agent',
+ id: 'agent-tool-1',
+ input: { description: 'Inspect src/components' },
+ },
+ ],
+ },
+ uuid: agentAssistantUuid,
+ timestamp: '2026-01-01T00:02:00.000Z',
+ },
+ {
+ parentUuid: agentAssistantUuid,
+ isSidechain: true,
+ type: 'assistant',
+ message: {
+ model: 'claude-opus-4-6',
+ id: `msg_${crypto.randomUUID().slice(0, 20)}`,
+ type: 'message',
+ role: 'assistant',
+ content: [
+ {
+ type: 'tool_use',
+ name: 'Read',
+ id: 'read-tool-1',
+ input: { file_path: 'src/components/App.tsx' },
+ },
+ ],
+ },
+ uuid: childAssistantUuid,
+ timestamp: '2026-01-01T00:02:30.000Z',
+ },
+ {
+ parentUuid: childAssistantUuid,
+ isSidechain: true,
+ type: 'user',
+ message: {
+ role: 'user',
+ content: [
+ {
+ type: 'tool_result',
+ tool_use_id: 'read-tool-1',
+ content: 'ok',
+ is_error: false,
+ },
+ ],
+ },
+ uuid: crypto.randomUUID(),
+ timestamp: '2026-01-01T00:03:00.000Z',
+ userType: 'external',
+ cwd: '/tmp/test',
+ sessionId: 'test-session',
+ },
+ ])
+
+ const messages = await service.getSessionMessages(sessionId)
+
+ expect(messages[1]).toMatchObject({
+ type: 'tool_use',
+ parentToolUseId: undefined,
+ })
+ expect(messages[2]).toMatchObject({
+ type: 'tool_use',
+ parentToolUseId: 'agent-tool-1',
+ })
+ expect(messages[3]).toMatchObject({
+ type: 'tool_result',
+ parentToolUseId: 'agent-tool-1',
+ })
+ })
+
it('should recover workDir from session-meta entries', async () => {
const sessionId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
await writeSessionFile('-tmp-project', sessionId, [
diff --git a/src/server/services/sessionService.ts b/src/server/services/sessionService.ts
index b76f6db0..640c7f14 100644
--- a/src/server/services/sessionService.ts
+++ b/src/server/services/sessionService.ts
@@ -44,6 +44,7 @@ export type MessageEntry = {
timestamp: string
model?: string
parentUuid?: string
+ parentToolUseId?: string
isSidechain?: boolean
}
@@ -52,6 +53,7 @@ type RawEntry = {
type?: string
uuid?: string
parentUuid?: string | null
+ parent_tool_use_id?: string | null
isSidechain?: boolean
isMeta?: boolean
cwd?: string
@@ -150,7 +152,10 @@ export class SessionService {
// Entry → MessageEntry conversion
// --------------------------------------------------------------------------
- private entryToMessage(entry: RawEntry): MessageEntry | null {
+ private entryToMessage(
+ entry: RawEntry,
+ parentToolUseId?: string,
+ ): MessageEntry | null {
const msg = entry.message
if (!msg || !msg.role) return null
@@ -193,10 +198,83 @@ export class SessionService {
timestamp: entry.timestamp || new Date().toISOString(),
model: msg.model,
parentUuid: entry.parentUuid ?? undefined,
+ parentToolUseId,
isSidechain: entry.isSidechain,
}
}
+ private extractAgentToolUseId(entry: RawEntry): string | undefined {
+ const content = entry.message?.content
+ if (!Array.isArray(content)) return undefined
+
+ for (const block of content as Array>) {
+ if (
+ block.type === 'tool_use' &&
+ block.name === 'Agent' &&
+ typeof block.id === 'string'
+ ) {
+ return block.id
+ }
+ }
+
+ return undefined
+ }
+
+ private resolveParentToolUseId(
+ entry: RawEntry,
+ entriesByUuid: Map,
+ cache: Map,
+ ): string | undefined {
+ if (
+ typeof entry.parent_tool_use_id === 'string' &&
+ entry.parent_tool_use_id.length > 0
+ ) {
+ return entry.parent_tool_use_id
+ }
+
+ if (entry.isSidechain !== true) {
+ return undefined
+ }
+
+ const cacheKey = entry.uuid
+ if (cacheKey && cache.has(cacheKey)) {
+ return cache.get(cacheKey)
+ }
+
+ let resolved: string | undefined
+ let currentParentUuid =
+ typeof entry.parentUuid === 'string' ? entry.parentUuid : undefined
+ const visited = new Set()
+
+ while (currentParentUuid && !visited.has(currentParentUuid)) {
+ visited.add(currentParentUuid)
+ const parentEntry = entriesByUuid.get(currentParentUuid)
+ if (!parentEntry) break
+
+ const directAgentToolUseId = this.extractAgentToolUseId(parentEntry)
+ if (directAgentToolUseId) {
+ resolved = directAgentToolUseId
+ break
+ }
+
+ if (parentEntry.uuid && cache.has(parentEntry.uuid)) {
+ resolved = cache.get(parentEntry.uuid)
+ break
+ }
+
+ currentParentUuid =
+ typeof parentEntry.parentUuid === 'string'
+ ? parentEntry.parentUuid
+ : undefined
+ }
+
+ if (cacheKey) {
+ cache.set(cacheKey, resolved)
+ }
+
+ return resolved
+ }
+
// --------------------------------------------------------------------------
// Title extraction
// --------------------------------------------------------------------------
@@ -651,6 +729,15 @@ export class SessionService {
private entriesToMessages(entries: RawEntry[]): MessageEntry[] {
const messages: MessageEntry[] = []
+ const entriesByUuid = new Map()
+ const parentToolUseIdCache = new Map()
+
+ for (const entry of entries) {
+ if (typeof entry.uuid === 'string' && entry.uuid.length > 0) {
+ entriesByUuid.set(entry.uuid, entry)
+ }
+ }
+
for (const entry of entries) {
// Only process transcript entries (user / assistant / system with messages)
if (!entry.message?.role) continue
@@ -668,7 +755,12 @@ export class SessionService {
continue
}
- const msg = this.entryToMessage(entry)
+ const parentToolUseId = this.resolveParentToolUseId(
+ entry,
+ entriesByUuid,
+ parentToolUseIdCache,
+ )
+ const msg = this.entryToMessage(entry, parentToolUseId)
if (msg) {
messages.push(msg)
}
diff --git a/src/server/ws/events.ts b/src/server/ws/events.ts
index 93431d9f..508740ec 100644
--- a/src/server/ws/events.ts
+++ b/src/server/ws/events.ts
@@ -29,10 +29,10 @@ export type AttachmentRef = {
export type ServerMessage =
| { type: 'connected'; sessionId: string }
- | { type: 'content_start'; blockType: 'text' | 'tool_use'; toolName?: string; toolUseId?: string }
+ | { type: 'content_start'; blockType: 'text' | 'tool_use'; toolName?: string; toolUseId?: string; parentToolUseId?: string }
| { type: 'content_delta'; text?: string; toolInput?: string }
- | { type: 'tool_use_complete'; toolName: string; toolUseId: string; input: unknown }
- | { type: 'tool_result'; toolUseId: string; content: unknown; isError: boolean }
+ | { type: 'tool_use_complete'; toolName: string; toolUseId: string; input: unknown; parentToolUseId?: string }
+ | { type: 'tool_result'; toolUseId: string; content: unknown; isError: boolean; parentToolUseId?: string }
| { type: 'permission_request'; requestId: string; toolName: string; input: unknown; description?: string }
| { type: 'message_complete'; usage: TokenUsage }
| { type: 'thinking'; text: string }
diff --git a/src/server/ws/handler.ts b/src/server/ws/handler.ts
index 46966cc7..dcb27a5c 100644
--- a/src/server/ws/handler.ts
+++ b/src/server/ws/handler.ts
@@ -377,6 +377,10 @@ function translateCliMessage(cliMsg: any, sessionId: string): ServerMessage[] {
toolName: block.name,
toolUseId: block.id,
input: block.input,
+ parentToolUseId:
+ typeof cliMsg.parent_tool_use_id === 'string'
+ ? cliMsg.parent_tool_use_id
+ : undefined,
})
}
}
@@ -402,6 +406,10 @@ function translateCliMessage(cliMsg: any, sessionId: string): ServerMessage[] {
toolUseId: block.tool_use_id,
content: block.content,
isError: !!block.is_error,
+ parentToolUseId:
+ typeof cliMsg.parent_tool_use_id === 'string'
+ ? cliMsg.parent_tool_use_id
+ : undefined,
})
}
}
@@ -439,6 +447,10 @@ function translateCliMessage(cliMsg: any, sessionId: string): ServerMessage[] {
blockType: 'tool_use',
toolName: contentBlock.name,
toolUseId: contentBlock.id,
+ parentToolUseId:
+ typeof cliMsg.parent_tool_use_id === 'string'
+ ? cliMsg.parent_tool_use_id
+ : undefined,
}]
}
return [{ type: 'content_start', blockType: 'text' }]
@@ -480,6 +492,10 @@ function translateCliMessage(cliMsg: any, sessionId: string): ServerMessage[] {
toolName: toolBlock.toolName,
toolUseId: toolBlock.toolUseId,
input: parsedInput,
+ parentToolUseId:
+ typeof cliMsg.parent_tool_use_id === 'string'
+ ? cliMsg.parent_tool_use_id
+ : undefined,
}]
}
}