mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Restore sub-agent context in the desktop chat transcript
The desktop chat view flattened Agent tool activity, which made sub-agent work hard to follow and separated key evidence from the main conversation. This change threads parent tool linkage through the server bridge and desktop store, renders dispatched sub-agents as grouped cards with nested tool activity, and moves long final outputs into a markdown preview dialog so the main transcript stays readable on narrow layouts. Constraint: Existing sessions and live websocket events both needed to preserve parent-child relationships Rejected: Add brand-new subagent websocket event types | unnecessary protocol expansion when parent linkage already existed upstream Rejected: Inline full sub-agent markdown in the card body | too cramped for narrow desktop chat layouts Confidence: medium Scope-risk: moderate Reversibility: clean Directive: Keep Agent card summaries compact; route long-form sub-agent output through the preview dialog unless the main chat layout is widened substantially Tested: cd desktop && bun run test -- MessageList.test.tsx chatStore.test.ts Tested: cd desktop && bun run lint Tested: bun test src/server/__tests__/sessions.test.ts -t should\ reconstruct\ parent\ agent\ tool\ linkage\ from\ parentUuid\ chains Not-tested: Full end-to-end visual verification against live CLI sessions with sub-agent text/thinking nested inline
This commit is contained in:
parent
bfd2405857
commit
91b4d16dc5
@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import { MessageList, buildRenderItems } from './MessageList'
|
||||
import { useChatStore } from '../../stores/chatStore'
|
||||
import type { UIMessage } from '../../types/chat'
|
||||
@ -132,6 +132,45 @@ describe('MessageList nested tool calls', () => {
|
||||
expect(screen.getByText('Explore agent unavailable in this session')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('shows completed agent output when no nested tool activity is available', () => {
|
||||
const longResult = '探索完成。让我将结果整合写入计划文件。第二段补充内容用于验证 dialog 展示的是完整结果而不是截断摘要。'
|
||||
|
||||
useChatStore.setState({
|
||||
messages: [
|
||||
{
|
||||
id: 'tool-agent',
|
||||
type: 'tool_use',
|
||||
toolName: 'Agent',
|
||||
toolUseId: 'agent-1',
|
||||
input: { description: '探索整体架构' },
|
||||
timestamp: 1,
|
||||
},
|
||||
{
|
||||
id: 'result-agent',
|
||||
type: 'tool_result',
|
||||
toolUseId: 'agent-1',
|
||||
content: {
|
||||
status: 'completed',
|
||||
content: [{ type: 'text', text: longResult }],
|
||||
},
|
||||
isError: false,
|
||||
timestamp: 2,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
render(<MessageList />)
|
||||
|
||||
expect(screen.getByText('Done')).toBeTruthy()
|
||||
expect(screen.getByRole('button', { name: 'View result' })).toBeTruthy()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'View result' }))
|
||||
|
||||
const dialog = screen.getByRole('dialog')
|
||||
expect(within(dialog).getByText(/第二段补充内容用于验证 dialog 展示的是完整结果而不是截断摘要。/)).toBeTruthy()
|
||||
expect(screen.getByRole('button', { name: 'Close dialog' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders copy controls for user messages and scopes assistant copy to a single reply', async () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined)
|
||||
Object.assign(navigator, {
|
||||
|
||||
@ -21,7 +21,7 @@ type RenderItem =
|
||||
| { kind: 'tool_group'; toolCalls: ToolCall[]; id: string }
|
||||
| { kind: 'message'; message: UIMessage }
|
||||
|
||||
function buildRenderItems(messages: UIMessage[], toolUseIds: Set<string>): RenderItem[] {
|
||||
export function buildRenderItems(messages: UIMessage[], toolUseIds: Set<string>): RenderItem[] {
|
||||
const items: RenderItem[] = []
|
||||
let pendingToolCalls: ToolCall[] = []
|
||||
|
||||
@ -42,6 +42,10 @@ function buildRenderItems(messages: UIMessage[], toolUseIds: Set<string>): Rende
|
||||
}
|
||||
|
||||
if (msg.type === 'tool_use') {
|
||||
if (msg.parentToolUseId) {
|
||||
flushGroup()
|
||||
continue
|
||||
}
|
||||
if (msg.toolName === 'AskUserQuestion') {
|
||||
flushGroup()
|
||||
items.push({ kind: 'message', message: msg })
|
||||
@ -68,10 +72,19 @@ export function MessageList() {
|
||||
|
||||
const toolUseIds = new Set<string>()
|
||||
const toolResultMap = new Map<string, ToolResult>()
|
||||
const childToolCallsByParent = new Map<string, ToolCall[]>()
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.type === 'tool_use') {
|
||||
toolUseIds.add(msg.toolUseId)
|
||||
if (msg.parentToolUseId) {
|
||||
const siblings = childToolCallsByParent.get(msg.parentToolUseId)
|
||||
if (siblings) {
|
||||
siblings.push(msg)
|
||||
} else {
|
||||
childToolCallsByParent.set(msg.parentToolUseId, [msg])
|
||||
}
|
||||
}
|
||||
}
|
||||
if (msg.type === 'tool_result' && msg.toolUseId) {
|
||||
toolResultMap.set(msg.toolUseId, msg)
|
||||
@ -90,6 +103,7 @@ export function MessageList() {
|
||||
key={item.id}
|
||||
toolCalls={item.toolCalls}
|
||||
resultMap={toolResultMap}
|
||||
childToolCallsByParent={childToolCallsByParent}
|
||||
isStreaming={
|
||||
chatState === 'tool_executing' &&
|
||||
item.toolCalls.some((tc) => !toolResultMap.has(tc.toolUseId))
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ToolCallBlock } from './ToolCallBlock'
|
||||
import { MarkdownRenderer } from '../markdown/MarkdownRenderer'
|
||||
import { Modal } from '../shared/Modal'
|
||||
import { useTranslation } from '../../i18n'
|
||||
import type { UIMessage } from '../../types/chat'
|
||||
|
||||
@ -9,6 +11,7 @@ type ToolResult = Extract<UIMessage, { type: 'tool_result' }>
|
||||
type Props = {
|
||||
toolCalls: ToolCall[]
|
||||
resultMap: Map<string, ToolResult>
|
||||
childToolCallsByParent: Map<string, ToolCall[]>
|
||||
/** When true, the last tool is still executing — show expanded */
|
||||
isStreaming?: boolean
|
||||
}
|
||||
@ -47,30 +50,122 @@ function groupHasErrors(toolCalls: ToolCall[], resultMap: Map<string, ToolResult
|
||||
})
|
||||
}
|
||||
|
||||
export function ToolCallGroup({ toolCalls, resultMap, isStreaming }: Props) {
|
||||
// Single tool call — render directly without group wrapper
|
||||
if (toolCalls.length === 1) {
|
||||
const tc = toolCalls[0]!
|
||||
const result = resultMap.get(tc.toolUseId)
|
||||
export function ToolCallGroup({ toolCalls, resultMap, childToolCallsByParent, isStreaming }: Props) {
|
||||
const allAgents = toolCalls.every((toolCall) => toolCall.toolName === 'Agent')
|
||||
|
||||
if (allAgents) {
|
||||
return (
|
||||
<ToolCallBlock
|
||||
toolName={tc.toolName}
|
||||
input={tc.input}
|
||||
result={result ? { content: result.content, isError: result.isError } : null}
|
||||
<AgentToolGroup
|
||||
toolCalls={toolCalls}
|
||||
resultMap={resultMap}
|
||||
childToolCallsByParent={childToolCallsByParent}
|
||||
isStreaming={isStreaming}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <ToolCallGroupMulti toolCalls={toolCalls} resultMap={resultMap} isStreaming={isStreaming} />
|
||||
// Single tool call — render directly without group wrapper
|
||||
if (toolCalls.length === 1) {
|
||||
const tc = toolCalls[0]!
|
||||
return (
|
||||
<ToolCallTree
|
||||
toolCall={tc}
|
||||
resultMap={resultMap}
|
||||
childToolCallsByParent={childToolCallsByParent}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolCallGroupMulti
|
||||
toolCalls={toolCalls}
|
||||
resultMap={resultMap}
|
||||
childToolCallsByParent={childToolCallsByParent}
|
||||
isStreaming={isStreaming}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentToolGroup({ toolCalls, resultMap, childToolCallsByParent, isStreaming }: Props) {
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
const t = useTranslation()
|
||||
const errorPresent = groupHasErrors(toolCalls, resultMap)
|
||||
const allComplete = toolCalls.every((tc) => resultMap.has(tc.toolUseId))
|
||||
|
||||
useEffect(() => {
|
||||
if (isStreaming) {
|
||||
setExpanded(true)
|
||||
}
|
||||
}, [isStreaming])
|
||||
|
||||
return (
|
||||
<div className="mb-2 ml-10">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((value) => !value)}
|
||||
className="flex w-full items-center gap-2 rounded-lg border border-[var(--color-border)]/40 bg-[var(--color-surface-container-low)] px-3 py-1.5 text-left transition-colors hover:bg-[var(--color-surface-container-high)]"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] text-[var(--color-outline)]">
|
||||
{expanded ? 'expand_less' : 'expand_more'}
|
||||
</span>
|
||||
<span className="flex-1 truncate text-[12px] text-[var(--color-text-secondary)]">
|
||||
{toolCalls.length === 1 ? t('toolGroup.agentOne') : t('toolGroup.agentMany', { count: toolCalls.length })}
|
||||
</span>
|
||||
{isStreaming && (
|
||||
<span className="rounded-full bg-[var(--color-warning)]/12 px-2 py-0.5 text-[10px] font-semibold text-[var(--color-warning)]">
|
||||
{t('agentStatus.running')}
|
||||
</span>
|
||||
)}
|
||||
{!isStreaming && errorPresent && (
|
||||
<span className="material-symbols-outlined text-[14px] text-[var(--color-error)]">error</span>
|
||||
)}
|
||||
{!isStreaming && !errorPresent && allComplete && (
|
||||
<span className="material-symbols-outlined text-[14px] text-[var(--color-success)]">check_circle</span>
|
||||
)}
|
||||
{!isStreaming && !errorPresent && !allComplete && (
|
||||
<span className="material-symbols-outlined text-[14px] text-[var(--color-outline)]">pending</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="relative mt-3 pl-5">
|
||||
<div className="absolute bottom-6 left-[11px] top-4 w-px rounded-full bg-[var(--color-border)]/45" />
|
||||
<div className="space-y-2">
|
||||
{toolCalls.map((toolCall) => (
|
||||
<div key={toolCall.id} className="relative pl-7">
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2">
|
||||
<div className="absolute left-[11px] top-1/2 h-px w-4 -translate-y-1/2 bg-[var(--color-border)]/45" />
|
||||
<div className="absolute left-[8px] top-1/2 h-2.5 w-2.5 -translate-y-1/2 rounded-full border border-[var(--color-border)]/65 bg-[var(--color-surface-container-lowest)] shadow-[0_0_0_2px_var(--color-surface)]" />
|
||||
</div>
|
||||
<AgentCallCard
|
||||
toolCall={toolCall}
|
||||
resultMap={resultMap}
|
||||
childToolCallsByParent={childToolCallsByParent}
|
||||
isStreaming={isStreaming && !resultMap.has(toolCall.toolUseId)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Separated so the useState hook is never called conditionally. */
|
||||
function ToolCallGroupMulti({ toolCalls, resultMap, isStreaming }: Props) {
|
||||
function ToolCallGroupMulti({ toolCalls, resultMap, childToolCallsByParent, isStreaming }: Props) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const t = useTranslation()
|
||||
const summary = generateSummary(toolCalls, t)
|
||||
const errorPresent = groupHasErrors(toolCalls, resultMap)
|
||||
const allComplete = toolCalls.every((tc) => resultMap.has(tc.toolUseId))
|
||||
const hasNestedToolCalls = toolCalls.some((tc) => (childToolCallsByParent.get(tc.toolUseId)?.length ?? 0) > 0)
|
||||
|
||||
useEffect(() => {
|
||||
if (isStreaming || hasNestedToolCalls) {
|
||||
setExpanded(true)
|
||||
}
|
||||
}, [hasNestedToolCalls, isStreaming])
|
||||
|
||||
return (
|
||||
<div className="mb-2 ml-10">
|
||||
@ -102,13 +197,12 @@ function ToolCallGroupMulti({ toolCalls, resultMap, isStreaming }: Props) {
|
||||
{expanded && (
|
||||
<div className="mt-1.5 space-y-1">
|
||||
{toolCalls.map((tc) => {
|
||||
const result = resultMap.get(tc.toolUseId)
|
||||
return (
|
||||
<ToolCallBlock
|
||||
<ToolCallTree
|
||||
key={tc.id}
|
||||
toolName={tc.toolName}
|
||||
input={tc.input}
|
||||
result={result ? { content: result.content, isError: result.isError } : null}
|
||||
toolCall={tc}
|
||||
resultMap={resultMap}
|
||||
childToolCallsByParent={childToolCallsByParent}
|
||||
compact
|
||||
/>
|
||||
)
|
||||
@ -118,3 +212,311 @@ function ToolCallGroupMulti({ toolCalls, resultMap, isStreaming }: Props) {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentCallCard({
|
||||
toolCall,
|
||||
resultMap,
|
||||
childToolCallsByParent,
|
||||
isStreaming = false,
|
||||
}: {
|
||||
toolCall: ToolCall
|
||||
resultMap: Map<string, ToolResult>
|
||||
childToolCallsByParent: Map<string, ToolCall[]>
|
||||
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<string, unknown>
|
||||
: {}
|
||||
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 (
|
||||
<div className="overflow-hidden rounded-lg border border-[var(--color-border)]/50 bg-[var(--color-surface-container-lowest)]">
|
||||
<div className="flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-[var(--color-surface-hover)]/50">
|
||||
<span className="material-symbols-outlined text-[18px] text-[var(--color-outline)]">smart_toy</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[13px] font-semibold text-[var(--color-text-primary)]">Agent</span>
|
||||
{description && (
|
||||
<span className="truncate text-[12px] text-[var(--color-text-secondary)]">
|
||||
{description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!expanded && recentToolCalls.length > 0 && (
|
||||
<div className="mt-1 space-y-1">
|
||||
{recentToolCalls.map((recentToolCall) => (
|
||||
<div
|
||||
key={recentToolCall.id}
|
||||
className="truncate text-[11px] text-[var(--color-text-tertiary)]"
|
||||
>
|
||||
{formatRecentToolUseSummary(recentToolCall, resultMap)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!expanded && !recentToolCalls.length && errorText && (
|
||||
<div className="mt-1 truncate text-[11px] text-[var(--color-error)]">
|
||||
{errorText}
|
||||
</div>
|
||||
)}
|
||||
{!expanded && !recentToolCalls.length && !errorText && outputSummary && (
|
||||
<div className="mt-1 line-clamp-2 text-[11px] text-[var(--color-text-tertiary)]">
|
||||
{outputSummary}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{outputSummary && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
setPreviewOpen(true)
|
||||
}}
|
||||
className="shrink-0 rounded-md border border-[var(--color-border)] px-2.5 py-1 text-[11px] font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]"
|
||||
>
|
||||
{t('agentStatus.viewResult')}
|
||||
</button>
|
||||
)}
|
||||
<span className={`rounded-full px-2 py-0.5 text-[10px] font-semibold ${statusClassName}`}>
|
||||
{statusLabel}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((value) => !value)}
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[var(--color-outline)] transition-colors hover:bg-[var(--color-surface-hover)]"
|
||||
aria-label={expanded ? 'Collapse agent' : 'Expand agent'}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">
|
||||
{expanded ? 'expand_less' : 'expand_more'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="border-t border-[var(--color-border)]/60 px-3 py-3">
|
||||
{errorText && (
|
||||
<div className="mb-3 rounded-lg border border-[var(--color-error)]/20 bg-[var(--color-error-container)]/60 px-3 py-2 text-[11px] text-[var(--color-error)]">
|
||||
{errorText}
|
||||
</div>
|
||||
)}
|
||||
{childToolCalls.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{childToolCalls.map((childToolCall) => (
|
||||
<ToolCallTree
|
||||
key={childToolCall.id}
|
||||
toolCall={childToolCall}
|
||||
resultMap={resultMap}
|
||||
childToolCallsByParent={childToolCallsByParent}
|
||||
compact
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : outputSummary ? (
|
||||
<div className="rounded-lg border border-[var(--color-border)]/60 bg-[var(--color-surface)] px-3 py-3">
|
||||
<div className="line-clamp-3 text-[11px] leading-[1.55] text-[var(--color-text-secondary)]">
|
||||
{outputSummary}
|
||||
</div>
|
||||
<div className="mt-3 flex justify-end">
|
||||
<span className="text-[10px] text-[var(--color-text-tertiary)]">
|
||||
{t('agentStatus.viewResult')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[11px] text-[var(--color-text-tertiary)]">
|
||||
{status === 'starting' ? t('agentStatus.starting') : t('agentStatus.noActivity')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
open={previewOpen}
|
||||
onClose={() => setPreviewOpen(false)}
|
||||
title={description || t('agentStatus.resultTitle')}
|
||||
width={900}
|
||||
>
|
||||
<div className="max-h-[70vh] overflow-y-auto">
|
||||
<MarkdownRenderer content={fullOutputText || errorText} />
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ToolCallTree({
|
||||
toolCall,
|
||||
resultMap,
|
||||
childToolCallsByParent,
|
||||
compact = false,
|
||||
}: {
|
||||
toolCall: ToolCall
|
||||
resultMap: Map<string, ToolResult>
|
||||
childToolCallsByParent: Map<string, ToolCall[]>
|
||||
compact?: boolean
|
||||
}) {
|
||||
const result = resultMap.get(toolCall.toolUseId)
|
||||
const childToolCalls = childToolCallsByParent.get(toolCall.toolUseId) ?? []
|
||||
|
||||
return (
|
||||
<div className={compact ? 'space-y-1' : ''}>
|
||||
<ToolCallBlock
|
||||
toolName={toolCall.toolName}
|
||||
input={toolCall.input}
|
||||
result={result ? { content: result.content, isError: result.isError } : null}
|
||||
compact={compact}
|
||||
/>
|
||||
{childToolCalls.length > 0 && (
|
||||
<div className={compact ? 'ml-4 border-l border-[var(--color-border)]/60 pl-3' : 'mb-2 ml-16 border-l border-[var(--color-border)]/60 pl-3'}>
|
||||
<div className="space-y-1">
|
||||
{childToolCalls.map((childToolCall) => (
|
||||
<ToolCallTree
|
||||
key={childToolCall.id}
|
||||
toolCall={childToolCall}
|
||||
resultMap={resultMap}
|
||||
childToolCallsByParent={childToolCallsByParent}
|
||||
compact
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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, ToolResult>,
|
||||
): string {
|
||||
const input = toolCall.input && typeof toolCall.input === 'object'
|
||||
? toolCall.input as Record<string, unknown>
|
||||
: {}
|
||||
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<string, unknown>).status === 'completed' &&
|
||||
Array.isArray((content as Record<string, unknown>).content)
|
||||
) {
|
||||
return extractTextContent((content as Record<string, unknown>).content)
|
||||
}
|
||||
}
|
||||
if (content && typeof content === 'object') {
|
||||
return JSON.stringify(content)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -33,10 +33,20 @@ export function Modal({ open, onClose, title, children, width = 560, footer }: M
|
||||
<div
|
||||
className="relative bg-[var(--color-surface)] rounded-[var(--radius-xl)] shadow-[var(--shadow-modal)] max-h-[85vh] flex flex-col"
|
||||
style={{ width, maxWidth: 'calc(100vw - 48px)' }}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
{title && (
|
||||
<div className="px-6 pt-6 pb-0">
|
||||
<div className="flex items-start justify-between gap-4 px-6 pt-6 pb-0">
|
||||
<h2 className="text-xl font-bold text-[var(--color-text-primary)]">{title}</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Close dialog"
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)]"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">close</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -230,6 +230,13 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'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} 个页面',
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -334,6 +334,7 @@ export const useChatStore = create<ChatStore>((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<ChatStore>((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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -18,6 +18,7 @@ export type MessageEntry = {
|
||||
timestamp: string
|
||||
model?: string
|
||||
parentUuid?: string
|
||||
parentToolUseId?: string
|
||||
isSidechain?: boolean
|
||||
}
|
||||
|
||||
|
||||
@ -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, [
|
||||
|
||||
@ -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<Record<string, unknown>>) {
|
||||
if (
|
||||
block.type === 'tool_use' &&
|
||||
block.name === 'Agent' &&
|
||||
typeof block.id === 'string'
|
||||
) {
|
||||
return block.id
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
private resolveParentToolUseId(
|
||||
entry: RawEntry,
|
||||
entriesByUuid: Map<string, RawEntry>,
|
||||
cache: Map<string, string | undefined>,
|
||||
): 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<string>()
|
||||
|
||||
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<string, RawEntry>()
|
||||
const parentToolUseIdCache = new Map<string, string | undefined>()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user