bingocode 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/bin/bingo-win.cjs +34 -3
  2. package/desktop/README.md +30 -0
  3. package/desktop/bunfig.toml +1 -0
  4. package/desktop/index.html +17 -0
  5. package/desktop/package.json +55 -0
  6. package/desktop/pnpm-lock.yaml +3832 -0
  7. package/desktop/public/app-icon.jpg +0 -0
  8. package/desktop/public/fonts/inter-latin-ext.woff2 +0 -0
  9. package/desktop/public/fonts/inter-latin.woff2 +0 -0
  10. package/desktop/public/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
  11. package/desktop/public/fonts/jetbrains-mono-latin.woff2 +0 -0
  12. package/desktop/public/fonts/manrope-latin-ext.woff2 +0 -0
  13. package/desktop/public/fonts/manrope-latin.woff2 +0 -0
  14. package/desktop/public/fonts/material-symbols-outlined.woff2 +0 -0
  15. package/desktop/public/icons/bilibili.svg +1 -0
  16. package/desktop/public/icons/douyin.svg +1 -0
  17. package/desktop/public/icons/github.svg +3 -0
  18. package/desktop/public/icons/xiaohongshu.svg +1 -0
  19. package/desktop/scripts/build-macos-arm64.sh +270 -0
  20. package/desktop/scripts/build-sidecars.ts +183 -0
  21. package/desktop/scripts/build-windows-x64.ps1 +295 -0
  22. package/desktop/scripts/scan-missing-imports.ts +235 -0
  23. package/desktop/sidecars/claude-sidecar.ts +156 -0
  24. package/desktop/src/App.tsx +5 -0
  25. package/desktop/src/__tests__/agentsSettings.test.tsx +349 -0
  26. package/desktop/src/__tests__/pages.test.tsx +290 -0
  27. package/desktop/src/__tests__/skillsSettings.test.tsx +205 -0
  28. package/desktop/src/api/adapters.ts +12 -0
  29. package/desktop/src/api/agents.ts +36 -0
  30. package/desktop/src/api/cliTasks.ts +28 -0
  31. package/desktop/src/api/client.ts +63 -0
  32. package/desktop/src/api/computerUse.ts +76 -0
  33. package/desktop/src/api/filesystem.ts +30 -0
  34. package/desktop/src/api/hahaOAuth.ts +38 -0
  35. package/desktop/src/api/models.ts +28 -0
  36. package/desktop/src/api/providers.ts +63 -0
  37. package/desktop/src/api/search.ts +29 -0
  38. package/desktop/src/api/sessions.ts +56 -0
  39. package/desktop/src/api/settings.ts +20 -0
  40. package/desktop/src/api/skills.ts +19 -0
  41. package/desktop/src/api/tasks.ts +36 -0
  42. package/desktop/src/api/teams.ts +44 -0
  43. package/desktop/src/api/websocket.ts +164 -0
  44. package/desktop/src/components/chat/AskUserQuestion.tsx +268 -0
  45. package/desktop/src/components/chat/AssistantMessage.tsx +29 -0
  46. package/desktop/src/components/chat/AttachmentGallery.tsx +113 -0
  47. package/desktop/src/components/chat/ChatInput.tsx +622 -0
  48. package/desktop/src/components/chat/CodeViewer.tsx +161 -0
  49. package/desktop/src/components/chat/ComputerUsePermissionModal.test.tsx +174 -0
  50. package/desktop/src/components/chat/ComputerUsePermissionModal.tsx +311 -0
  51. package/desktop/src/components/chat/DiffViewer.tsx +157 -0
  52. package/desktop/src/components/chat/FileSearchMenu.tsx +198 -0
  53. package/desktop/src/components/chat/ImageGalleryModal.tsx +91 -0
  54. package/desktop/src/components/chat/InlineImageGallery.tsx +106 -0
  55. package/desktop/src/components/chat/InlineTaskSummary.tsx +60 -0
  56. package/desktop/src/components/chat/MermaidRenderer.test.tsx +98 -0
  57. package/desktop/src/components/chat/MermaidRenderer.tsx +361 -0
  58. package/desktop/src/components/chat/MessageActionBar.tsx +27 -0
  59. package/desktop/src/components/chat/MessageList.test.tsx +313 -0
  60. package/desktop/src/components/chat/MessageList.tsx +249 -0
  61. package/desktop/src/components/chat/PermissionDialog.tsx +262 -0
  62. package/desktop/src/components/chat/SessionTaskBar.test.tsx +99 -0
  63. package/desktop/src/components/chat/SessionTaskBar.tsx +159 -0
  64. package/desktop/src/components/chat/StreamingIndicator.tsx +41 -0
  65. package/desktop/src/components/chat/TerminalChrome.tsx +35 -0
  66. package/desktop/src/components/chat/ThinkingBlock.tsx +87 -0
  67. package/desktop/src/components/chat/ToolCallBlock.tsx +247 -0
  68. package/desktop/src/components/chat/ToolCallGroup.tsx +617 -0
  69. package/desktop/src/components/chat/ToolResultBlock.tsx +107 -0
  70. package/desktop/src/components/chat/UserMessage.tsx +38 -0
  71. package/desktop/src/components/chat/chatBlocks.test.tsx +136 -0
  72. package/desktop/src/components/chat/clipboard.ts +25 -0
  73. package/desktop/src/components/chat/composerUtils.test.ts +55 -0
  74. package/desktop/src/components/chat/composerUtils.ts +149 -0
  75. package/desktop/src/components/controls/ModelSelector.tsx +156 -0
  76. package/desktop/src/components/controls/PermissionModeSelector.tsx +229 -0
  77. package/desktop/src/components/layout/AppShell.tsx +107 -0
  78. package/desktop/src/components/layout/ContentRouter.tsx +27 -0
  79. package/desktop/src/components/layout/ProjectFilter.tsx +126 -0
  80. package/desktop/src/components/layout/Sidebar.test.tsx +158 -0
  81. package/desktop/src/components/layout/Sidebar.tsx +384 -0
  82. package/desktop/src/components/layout/StatusBar.tsx +31 -0
  83. package/desktop/src/components/layout/TabBar.test.tsx +136 -0
  84. package/desktop/src/components/layout/TabBar.tsx +318 -0
  85. package/desktop/src/components/layout/TitleBar.tsx +96 -0
  86. package/desktop/src/components/layout/WindowControls.test.tsx +69 -0
  87. package/desktop/src/components/layout/WindowControls.tsx +89 -0
  88. package/desktop/src/components/markdown/MarkdownRenderer.test.tsx +100 -0
  89. package/desktop/src/components/markdown/MarkdownRenderer.tsx +229 -0
  90. package/desktop/src/components/settings/ClaudeOfficialLogin.tsx +107 -0
  91. package/desktop/src/components/shared/Button.tsx +63 -0
  92. package/desktop/src/components/shared/CopyButton.tsx +58 -0
  93. package/desktop/src/components/shared/DirectoryPicker.tsx +316 -0
  94. package/desktop/src/components/shared/Dropdown.tsx +91 -0
  95. package/desktop/src/components/shared/Input.tsx +38 -0
  96. package/desktop/src/components/shared/Modal.tsx +65 -0
  97. package/desktop/src/components/shared/ProjectContextChip.tsx +30 -0
  98. package/desktop/src/components/shared/Spinner.tsx +30 -0
  99. package/desktop/src/components/shared/Textarea.tsx +38 -0
  100. package/desktop/src/components/shared/Toast.tsx +47 -0
  101. package/desktop/src/components/shared/UpdateChecker.tsx +90 -0
  102. package/desktop/src/components/skills/SkillDetail.test.tsx +89 -0
  103. package/desktop/src/components/skills/SkillDetail.tsx +403 -0
  104. package/desktop/src/components/skills/SkillList.tsx +254 -0
  105. package/desktop/src/components/tasks/DayOfWeekPicker.tsx +57 -0
  106. package/desktop/src/components/tasks/NewTaskModal.tsx +407 -0
  107. package/desktop/src/components/tasks/PromptEditor.tsx +74 -0
  108. package/desktop/src/components/tasks/TaskEmptyState.tsx +30 -0
  109. package/desktop/src/components/tasks/TaskList.tsx +46 -0
  110. package/desktop/src/components/tasks/TaskRow.tsx +253 -0
  111. package/desktop/src/components/tasks/TaskRunsPanel.tsx +195 -0
  112. package/desktop/src/components/teams/TeamStatusBar.tsx +147 -0
  113. package/desktop/src/config/providerPresets.ts +78 -0
  114. package/desktop/src/config/spinnerVerbs.ts +193 -0
  115. package/desktop/src/hooks/useKeyboardShortcuts.ts +60 -0
  116. package/desktop/src/i18n/index.ts +54 -0
  117. package/desktop/src/i18n/locales/en.ts +670 -0
  118. package/desktop/src/i18n/locales/zh.ts +670 -0
  119. package/desktop/src/lib/__tests__/cronDescribe.test.ts +93 -0
  120. package/desktop/src/lib/cronDescribe.ts +188 -0
  121. package/desktop/src/lib/desktopRuntime.ts +54 -0
  122. package/desktop/src/lib/parseRunOutput.ts +79 -0
  123. package/desktop/src/main.tsx +13 -0
  124. package/desktop/src/mocks/data.ts +202 -0
  125. package/desktop/src/pages/ActiveSession.test.tsx +181 -0
  126. package/desktop/src/pages/ActiveSession.tsx +219 -0
  127. package/desktop/src/pages/AdapterSettings.tsx +375 -0
  128. package/desktop/src/pages/AgentTeams.tsx +200 -0
  129. package/desktop/src/pages/ComputerUseSettings.tsx +420 -0
  130. package/desktop/src/pages/EmptySession.tsx +518 -0
  131. package/desktop/src/pages/NewTaskModal.tsx +346 -0
  132. package/desktop/src/pages/ScheduledTasks.tsx +66 -0
  133. package/desktop/src/pages/ScheduledTasksEmpty.tsx +152 -0
  134. package/desktop/src/pages/ScheduledTasksList.tsx +416 -0
  135. package/desktop/src/pages/SessionControls.tsx +460 -0
  136. package/desktop/src/pages/Settings.tsx +1448 -0
  137. package/desktop/src/pages/ToolInspection.tsx +235 -0
  138. package/desktop/src/stores/adapterStore.ts +106 -0
  139. package/desktop/src/stores/agentStore.ts +34 -0
  140. package/desktop/src/stores/chatStore.test.ts +505 -0
  141. package/desktop/src/stores/chatStore.ts +850 -0
  142. package/desktop/src/stores/cliTaskStore.ts +152 -0
  143. package/desktop/src/stores/hahaOAuthStore.test.ts +77 -0
  144. package/desktop/src/stores/hahaOAuthStore.ts +97 -0
  145. package/desktop/src/stores/providerStore.ts +101 -0
  146. package/desktop/src/stores/sessionStore.test.ts +63 -0
  147. package/desktop/src/stores/sessionStore.ts +102 -0
  148. package/desktop/src/stores/settingsStore.ts +120 -0
  149. package/desktop/src/stores/skillStore.ts +51 -0
  150. package/desktop/src/stores/tabStore.ts +169 -0
  151. package/desktop/src/stores/taskStore.ts +68 -0
  152. package/desktop/src/stores/teamStore.ts +344 -0
  153. package/desktop/src/stores/uiStore.ts +100 -0
  154. package/desktop/src/stores/updateStore.test.ts +71 -0
  155. package/desktop/src/stores/updateStore.ts +221 -0
  156. package/desktop/src/theme/globals.css +465 -0
  157. package/desktop/src/types/adapter.ts +33 -0
  158. package/desktop/src/types/chat.ts +152 -0
  159. package/desktop/src/types/cliTask.ts +24 -0
  160. package/desktop/src/types/provider.ts +62 -0
  161. package/desktop/src/types/session.ts +27 -0
  162. package/desktop/src/types/settings.ts +22 -0
  163. package/desktop/src/types/skill.ts +38 -0
  164. package/desktop/src/types/task.ts +56 -0
  165. package/desktop/src/types/team.ts +38 -0
  166. package/desktop/src-tauri/Cargo.lock +5549 -0
  167. package/desktop/src-tauri/Cargo.toml +20 -0
  168. package/desktop/src-tauri/app-icon.svg +13 -0
  169. package/desktop/src-tauri/build.rs +3 -0
  170. package/desktop/src-tauri/capabilities/default.json +106 -0
  171. package/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  172. package/desktop/src-tauri/icons/android/values/ic_launcher_background.xml +4 -0
  173. package/desktop/src-tauri/icons/icon.icns +0 -0
  174. package/desktop/src-tauri/icons/icon.ico +0 -0
  175. package/desktop/src-tauri/src/lib.rs +408 -0
  176. package/desktop/src-tauri/src/main.rs +6 -0
  177. package/desktop/src-tauri/tauri.conf.json +78 -0
  178. package/desktop/src-tauri/tauri.macos.conf.json +18 -0
  179. package/desktop/src-tauri/tauri.release-ci.json +5 -0
  180. package/desktop/src-tauri/tauri.windows.conf.json +16 -0
  181. package/desktop/src-tauri/windows-installer-hooks.nsh +17 -0
  182. package/desktop/tsconfig.json +25 -0
  183. package/desktop/vite.config.ts +26 -0
  184. package/desktop/vitest.config.ts +18 -0
  185. package/package.json +1 -1
  186. package/src/commands/desktop/desktop.tsx +9 -0
  187. package/src/commands/desktop/index.ts +26 -0
