fix(desktop): improve activity panel interactions

Tested: cd desktop && bun run test -- src/components/activity/SessionActivityPanel.test.tsx src/pages/ActiveSession.test.tsx src/pages/SubagentRunPage.test.tsx --run
Tested: bun run check:desktop
Tested: browser smoke for activity panel auto-open and SubAgent run rendering
Not-tested: full bun run verify was not run for this scoped desktop handoff
Confidence: high
Scope-risk: narrow
This commit is contained in:
程序员阿江(Relakkes) 2026-07-06 21:12:00 +08:00
parent c68d42f6a1
commit 75ddd158b4
6 changed files with 237 additions and 82 deletions

View File

@ -160,6 +160,7 @@ describe('SessionActivityPanel', () => {
expect(screen.getByLabelText('Task completed')).toBeInTheDocument()
expect(screen.getByLabelText('Task in progress')).toBeInTheDocument()
expect(screen.getByLabelText('Task pending')).toBeInTheDocument()
expect(screen.getByText('Active task').closest('button,div')).toHaveClass('py-3')
expect(screen.queryByText('Completed')).not.toBeInTheDocument()
expect(screen.queryByText('Pending')).not.toBeInTheDocument()
})
@ -392,17 +393,17 @@ describe('SessionActivityPanel', () => {
expect(screen.getByTestId('session-activity-panel')).toHaveAttribute('data-placement', 'rail')
expect(screen.getByTestId('session-activity-panel')).toHaveClass('my-4')
expect(screen.getByTestId('session-activity-panel')).toHaveClass('mr-4')
expect(screen.getByTestId('session-activity-panel')).toHaveClass('w-[340px]')
expect(screen.getByTestId('session-activity-panel')).toHaveClass('w-[360px]')
expect(screen.getByTestId('session-activity-panel')).toHaveClass('rounded-[24px]')
expect(screen.getByTestId('session-activity-panel')).toHaveClass('self-start')
expect(screen.getByTestId('session-activity-panel')).toHaveClass('max-h-[min(480px,calc(100vh-96px))]')
expect(screen.getByTestId('session-activity-panel')).toHaveClass('max-h-[min(620px,calc(100vh-72px))]')
expect(screen.getByTestId('session-activity-panel')).not.toHaveClass('h-[calc(100%-24px)]')
fireEvent.pointerDown(screen.getByRole('button', { name: 'Outside' }))
expect(onClose).not.toHaveBeenCalled()
})
it('caps each populated activity section independently', () => {
it('uses one polished scroll owner instead of nested section scrollbars', () => {
const taskRows = Array.from({ length: 12 }, (_, index) => ({
id: `task-${index + 1}`,
section: 'tasks' as const,
@ -455,15 +456,19 @@ describe('SessionActivityPanel', () => {
/>,
)
const scrollOwner = screen.getByTestId('session-activity-scroll')
const tasksSection = document.querySelector('section[aria-label="Tasks"]')
const teamSection = document.querySelector('section[aria-label="Team"]')
const backgroundSection = document.querySelector('section[aria-label="Background Tasks"]')
const subagentsSection = document.querySelector('section[aria-label="SubAgents"]')
expect(tasksSection?.querySelector('.max-h-44.overflow-y-auto')).toBeInTheDocument()
expect(teamSection?.querySelector('.max-h-36.overflow-y-auto')).toBeInTheDocument()
expect(backgroundSection?.querySelector('.max-h-40.overflow-y-auto')).toBeInTheDocument()
expect(subagentsSection?.querySelector('.max-h-40.overflow-y-auto')).toBeInTheDocument()
expect(scrollOwner).toHaveClass('overflow-y-auto')
expect(scrollOwner).toHaveClass('[scrollbar-width:auto]')
expect(scrollOwner).toHaveClass('[&::-webkit-scrollbar]:w-2.5')
expect(tasksSection?.querySelector('.overflow-y-auto')).not.toBeInTheDocument()
expect(teamSection?.querySelector('.overflow-y-auto')).not.toBeInTheDocument()
expect(backgroundSection?.querySelector('.overflow-y-auto')).not.toBeInTheDocument()
expect(subagentsSection?.querySelector('.overflow-y-auto')).not.toBeInTheDocument()
})
it('opens a SubAgent row when the row is openable', () => {

View File

@ -16,6 +16,20 @@ type SessionActivityPanelPlacement = 'overlay' | 'rail'
type TranslationFn = ReturnType<typeof useTranslation>
const ACTIVITY_SCROLLBAR_CLASS = [
'[scrollbar-width:auto]',
'[scrollbar-color:color-mix(in_srgb,var(--color-outline)_62%,transparent)_transparent]',
'[&::-webkit-scrollbar]:w-2.5',
'[&::-webkit-scrollbar-track]:bg-transparent',
'[&::-webkit-scrollbar-thumb]:rounded-full',
'[&::-webkit-scrollbar-thumb]:border-[3px]',
'[&::-webkit-scrollbar-thumb]:border-transparent',
'[&::-webkit-scrollbar-thumb]:bg-[color-mix(in_srgb,var(--color-outline)_68%,transparent)]',
'[&::-webkit-scrollbar-thumb]:bg-clip-content',
'[&::-webkit-scrollbar-thumb:hover]:border-2',
'[&::-webkit-scrollbar-thumb:hover]:bg-[color-mix(in_srgb,var(--color-outline)_84%,transparent)]',
].join(' ')
function fallbackStatusLabel(status: ActivityRow['status']): string {
const label = String(status).replace(/[_-]/g, ' ').replace(/\s+/g, ' ').trim()
if (!label) return ''
@ -63,22 +77,22 @@ function getSectionTitle(sectionId: ActivitySectionId, t: TranslationFn): string
}
function getSectionRowsClassName(sectionId: ActivitySectionId, rowCount: number): string {
const base = 'space-y-1'
const base = 'space-y-1.5'
if (rowCount === 0) return base
switch (sectionId) {
case 'tasks':
return `${base} max-h-44 overflow-y-auto overscroll-contain pr-1`
return base
case 'team':
return `${base} max-h-36 overflow-y-auto overscroll-contain pr-1`
return base
case 'backgroundTasks':
return `${base} max-h-40 overflow-y-auto overscroll-contain pr-1`
return base
case 'subagents':
return `${base} max-h-40 overflow-y-auto overscroll-contain pr-1`
return base
case 'sources':
return `${base} max-h-28 overflow-y-auto overscroll-contain pr-1`
return base
case 'output':
return `${base} max-h-28 overflow-y-auto overscroll-contain pr-1`
return base
}
}
@ -263,7 +277,7 @@ function ActivityRowView({
</>
)
const interactiveRowClassName =
'flex w-full items-center gap-2.5 rounded-xl px-3 py-2.5 text-left transition-[background-color,transform] duration-150 ease-out hover:bg-[var(--color-surface-hover)] active:translate-y-px focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)]'
'flex w-full items-center gap-3 rounded-xl px-3 py-3 text-left transition-[background-color,transform] duration-150 ease-out hover:bg-[var(--color-surface-hover)] active:translate-y-px focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)]'
if (row.section === 'team' && row.member && onOpenMember) {
return (
@ -306,7 +320,7 @@ function ActivityRowView({
}
return (
<div className="flex items-center gap-2.5 rounded-xl px-3 py-2.5">
<div className="flex items-center gap-3 rounded-xl px-3 py-3">
{content}
</div>
)
@ -427,8 +441,8 @@ export function SessionActivityPanel({
if (!open) return null
const className = placement === 'rail'
? 'my-4 ml-3 mr-4 flex max-h-[min(480px,calc(100vh-96px))] w-[340px] shrink-0 self-start flex-col overflow-hidden rounded-[24px] border border-[var(--color-border)] bg-[var(--color-surface)] shadow-[0_26px_80px_-48px_rgba(15,23,42,0.58),0_12px_30px_-22px_rgba(15,23,42,0.34),inset_0_1px_0_rgba(255,255,255,0.82)]'
: 'absolute right-4 top-4 z-40 flex max-h-[calc(100%-112px)] w-[min(340px,calc(100%-32px))] flex-col overflow-hidden rounded-[24px] border border-[var(--color-border)] bg-[var(--color-surface)] shadow-[0_26px_80px_-48px_rgba(15,23,42,0.58),0_12px_30px_-22px_rgba(15,23,42,0.34),inset_0_1px_0_rgba(255,255,255,0.82)]'
? 'my-4 ml-3 mr-4 flex max-h-[min(620px,calc(100vh-72px))] w-[360px] shrink-0 self-start flex-col overflow-hidden rounded-[24px] border border-[var(--color-border)] bg-[var(--color-surface)] shadow-[0_26px_80px_-48px_rgba(15,23,42,0.58),0_12px_30px_-22px_rgba(15,23,42,0.34),inset_0_1px_0_rgba(255,255,255,0.82)]'
: 'absolute right-4 top-4 z-40 flex max-h-[calc(100%-80px)] w-[min(360px,calc(100%-32px))] flex-col overflow-hidden rounded-[24px] border border-[var(--color-border)] bg-[var(--color-surface)] shadow-[0_26px_80px_-48px_rgba(15,23,42,0.58),0_12px_30px_-22px_rgba(15,23,42,0.34),inset_0_1px_0_rgba(255,255,255,0.82)]'
return (
<div
@ -451,7 +465,10 @@ export function SessionActivityPanel({
</button>
</div>
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto px-5 pb-5 pt-1">
<div
data-testid="session-activity-scroll"
className={`min-h-0 flex-1 space-y-4 overflow-y-auto overscroll-contain px-5 pb-5 pt-1 ${ACTIVITY_SCROLLBAR_CLASS}`}
>
{visibleSections.map((section, index) => {
const sectionTitle = getSectionTitle(section.id, t)

View File

@ -635,6 +635,83 @@ describe('ActiveSession task polling', () => {
})
})
it('auto-opens the activity panel when the current session first produces activity', async () => {
const sessionId = 'activity-auto-open-session'
const fetchSessionTasks = vi.fn().mockResolvedValue(undefined)
useCLITaskStore.setState({ fetchSessionTasks })
useSessionStore.setState({
sessions: [{
id: sessionId,
title: 'Auto Open Activity Session',
createdAt: '2026-05-07T00:00:00.000Z',
modifiedAt: '2026-05-07T00:00:00.000Z',
messageCount: 0,
projectPath: '/workspace/project',
workDir: '/workspace/project',
workDirExists: true,
}],
activeSessionId: sessionId,
isLoading: false,
error: null,
})
useTabStore.setState({
tabs: [{ sessionId, title: 'Auto Open Activity Session', type: 'session', status: 'idle' }],
activeTabId: sessionId,
})
useChatStore.setState({
sessions: {
[sessionId]: {
messages: [],
chatState: 'idle',
connectionState: 'connected',
streamingText: '',
streamingToolInput: '',
activeToolUseId: null,
activeToolName: null,
activeThinkingId: null,
pendingPermission: null,
pendingComputerUsePermission: null,
tokenUsage: { input_tokens: 0, output_tokens: 0 },
streamingResponseChars: 0,
elapsedSeconds: 0,
statusVerb: '',
slashCommands: [],
backgroundAgentTasks: {},
agentTaskNotifications: {},
elapsedTimer: null,
},
},
})
render(<ActiveSession />)
expect(screen.queryByTestId('session-activity-panel')).not.toBeInTheDocument()
expect(useActivityPanelStore.getState().isOpen(sessionId)).toBe(false)
act(() => {
useCLITaskStore.setState({
sessionId,
tasks: [{
id: 'task-1',
subject: 'Draft implementation plan',
description: 'Create the first activity row',
status: 'in_progress',
blocks: [],
blockedBy: [],
taskListId: sessionId,
}],
completedAndDismissed: false,
})
})
await waitFor(() => {
expect(useActivityPanelStore.getState().isOpen(sessionId)).toBe(true)
})
expect(screen.getByTestId('session-activity-panel')).toHaveAttribute('data-placement', 'rail')
expect(screen.getByText('Draft implementation plan')).toBeInTheDocument()
})
it('renders completed historical TodoWrite activity in the rail', () => {
const sessionId = 'activity-todowrite-history-session'

View File

@ -302,6 +302,7 @@ export function ActiveSession() {
const hasIncompleteTasks = cliTasks.some((task) => task.status !== 'completed')
const hasRunningTasks = cliTasks.some((task) => task.status === 'in_progress')
const isActivityPanelOpen = useActivityPanelStore((state) => activeTabId ? state.isOpen(activeTabId) : false)
const openActivityPanel = useActivityPanelStore((state) => state.open)
const closeActivityPanel = useActivityPanelStore((state) => state.close)
const dismissBackgroundTaskKeys = useActivityPanelStore((state) => state.dismissBackgroundTaskKeys)
const pruneDismissedBackgroundTaskKeys = useActivityPanelStore((state) => state.pruneDismissedBackgroundTaskKeys)
@ -336,6 +337,7 @@ export function ActiveSession() {
: undefined,
)
const terminalPanelHeight = useTerminalPanelStore((state) => state.height)
const activityVisibilityBySessionRef = useRef<Record<string, { hadAutoOpenActivity: boolean }>>({})
useEffect(() => {
if (activeTabId && !isMemberSession) {
@ -442,6 +444,31 @@ export function ActiveSession() {
trackedTaskSessionId,
])
const hasVisibleActivity = activityModel ? hasVisibleSessionActivity(activityModel) : false
const hasAutoOpenActivity = activityModel ? activityModel.badgeCount > 0 : false
useEffect(() => {
if (!activeTabId || isMemberSession || !isSessionTabState(activeTabId, activeTabType)) return
const state = activityVisibilityBySessionRef.current[activeTabId]
if (!state) {
activityVisibilityBySessionRef.current[activeTabId] = {
hadAutoOpenActivity: hasAutoOpenActivity,
}
return
}
if (!state.hadAutoOpenActivity && hasAutoOpenActivity && !isActivityPanelOpen) {
openActivityPanel(activeTabId)
}
state.hadAutoOpenActivity = hasAutoOpenActivity
}, [
activeTabId,
activeTabType,
hasAutoOpenActivity,
isActivityPanelOpen,
isMemberSession,
openActivityPanel,
])
useEffect(() => {
if (!activeTabId || !isActivityPanelOpen || hasVisibleActivity) return

View File

@ -61,6 +61,7 @@ describe('SubagentRunPage', () => {
afterEach(() => {
cleanup()
vi.useRealTimers()
vi.mocked(subagentsApi.getRunByTool).mockReset()
})
@ -72,16 +73,15 @@ describe('SubagentRunPage', () => {
render(<SubagentRunPage sourceSessionId="session-1" toolUseId="tool-1" title="Kuhn" />)
expect(await screen.findByText('Kuhn')).toBeInTheDocument()
expect(screen.getAllByText('Agent').length).toBeGreaterThan(0)
expect(screen.getByText('Agent: abc123')).toBeInTheDocument()
expect(screen.getAllByText('Explore repo').length).toBeGreaterThan(0)
expect(screen.getAllByText('Found layout seam').length).toBeGreaterThan(0)
expect(screen.getByText('Output: /tmp/result.md')).toBeInTheDocument()
expect(screen.getByText('Parent Agent Tool Call')).toBeInTheDocument()
expect(document.body).toHaveTextContent('"prompt": "Read files"')
expect(screen.queryByText('Parent Agent Tool Call')).not.toBeInTheDocument()
expect(document.body).not.toHaveTextContent('"prompt": "Read files"')
expect(screen.queryByText(/Dispatched an agent|派遣了一个代理/)).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: /Open run/ })).not.toBeInTheDocument()
const transcript = screen.getByTestId('subagent-transcript')
const transcript = screen.getByTestId('subagent-conversation')
expect(transcript).toHaveTextContent('Read files')
expect(transcript).toHaveTextContent('Finding')
expect(transcript).not.toHaveTextContent('assistant_text')
@ -107,8 +107,33 @@ describe('SubagentRunPage', () => {
render(<SubagentRunPage sourceSessionId="session-1" toolUseId="tool-1" title="SubAgent" />)
expect(await screen.findByText('No local transcript messages captured for this SubAgent.')).toBeInTheDocument()
expect(screen.getAllByText('Only summary available').length).toBeGreaterThan(0)
const conversation = await screen.findByTestId('subagent-conversation')
expect(conversation).toHaveTextContent('Only summary available')
expect(screen.queryByText('No local transcript messages captured for this SubAgent.')).not.toBeInTheDocument()
})
it('refreshes running SubAgent runs while the detail tab is open', async () => {
vi.mocked(subagentsApi.getRunByTool)
.mockResolvedValueOnce(subagentRun({
status: 'running',
messages: [],
prompt: 'Review streaming changes',
}))
.mockResolvedValueOnce(subagentRun({
status: 'completed',
messages: [],
prompt: 'Review streaming changes',
result: 'Streaming review complete',
}))
render(<SubagentRunPage sourceSessionId="session-1" toolUseId="tool-1" title="SubAgent" />)
expect(await screen.findByText('Running')).toBeInTheDocument()
expect(screen.getByTestId('subagent-conversation')).toHaveTextContent('Review streaming changes')
await waitFor(() => expect(subagentsApi.getRunByTool).toHaveBeenCalledTimes(2), { timeout: 2500 })
expect(await screen.findByText('Completed')).toBeInTheDocument()
expect(screen.getByTestId('subagent-conversation')).toHaveTextContent('Streaming review complete')
})
it('keeps the tab open on API errors', async () => {
@ -145,7 +170,6 @@ describe('SubagentRunPage', () => {
await second.promise
})
expect((await screen.findAllByText('Second result')).length).toBeGreaterThan(0)
expect(screen.getByText(/Second finding/)).toBeInTheDocument()
await act(async () => {
@ -163,14 +187,14 @@ describe('SubagentRunPage', () => {
await first.promise
})
expect(screen.getAllByText('Second result').length).toBeGreaterThan(0)
expect(screen.getByText(/Second finding/)).toBeInTheDocument()
expect(screen.queryByText('Stale first result')).not.toBeInTheDocument()
expect(screen.queryByText(/Stale finding/)).not.toBeInTheDocument()
})
it('keeps existing details visible when refresh fails', async () => {
vi.mocked(subagentsApi.getRunByTool)
.mockResolvedValueOnce(subagentRun({ summary: 'Initial result' }))
.mockResolvedValueOnce(subagentRun({ messages: [], summary: 'Initial result' }))
.mockRejectedValueOnce(new Error('refresh failed'))
render(<SubagentRunPage sourceSessionId="session-1" toolUseId="tool-1" title="SubAgent" />)

View File

@ -6,15 +6,13 @@ import {
type SubagentRunStatus,
} from '../api/subagents'
import { buildRenderModel, MessageBlock } from '../components/chat/MessageList'
import { ToolCallBlock } from '../components/chat/ToolCallBlock'
import { ToolCallGroup } from '../components/chat/ToolCallGroup'
import { useTranslation } from '../i18n'
import { mapHistoryMessagesToUiMessages } from '../stores/chatStore'
import type { AgentTaskNotification, UIMessage } from '../types/chat'
type ToolCall = Extract<UIMessage, { type: 'tool_use' }>
type ToolResult = Extract<UIMessage, { type: 'tool_result' }>
type TranslationFn = ReturnType<typeof useTranslation>
const LIVE_RUN_REFRESH_MS = 2000
export function SubagentRunPage({
sourceSessionId,
@ -54,6 +52,16 @@ export function SubagentRunPage({
void load({ resetData: true })
}, [load])
useEffect(() => {
if (data?.status !== 'running' || loading) return
const timer = window.setTimeout(() => {
void load()
}, LIVE_RUN_REFRESH_MS)
return () => window.clearTimeout(timer)
}, [data?.status, load, loading])
return (
<div className="flex min-h-0 flex-1 flex-col bg-[var(--color-surface)] text-[var(--color-text-primary)]">
<header className="flex shrink-0 items-start justify-between gap-4 border-b border-[var(--color-border)] px-5 py-3">
@ -96,8 +104,6 @@ export function SubagentRunPage({
function SubagentRunDetails({ data }: { data: SubagentRunResponse }) {
const t = useTranslation()
const agentToolCall = useMemo(() => buildAgentToolCall(data), [data])
const agentToolResult = useMemo(() => buildAgentToolResult(data, t), [data, t])
return (
<div className="mx-auto flex max-w-4xl flex-col gap-5">
@ -105,6 +111,12 @@ function SubagentRunDetails({ data }: { data: SubagentRunResponse }) {
<span>{t('subagentRun.source')}: {sourceLabel(data.source, t)}</span>
<span aria-hidden="true">/</span>
<span>{t('subagentRun.agent')}: {data.agentId ?? t('subagentRun.unknown')}</span>
{data.description ? (
<>
<span aria-hidden="true">/</span>
<span>{data.description}</span>
</>
) : null}
{data.taskId ? (
<>
<span aria-hidden="true">/</span>
@ -127,30 +139,17 @@ function SubagentRunDetails({ data }: { data: SubagentRunResponse }) {
) : null}
</div>
<section aria-label={t('subagentRun.parentToolInput')}>
<h2 className="mb-2 text-xs font-semibold uppercase tracking-normal text-[var(--color-text-tertiary)]">{t('subagentRun.parentToolCall')}</h2>
<ToolCallBlock
toolName="Agent"
input={agentToolCall.input}
result={agentToolResult}
isPending={data.status === 'running' && !agentToolResult}
defaultExpanded
/>
</section>
<TranscriptSection data={data} />
<ConversationSection data={data} />
</div>
)
}
const EMPTY_AGENT_TASK_NOTIFICATIONS: Record<string, AgentTaskNotification> = {}
function TranscriptSection({ data }: { data: SubagentRunResponse }) {
function ConversationSection({ data }: { data: SubagentRunResponse }) {
const t = useTranslation()
const transcriptMessages = useMemo(() => (
mapHistoryMessagesToUiMessages(data.messages, { includeTeammateMessages: true })
), [data.messages])
const renderModel = useMemo(() => buildRenderModel(transcriptMessages), [transcriptMessages])
const conversationMessages = useMemo(() => buildSubagentConversationMessages(data), [data])
const renderModel = useMemo(() => buildRenderModel(conversationMessages), [conversationMessages])
if (renderModel.renderItems.length === 0) {
return (
@ -171,7 +170,7 @@ function TranscriptSection({ data }: { data: SubagentRunResponse }) {
<span className="text-[11px] text-[var(--color-text-tertiary)]">{t('subagentRun.truncated')}</span>
) : null}
</div>
<div data-testid="subagent-transcript" className="space-y-3">
<div data-testid="subagent-conversation" className="space-y-3">
{renderModel.renderItems.map((item) => {
if (item.kind === 'tool_group') {
return (
@ -265,38 +264,44 @@ function timestampMs(value: string | undefined) {
return Number.isFinite(time) ? time : Date.now()
}
function buildAgentToolCall(data: SubagentRunResponse): ToolCall {
return {
id: `subagent-tool-${data.toolUseId}`,
type: 'tool_use',
toolName: 'Agent',
toolUseId: data.toolUseId,
input: {
...(data.description ? { description: data.description } : {}),
...(data.prompt ? { prompt: data.prompt } : {}),
},
timestamp: timestampMs(data.updatedAt),
function normalizedText(value: string | undefined) {
return (value ?? '').replace(/\s+/g, ' ').trim()
}
function hasPromptMessage(messages: UIMessage[], prompt: string) {
const normalizedPrompt = normalizedText(prompt)
if (!normalizedPrompt) return false
return messages.some((message) => (
message.type === 'user_text' &&
normalizedText(message.content) === normalizedPrompt
))
}
function buildSubagentConversationMessages(data: SubagentRunResponse): UIMessage[] {
const transcriptMessages = mapHistoryMessagesToUiMessages(data.messages, { includeTeammateMessages: true })
const messages = [...transcriptMessages]
const prompt = data.prompt?.trim()
const baseTimestamp = timestampMs(data.updatedAt)
if (prompt && !hasPromptMessage(transcriptMessages, prompt)) {
messages.unshift({
id: `subagent-prompt-${data.toolUseId}`,
type: 'user_text',
content: prompt,
timestamp: baseTimestamp - 1,
})
}
}
function buildAgentToolResult(data: SubagentRunResponse, t: TranslationFn): ToolResult | null {
const resultText = data.result || data.summary
if (!resultText && data.status === 'running') return null
if (!resultText && data.status === 'unknown') return null
return {
id: `subagent-result-${data.toolUseId}`,
type: 'tool_result',
toolUseId: data.toolUseId,
content: resultText || statusFallbackResult(data.status, t),
isError: data.status === 'failed' || data.status === 'stopped',
timestamp: timestampMs(data.updatedAt),
const resultText = (data.result || data.summary)?.trim()
if (transcriptMessages.length === 0 && resultText) {
messages.push({
id: `subagent-result-message-${data.toolUseId}`,
type: 'assistant_text',
content: resultText,
timestamp: baseTimestamp,
})
}
}
function statusFallbackResult(status: SubagentRunStatus, t: TranslationFn) {
if (status === 'completed') return t('subagentRun.result.completedFallback')
if (status === 'failed') return t('subagentRun.result.failedFallback')
if (status === 'stopped') return t('subagentRun.result.stoppedFallback')
return t('subagentRun.result.noneFallback')
return messages
}