@@ -0,0 +1,617 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { ToolCallBlock } from './ToolCallBlock'
3
+ import { MarkdownRenderer } from '../markdown/MarkdownRenderer'
4
+ import { Modal } from '../shared/Modal'
5
+ import { useTranslation } from '../../i18n'
6
+ import type { TranslationKey } from '../../i18n'
7
+ import type { AgentTaskNotification, UIMessage } from '../../types/chat'
8
+ import { AGENT_LIFECYCLE_TYPES } from '../../types/team'
9
+
10
+ type ToolCall = Extract<UIMessage, { type: 'tool_use' }>
11
+ type ToolResult = Extract<UIMessage, { type: 'tool_result' }>
12
+
13
+ type Props = {
14
+ toolCalls: ToolCall[]
15
+ resultMap: Map<string, ToolResult>
16
+ childToolCallsByParent: Map<string, ToolCall[]>
17
+ agentTaskNotifications: Record<string, AgentTaskNotification>
18
+ /** When true, the last tool is still executing — show expanded */
19
+ isStreaming?: boolean
20
+ }
21
+
22
+ const TOOL_VERBS: Record<string, (count: number, t: (key: TranslationKey, params?: Record<string, string | number>) => string) => string> = {
23
+ Read: (n, t) => n === 1 ? t('toolGroup.readOne') : t('toolGroup.readMany', { count: n }),
24
+ Write: (n, t) => n === 1 ? t('toolGroup.createdOne') : t('toolGroup.createdMany', { count: n }),
25
+ Edit: (n, t) => n === 1 ? t('toolGroup.editedOne') : t('toolGroup.editedMany', { count: n }),
26
+ Bash: (n, t) => n === 1 ? t('toolGroup.ranOne') : t('toolGroup.ranMany', { count: n }),
27
+ Glob: (_n, t) => t('toolGroup.foundFiles'),
28
+ Grep: (n, t) => n === 1 ? t('toolGroup.searchedOne') : t('toolGroup.searchedMany', { count: n }),
29
+ Agent: (n, t) => n === 1 ? t('toolGroup.agentOne') : t('toolGroup.agentMany', { count: n }),
30
+ WebSearch: (_n, t) => t('toolGroup.searchedWeb'),
31
+ WebFetch: (n, t) => n === 1 ? t('toolGroup.fetchedOne') : t('toolGroup.fetchedMany', { count: n }),
32
+ }
33
+
34
+ function generateSummary(toolCalls: ToolCall[], t: (key: TranslationKey, params?: Record<string, string | number>) => string): string {
35
+ const counts = new Map<string, number>()
36
+ for (const tc of toolCalls) {
37
+ counts.set(tc.toolName, (counts.get(tc.toolName) ?? 0) + 1)
38
+ }
39
+
40
+ const parts: string[] = []
41
+ for (const [name, count] of counts) {
42
+ const verbFn = TOOL_VERBS[name]
43
+ parts.push(verbFn ? verbFn(count, t) : `${name} (${count})`)
44
+ }
45
+
46
+ return parts.join(', ')
47
+ }
48
+
49
+ function groupHasErrors(toolCalls: ToolCall[], resultMap: Map<string, ToolResult>): boolean {
50
+ return toolCalls.some((tc) => {
51
+ const result = resultMap.get(tc.toolUseId)
52
+ return result?.isError
53
+ })
54
+ }
55
+
56
+ export function ToolCallGroup({
57
+ toolCalls,
58
+ resultMap,
59
+ childToolCallsByParent,
60
+ agentTaskNotifications,
61
+ isStreaming,
62
+ }: Props) {
63
+ const allAgents = toolCalls.every((toolCall) => toolCall.toolName === 'Agent')
64
+
65
+ if (allAgents) {
66
+ return (
67
+ <AgentToolGroup
68
+ toolCalls={toolCalls}
69
+ resultMap={resultMap}
70
+ childToolCallsByParent={childToolCallsByParent}
71
+ agentTaskNotifications={agentTaskNotifications}
72
+ isStreaming={isStreaming}
73
+ />
74
+ )
75
+ }
76
+
77
+ // Single tool call — render directly without group wrapper
78
+ if (toolCalls.length === 1) {
79
+ const tc = toolCalls[0]!
80
+ return (
81
+ <ToolCallTree
82
+ toolCall={tc}
83
+ resultMap={resultMap}
84
+ childToolCallsByParent={childToolCallsByParent}
85
+ />
86
+ )
87
+ }
88
+
89
+ return (
90
+ <ToolCallGroupMulti
91
+ toolCalls={toolCalls}
92
+ resultMap={resultMap}
93
+ childToolCallsByParent={childToolCallsByParent}
94
+ agentTaskNotifications={agentTaskNotifications}
95
+ isStreaming={isStreaming}
96
+ />
97
+ )
98
+ }
99
+
100
+ function AgentToolGroup({
101
+ toolCalls,
102
+ resultMap,
103
+ childToolCallsByParent,
104
+ agentTaskNotifications,
105
+ isStreaming,
106
+ }: Props) {
107
+ const [expanded, setExpanded] = useState(true)
108
+ const t = useTranslation()
109
+ const statuses = toolCalls.map((toolCall) =>
110
+ getAgentStatus({
111
+ hasResult: resultMap.has(toolCall.toolUseId),
112
+ isError: !!resultMap.get(toolCall.toolUseId)?.isError,
113
+ isLaunchResult: isAgentLaunchResult(resultMap.get(toolCall.toolUseId)?.content),
114
+ isStreaming: !!isStreaming && !resultMap.has(toolCall.toolUseId),
115
+ childCount: (childToolCallsByParent.get(toolCall.toolUseId) ?? []).length,
116
+ taskStatus: agentTaskNotifications[toolCall.toolUseId]?.status,
117
+ }),
118
+ )
119
+ const isAnyRunning = statuses.some((status) => status === 'running' || status === 'starting')
120
+ const errorPresent = statuses.some((status) => status === 'failed')
121
+ const allComplete = statuses.every((status) => status === 'done')
122
+ const anyStopped = statuses.some((status) => status === 'stopped')
123
+
124
+ useEffect(() => {
125
+ if (isStreaming) {
126
+ setExpanded(true)
127
+ }
128
+ }, [isStreaming])
129
+
130
+ return (
131
+ <div className="mb-2 ml-10">
132
+ <button
133
+ type="button"
134
+ onClick={() => setExpanded((value) => !value)}
135
+ 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)]"
136
+ >
137
+ <span className="material-symbols-outlined text-[14px] text-[var(--color-outline)]">
138
+ {expanded ? 'expand_less' : 'expand_more'}
139
+ </span>
140
+ <span className="flex-1 truncate text-[12px] text-[var(--color-text-secondary)]">
141
+ {toolCalls.length === 1 ? t('toolGroup.agentOne') : t('toolGroup.agentMany', { count: toolCalls.length })}
142
+ </span>
143
+ {isAnyRunning && (
144
+ <span className="rounded-full bg-[var(--color-warning)]/12 px-2 py-0.5 text-[10px] font-semibold text-[var(--color-warning)]">
145
+ {t('agentStatus.running')}
146
+ </span>
147
+ )}
148
+ {!isAnyRunning && errorPresent && (
149
+ <span className="material-symbols-outlined text-[14px] text-[var(--color-error)]">error</span>
150
+ )}
151
+ {!isAnyRunning && !errorPresent && allComplete && (
152
+ <span className="material-symbols-outlined text-[14px] text-[var(--color-success)]">check_circle</span>
153
+ )}
154
+ {!isAnyRunning && !errorPresent && !allComplete && !anyStopped && (
155
+ <span className="material-symbols-outlined text-[14px] text-[var(--color-outline)]">pending</span>
156
+ )}
157
+ {!isAnyRunning && !errorPresent && !allComplete && anyStopped && (
158
+ <span className="material-symbols-outlined text-[14px] text-[var(--color-outline)]">stop_circle</span>
159
+ )}
160
+ </button>
161
+
162
+ {expanded && (
163
+ <div className="relative mt-3 pl-5">
164
+ <div className="absolute bottom-6 left-[11px] top-4 w-px rounded-full bg-[var(--color-border)]/45" />
165
+ <div className="space-y-2">
166
+ {toolCalls.map((toolCall) => (
167
+ <div key={toolCall.id} className="relative pl-7">
168
+ <div className="absolute left-0 top-1/2 -translate-y-1/2">
169
+ <div className="absolute left-[11px] top-1/2 h-px w-4 -translate-y-1/2 bg-[var(--color-border)]/45" />
170
+ <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)]" />
171
+ </div>
172
+ <AgentCallCard
173
+ toolCall={toolCall}
174
+ resultMap={resultMap}
175
+ childToolCallsByParent={childToolCallsByParent}
176
+ agentTaskNotification={agentTaskNotifications[toolCall.toolUseId]}
177
+ isStreaming={isStreaming && !resultMap.has(toolCall.toolUseId)}
178
+ />
179
+ </div>
180
+ ))}
181
+ </div>
182
+ </div>
183
+ )}
184
+ </div>
185
+ )
186
+ }
187
+
188
+ /** Separated so the useState hook is never called conditionally. */
189
+ function ToolCallGroupMulti({ toolCalls, resultMap, childToolCallsByParent, isStreaming }: Props) {
190
+ const [expanded, setExpanded] = useState(false)
191
+ const t = useTranslation()
192
+ const summary = generateSummary(toolCalls, t)
193
+ const errorPresent = groupHasErrors(toolCalls, resultMap)
194
+ const allComplete = toolCalls.every((tc) => resultMap.has(tc.toolUseId))
195
+ const hasNestedToolCalls = toolCalls.some((tc) => (childToolCallsByParent.get(tc.toolUseId)?.length ?? 0) > 0)
196
+
197
+ useEffect(() => {
198
+ if (isStreaming || hasNestedToolCalls) {
199
+ setExpanded(true)
200
+ }
201
+ }, [hasNestedToolCalls, isStreaming])
202
+
203
+ return (
204
+ <div className="mb-2 ml-10">
205
+ <button
206
+ type="button"
207
+ onClick={() => setExpanded((v) => !v)}
208
+ 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)]"
209
+ >
210
+ <span className="material-symbols-outlined text-[14px] text-[var(--color-outline)]">
211
+ {expanded ? 'expand_less' : 'expand_more'}
212
+ </span>
213
+ <span className="flex-1 truncate text-[12px] text-[var(--color-text-secondary)]">
214
+ {summary}
215
+ </span>
216
+ {!isStreaming && allComplete && !errorPresent && (
217
+ <span className="material-symbols-outlined text-[14px] text-[var(--color-success)]">check_circle</span>
218
+ )}
219
+ {!isStreaming && errorPresent && (
220
+ <span className="material-symbols-outlined text-[14px] text-[var(--color-error)]">error</span>
221
+ )}
222
+ {!isStreaming && !allComplete && !errorPresent && (
223
+ <span className="material-symbols-outlined text-[14px] text-[var(--color-outline)]">pending</span>
224
+ )}
225
+ {isStreaming && (
226
+ <span className="h-1.5 w-1.5 rounded-full bg-[var(--color-brand)] animate-pulse-dot" />
227
+ )}
228
+ </button>
229
+
230
+ {expanded && (
231
+ <div className="mt-1.5 space-y-1">
232
+ {toolCalls.map((tc) => {
233
+ return (
234
+ <ToolCallTree
235
+ key={tc.id}
236
+ toolCall={tc}
237
+ resultMap={resultMap}
238
+ childToolCallsByParent={childToolCallsByParent}
239
+ compact
240
+ />
241
+ )
242
+ })}
243
+ </div>
244
+ )}
245
+ </div>
246
+ )
247
+ }
248
+
249
+ function AgentCallCard({
250
+ toolCall,
251
+ resultMap,
252
+ childToolCallsByParent,
253
+ agentTaskNotification,
254
+ isStreaming = false,
255
+ }: {
256
+ toolCall: ToolCall
257
+ resultMap: Map<string, ToolResult>
258
+ childToolCallsByParent: Map<string, ToolCall[]>
259
+ agentTaskNotification?: AgentTaskNotification
260
+ isStreaming?: boolean
261
+ }) {
262
+ const [expanded, setExpanded] = useState(false)
263
+ const [previewOpen, setPreviewOpen] = useState(false)
264
+ const t = useTranslation()
265
+ const input = toolCall.input && typeof toolCall.input === 'object'
266
+ ? toolCall.input as Record<string, unknown>
267
+ : {}
268
+ const result = resultMap.get(toolCall.toolUseId)
269
+ const childToolCalls = childToolCallsByParent.get(toolCall.toolUseId) ?? []
270
+ const isLaunchResult = isAgentLaunchResult(result?.content)
271
+ const recentToolCalls = childToolCalls.slice(-2)
272
+ const status = getAgentStatus({
273
+ hasResult: !!result,
274
+ isError: !!result?.isError,
275
+ isLaunchResult,
276
+ isStreaming,
277
+ childCount: childToolCalls.length,
278
+ taskStatus: agentTaskNotification?.status,
279
+ })
280
+ const statusClassName = getAgentStatusClassName(status)
281
+ const statusLabel = getAgentStatusLabel(status, t)
282
+ const taskSummary = agentTaskNotification?.summary?.trim() || ''
283
+ const errorText =
284
+ status === 'failed'
285
+ ? taskSummary || (result?.isError ? getAgentErrorSummary(result.content) : '')
286
+ : result?.isError
287
+ ? getAgentErrorSummary(result.content)
288
+ : ''
289
+ const fullOutputText =
290
+ result && !result.isError && !isLaunchResult && !isAgentLifecycleResult(result.content)
291
+ ? extractTextContent(result.content).trim()
292
+ : ''
293
+ const previewText = fullOutputText || (status === 'done' || status === 'stopped' ? taskSummary : '')
294
+ const outputSummary = previewText ? getAgentOutputSummary(previewText) : ''
295
+ const description = typeof input.description === 'string' ? input.description : ''
296
+
297
+ return (
298
+ <div className="overflow-hidden rounded-lg border border-[var(--color-border)]/50 bg-[var(--color-surface-container-lowest)]">
299
+ <div className="flex w-full items-center gap-3 px-4 py-3 text-left transition-colors hover:bg-[var(--color-surface-hover)]/50">
300
+ <span className="material-symbols-outlined text-[18px] text-[var(--color-outline)]">smart_toy</span>
301
+ <div className="min-w-0 flex-1">
302
+ <div className="flex items-center gap-2">
303
+ <span className="text-[13px] font-semibold text-[var(--color-text-primary)]">Agent</span>
304
+ {description && (
305
+ <span className="truncate text-[12px] text-[var(--color-text-secondary)]">
306
+ {description}
307
+ </span>
308
+ )}
309
+ </div>
310
+ {!expanded && outputSummary && (
311
+ <div className="mt-1 line-clamp-2 text-[11px] text-[var(--color-text-tertiary)]">
312
+ {outputSummary}
313
+ </div>
314
+ )}
315
+ {!expanded && !outputSummary && recentToolCalls.length > 0 && (
316
+ <div className="mt-1 space-y-1">
317
+ {recentToolCalls.map((recentToolCall) => (
318
+ <div
319
+ key={recentToolCall.id}
320
+ className="truncate text-[11px] text-[var(--color-text-tertiary)]"
321
+ >
322
+ {formatRecentToolUseSummary(recentToolCall, resultMap)}
323
+ </div>
324
+ ))}
325
+ </div>
326
+ )}
327
+ {!expanded && !outputSummary && !recentToolCalls.length && errorText && (
328
+ <div className="mt-1 truncate text-[11px] text-[var(--color-error)]">
329
+ {errorText}
330
+ </div>
331
+ )}
332
+ </div>
333
+ {outputSummary && (
334
+ <button
335
+ type="button"
336
+ onClick={(event) => {
337
+ event.stopPropagation()
338
+ setPreviewOpen(true)
339
+ }}
340
+ 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)]"
341
+ >
342
+ {t('agentStatus.viewResult')}
343
+ </button>
344
+ )}
345
+ <span className={`rounded-full px-2 py-0.5 text-[10px] font-semibold ${statusClassName}`}>
346
+ {statusLabel}
347
+ </span>
348
+ <button
349
+ type="button"
350
+ onClick={() => setExpanded((value) => !value)}
351
+ 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)]"
352
+ aria-label={expanded ? 'Collapse agent' : 'Expand agent'}
353
+ >
354
+ <span className="material-symbols-outlined text-[16px]">
355
+ {expanded ? 'expand_less' : 'expand_more'}
356
+ </span>
357
+ </button>
358
+ </div>
359
+
360
+ {expanded && (
361
+ <div className="border-t border-[var(--color-border)]/60 px-3 py-3">
362
+ {errorText && (
363
+ <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)]">
364
+ {errorText}
365
+ </div>
366
+ )}
367
+ {childToolCalls.length > 0 ? (
368
+ <div className="space-y-1">
369
+ {childToolCalls.map((childToolCall) => (
370
+ <ToolCallTree
371
+ key={childToolCall.id}
372
+ toolCall={childToolCall}
373
+ resultMap={resultMap}
374
+ childToolCallsByParent={childToolCallsByParent}
375
+ compact
376
+ />
377
+ ))}
378
+ </div>
379
+ ) : outputSummary ? (
380
+ <div className="rounded-lg border border-[var(--color-border)]/60 bg-[var(--color-surface)] px-3 py-3">
381
+ <div className="line-clamp-3 text-[11px] leading-[1.55] text-[var(--color-text-secondary)]">
382
+ {outputSummary}
383
+ </div>
384
+ <div className="mt-3 flex justify-end">
385
+ <span className="text-[10px] text-[var(--color-text-tertiary)]">
386
+ {t('agentStatus.viewResult')}
387
+ </span>
388
+ </div>
389
+ </div>
390
+ ) : (
391
+ <div className="text-[11px] text-[var(--color-text-tertiary)]">
392
+ {status === 'starting' ? t('agentStatus.starting') : t('agentStatus.noActivity')}
393
+ </div>
394
+ )}
395
+ </div>
396
+ )}
397
+ <Modal
398
+ open={previewOpen}
399
+ onClose={() => setPreviewOpen(false)}
400
+ title={description || t('agentStatus.resultTitle')}
401
+ width={900}
402
+ >
403
+ <div className="max-h-[70vh] overflow-y-auto">
404
+ <MarkdownRenderer content={previewText || errorText} />
405
+ </div>
406
+ </Modal>
407
+ </div>
408
+ )
409
+ }
410
+
411
+ function ToolCallTree({
412
+ toolCall,
413
+ resultMap,
414
+ childToolCallsByParent,
415
+ compact = false,
416
+ }: {
417
+ toolCall: ToolCall
418
+ resultMap: Map<string, ToolResult>
419
+ childToolCallsByParent: Map<string, ToolCall[]>
420
+ compact?: boolean
421
+ }) {
422
+ const result = resultMap.get(toolCall.toolUseId)
423
+ const childToolCalls = childToolCallsByParent.get(toolCall.toolUseId) ?? []
424
+
425
+ return (
426
+ <div className={compact ? 'space-y-1' : ''}>
427
+ <ToolCallBlock
428
+ toolName={toolCall.toolName}
429
+ input={toolCall.input}
430
+ result={result ? { content: result.content, isError: result.isError } : null}
431
+ compact={compact}
432
+ />
433
+ {childToolCalls.length > 0 && (
434
+ <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'}>
435
+ <div className="space-y-1">
436
+ {childToolCalls.map((childToolCall) => (
437
+ <ToolCallTree
438
+ key={childToolCall.id}
439
+ toolCall={childToolCall}
440
+ resultMap={resultMap}
441
+ childToolCallsByParent={childToolCallsByParent}
442
+ compact
443
+ />
444
+ ))}
445
+ </div>
446
+ </div>
447
+ )}
448
+ </div>
449
+ )
450
+ }
451
+
452
+ type AgentStatus = 'starting' | 'running' | 'done' | 'failed' | 'stopped'
453
+ type AgentTaskStatus = AgentTaskNotification['status']
454
+
455
+ function getAgentStatus({
456
+ hasResult,
457
+ isError,
458
+ isLaunchResult,
459
+ isStreaming,
460
+ childCount,
461
+ taskStatus,
462
+ }: {
463
+ hasResult: boolean
464
+ isError: boolean
465
+ isLaunchResult: boolean
466
+ isStreaming: boolean
467
+ childCount: number
468
+ taskStatus?: AgentTaskStatus
469
+ }): AgentStatus {
470
+ if (taskStatus === 'failed') return 'failed'
471
+ if (taskStatus === 'stopped') return 'stopped'
472
+ if (taskStatus === 'completed') return 'done'
473
+ if (hasResult && isError && !isLaunchResult) return 'failed'
474
+ if (hasResult && !isLaunchResult) return 'done'
475
+ if (isStreaming || childCount > 0 || isLaunchResult) return 'running'
476
+ return 'starting'
477
+ }
478
+
479
+ function getAgentStatusLabel(
480
+ status: AgentStatus,
481
+ t: (key: TranslationKey, params?: Record<string, string | number>) => string,
482
+ ): string {
483
+ switch (status) {
484
+ case 'failed':
485
+ return t('agentStatus.failed')
486
+ case 'stopped':
487
+ return t('agentStatus.stopped')
488
+ case 'done':
489
+ return t('agentStatus.done')
490
+ case 'running':
491
+ return t('agentStatus.running')
492
+ case 'starting':
493
+ default:
494
+ return t('agentStatus.starting')
495
+ }
496
+ }
497
+
498
+ function getAgentStatusClassName(status: AgentStatus): string {
499
+ switch (status) {
500
+ case 'failed':
501
+ return 'bg-[var(--color-error)]/10 text-[var(--color-error)]'
502
+ case 'stopped':
503
+ return 'bg-[var(--color-surface-container-high)] text-[var(--color-text-secondary)]'
504
+ case 'done':
505
+ return 'bg-[var(--color-success)]/10 text-[var(--color-success)]'
506
+ case 'running':
507
+ return 'bg-[var(--color-warning)]/10 text-[var(--color-warning)]'
508
+ case 'starting':
509
+ default:
510
+ return 'bg-[var(--color-surface-container-high)] text-[var(--color-text-secondary)]'
511
+ }
512
+ }
513
+
514
+ function formatRecentToolUseSummary(
515
+ toolCall: ToolCall,
516
+ resultMap: Map<string, ToolResult>,
517
+ ): string {
518
+ const input = toolCall.input && typeof toolCall.input === 'object'
519
+ ? toolCall.input as Record<string, unknown>
520
+ : {}
521
+ const result = resultMap.get(toolCall.toolUseId)
522
+ const suffix = result?.isError ? ' • failed' : result ? ' • done' : ' • running'
523
+
524
+ switch (toolCall.toolName) {
525
+ case 'Bash':
526
+ return `Bash · ${typeof input.command === 'string' ? input.command : ''}${suffix}`
527
+ case 'Read':
528
+ return `Read · ${typeof input.file_path === 'string' ? input.file_path.split('/').pop() : 'file'}${suffix}`
529
+ case 'Glob':
530
+ return `Glob · ${typeof input.pattern === 'string' ? input.pattern : ''}${suffix}`
531
+ case 'Grep':
532
+ return `Grep · ${typeof input.pattern === 'string' ? input.pattern : ''}${suffix}`
533
+ case 'Agent':
534
+ return `Agent · ${typeof input.description === 'string' ? input.description : ''}${suffix}`
535
+ default:
536
+ return `${toolCall.toolName}${suffix}`
537
+ }
538
+ }
539
+
540
+ function getAgentErrorSummary(content: unknown): string {
541
+ const text = extractTextContent(content).replace(/\s+/g, ' ').trim()
542
+ if (!text) return ''
543
+ if (text.includes(`Agent type 'Explore' not found`)) {
544
+ return 'Explore agent unavailable in this session'
545
+ }
546
+ return text.length > 120 ? `${text.slice(0, 120)}...` : text
547
+ }
548
+
549
+ function getAgentOutputSummary(content: string): string {
550
+ const text = content.replace(/\s+\n/g, '\n').trim()
551
+ if (!text) return ''
552
+ return text.length > 220 ? `${text.slice(0, 220)}...` : text
553
+ }
554
+
555
+ function isAgentLaunchResult(content: unknown): boolean {
556
+ const text = extractTextContent(content).trim()
557
+ if (!text) return false
558
+
559
+ return (
560
+ text.startsWith('Async agent launched successfully.') ||
561
+ text.startsWith('Remote agent launched in CCR.') ||
562
+ (text.startsWith('Spawned successfully.') &&
563
+ text.includes('The agent is now running and will receive instructions via mailbox.')) ||
564
+ text.includes('The agent is working in the background. You will be notified automatically when it completes.') ||
565
+ text.includes('The agent is running remotely. You will be notified automatically when it completes.')
566
+ )
567
+ }
568
+
569
+ /**
570
+ * Check if agent result content is a lifecycle notification (shutdown, terminated, etc.)
571
+ * rather than actual agent output. These should not be shown to the user as results.
572
+ */
573
+ function isAgentLifecycleResult(content: unknown): boolean {
574
+ const text = extractTextContent(content).trim()
575
+ if (!text) return false
576
+ // Detect JSON lifecycle messages: shutdown_approved, shutdown_rejected, teammate_terminated
577
+ if (text.startsWith('{') && text.endsWith('}')) {
578
+ try {
579
+ const parsed = JSON.parse(text) as Record<string, unknown>
580
+ if (typeof parsed.type === 'string' && AGENT_LIFECYCLE_TYPES.has(parsed.type)) {
581
+ return true
582
+ }
583
+ } catch {
584
+ // Not valid JSON, not a lifecycle message
585
+ }
586
+ }
587
+ return false
588
+ }
589
+
590
+ function extractTextContent(content: unknown): string {
591
+ if (typeof content === 'string') return content
592
+ if (Array.isArray(content)) {
593
+ return content
594
+ .map((chunk) => {
595
+ if (typeof chunk === 'string') return chunk
596
+ if (chunk && typeof chunk === 'object' && 'text' in chunk) {
597
+ return typeof chunk.text === 'string' ? chunk.text : ''
598
+ }
599
+ return ''
600
+ })
601
+ .filter(Boolean)
602
+ .join('\n')
603
+ }
604
+ if (content && typeof content === 'object') {
605
+ if (
606
+ 'status' in content &&
607
+ (content as Record<string, unknown>).status === 'completed' &&
608
+ Array.isArray((content as Record<string, unknown>).content)
609
+ ) {
610
+ return extractTextContent((content as Record<string, unknown>).content)
611
+ }
612
+ }
613
+ if (content && typeof content === 'object') {
614
+ return JSON.stringify(content)
615
+ }
616
+ return ''
617
+ }