bingocode 1.0.2 → 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 (186) hide show
  1. package/desktop/README.md +30 -0
  2. package/desktop/bunfig.toml +1 -0
  3. package/desktop/index.html +17 -0
  4. package/desktop/package.json +55 -0
  5. package/desktop/pnpm-lock.yaml +3832 -0
  6. package/desktop/public/app-icon.jpg +0 -0
  7. package/desktop/public/fonts/inter-latin-ext.woff2 +0 -0
  8. package/desktop/public/fonts/inter-latin.woff2 +0 -0
  9. package/desktop/public/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
  10. package/desktop/public/fonts/jetbrains-mono-latin.woff2 +0 -0
  11. package/desktop/public/fonts/manrope-latin-ext.woff2 +0 -0
  12. package/desktop/public/fonts/manrope-latin.woff2 +0 -0
  13. package/desktop/public/fonts/material-symbols-outlined.woff2 +0 -0
  14. package/desktop/public/icons/bilibili.svg +1 -0
  15. package/desktop/public/icons/douyin.svg +1 -0
  16. package/desktop/public/icons/github.svg +3 -0
  17. package/desktop/public/icons/xiaohongshu.svg +1 -0
  18. package/desktop/scripts/build-macos-arm64.sh +270 -0
  19. package/desktop/scripts/build-sidecars.ts +183 -0
  20. package/desktop/scripts/build-windows-x64.ps1 +295 -0
  21. package/desktop/scripts/scan-missing-imports.ts +235 -0
  22. package/desktop/sidecars/claude-sidecar.ts +156 -0
  23. package/desktop/src/App.tsx +5 -0
  24. package/desktop/src/__tests__/agentsSettings.test.tsx +349 -0
  25. package/desktop/src/__tests__/pages.test.tsx +290 -0
  26. package/desktop/src/__tests__/skillsSettings.test.tsx +205 -0
  27. package/desktop/src/api/adapters.ts +12 -0
  28. package/desktop/src/api/agents.ts +36 -0
  29. package/desktop/src/api/cliTasks.ts +28 -0
  30. package/desktop/src/api/client.ts +63 -0
  31. package/desktop/src/api/computerUse.ts +76 -0
  32. package/desktop/src/api/filesystem.ts +30 -0
  33. package/desktop/src/api/hahaOAuth.ts +38 -0
  34. package/desktop/src/api/models.ts +28 -0
  35. package/desktop/src/api/providers.ts +63 -0
  36. package/desktop/src/api/search.ts +29 -0
  37. package/desktop/src/api/sessions.ts +56 -0
  38. package/desktop/src/api/settings.ts +20 -0
  39. package/desktop/src/api/skills.ts +19 -0
  40. package/desktop/src/api/tasks.ts +36 -0
  41. package/desktop/src/api/teams.ts +44 -0
  42. package/desktop/src/api/websocket.ts +164 -0
  43. package/desktop/src/components/chat/AskUserQuestion.tsx +268 -0
  44. package/desktop/src/components/chat/AssistantMessage.tsx +29 -0
  45. package/desktop/src/components/chat/AttachmentGallery.tsx +113 -0
  46. package/desktop/src/components/chat/ChatInput.tsx +622 -0
  47. package/desktop/src/components/chat/CodeViewer.tsx +161 -0
  48. package/desktop/src/components/chat/ComputerUsePermissionModal.test.tsx +174 -0
  49. package/desktop/src/components/chat/ComputerUsePermissionModal.tsx +311 -0
  50. package/desktop/src/components/chat/DiffViewer.tsx +157 -0
  51. package/desktop/src/components/chat/FileSearchMenu.tsx +198 -0
  52. package/desktop/src/components/chat/ImageGalleryModal.tsx +91 -0
  53. package/desktop/src/components/chat/InlineImageGallery.tsx +106 -0
  54. package/desktop/src/components/chat/InlineTaskSummary.tsx +60 -0
  55. package/desktop/src/components/chat/MermaidRenderer.test.tsx +98 -0
  56. package/desktop/src/components/chat/MermaidRenderer.tsx +361 -0
  57. package/desktop/src/components/chat/MessageActionBar.tsx +27 -0
  58. package/desktop/src/components/chat/MessageList.test.tsx +313 -0
  59. package/desktop/src/components/chat/MessageList.tsx +249 -0
  60. package/desktop/src/components/chat/PermissionDialog.tsx +262 -0
  61. package/desktop/src/components/chat/SessionTaskBar.test.tsx +99 -0
  62. package/desktop/src/components/chat/SessionTaskBar.tsx +159 -0
  63. package/desktop/src/components/chat/StreamingIndicator.tsx +41 -0
  64. package/desktop/src/components/chat/TerminalChrome.tsx +35 -0
  65. package/desktop/src/components/chat/ThinkingBlock.tsx +87 -0
  66. package/desktop/src/components/chat/ToolCallBlock.tsx +247 -0
  67. package/desktop/src/components/chat/ToolCallGroup.tsx +617 -0
  68. package/desktop/src/components/chat/ToolResultBlock.tsx +107 -0
  69. package/desktop/src/components/chat/UserMessage.tsx +38 -0
  70. package/desktop/src/components/chat/chatBlocks.test.tsx +136 -0
  71. package/desktop/src/components/chat/clipboard.ts +25 -0
  72. package/desktop/src/components/chat/composerUtils.test.ts +55 -0
  73. package/desktop/src/components/chat/composerUtils.ts +149 -0
  74. package/desktop/src/components/controls/ModelSelector.tsx +156 -0
  75. package/desktop/src/components/controls/PermissionModeSelector.tsx +229 -0
  76. package/desktop/src/components/layout/AppShell.tsx +107 -0
  77. package/desktop/src/components/layout/ContentRouter.tsx +27 -0
  78. package/desktop/src/components/layout/ProjectFilter.tsx +126 -0
  79. package/desktop/src/components/layout/Sidebar.test.tsx +158 -0
  80. package/desktop/src/components/layout/Sidebar.tsx +384 -0
  81. package/desktop/src/components/layout/StatusBar.tsx +31 -0
  82. package/desktop/src/components/layout/TabBar.test.tsx +136 -0
  83. package/desktop/src/components/layout/TabBar.tsx +318 -0
  84. package/desktop/src/components/layout/TitleBar.tsx +96 -0
  85. package/desktop/src/components/layout/WindowControls.test.tsx +69 -0
  86. package/desktop/src/components/layout/WindowControls.tsx +89 -0
  87. package/desktop/src/components/markdown/MarkdownRenderer.test.tsx +100 -0
  88. package/desktop/src/components/markdown/MarkdownRenderer.tsx +229 -0
  89. package/desktop/src/components/settings/ClaudeOfficialLogin.tsx +107 -0
  90. package/desktop/src/components/shared/Button.tsx +63 -0
  91. package/desktop/src/components/shared/CopyButton.tsx +58 -0
  92. package/desktop/src/components/shared/DirectoryPicker.tsx +316 -0
  93. package/desktop/src/components/shared/Dropdown.tsx +91 -0
  94. package/desktop/src/components/shared/Input.tsx +38 -0
  95. package/desktop/src/components/shared/Modal.tsx +65 -0
  96. package/desktop/src/components/shared/ProjectContextChip.tsx +30 -0
  97. package/desktop/src/components/shared/Spinner.tsx +30 -0
  98. package/desktop/src/components/shared/Textarea.tsx +38 -0
  99. package/desktop/src/components/shared/Toast.tsx +47 -0
  100. package/desktop/src/components/shared/UpdateChecker.tsx +90 -0
  101. package/desktop/src/components/skills/SkillDetail.test.tsx +89 -0
  102. package/desktop/src/components/skills/SkillDetail.tsx +403 -0
  103. package/desktop/src/components/skills/SkillList.tsx +254 -0
  104. package/desktop/src/components/tasks/DayOfWeekPicker.tsx +57 -0
  105. package/desktop/src/components/tasks/NewTaskModal.tsx +407 -0
  106. package/desktop/src/components/tasks/PromptEditor.tsx +74 -0
  107. package/desktop/src/components/tasks/TaskEmptyState.tsx +30 -0
  108. package/desktop/src/components/tasks/TaskList.tsx +46 -0
  109. package/desktop/src/components/tasks/TaskRow.tsx +253 -0
  110. package/desktop/src/components/tasks/TaskRunsPanel.tsx +195 -0
  111. package/desktop/src/components/teams/TeamStatusBar.tsx +147 -0
  112. package/desktop/src/config/providerPresets.ts +78 -0
  113. package/desktop/src/config/spinnerVerbs.ts +193 -0
  114. package/desktop/src/hooks/useKeyboardShortcuts.ts +60 -0
  115. package/desktop/src/i18n/index.ts +54 -0
  116. package/desktop/src/i18n/locales/en.ts +670 -0
  117. package/desktop/src/i18n/locales/zh.ts +670 -0
  118. package/desktop/src/lib/__tests__/cronDescribe.test.ts +93 -0
  119. package/desktop/src/lib/cronDescribe.ts +188 -0
  120. package/desktop/src/lib/desktopRuntime.ts +54 -0
  121. package/desktop/src/lib/parseRunOutput.ts +79 -0
  122. package/desktop/src/main.tsx +13 -0
  123. package/desktop/src/mocks/data.ts +202 -0
  124. package/desktop/src/pages/ActiveSession.test.tsx +181 -0
  125. package/desktop/src/pages/ActiveSession.tsx +219 -0
  126. package/desktop/src/pages/AdapterSettings.tsx +375 -0
  127. package/desktop/src/pages/AgentTeams.tsx +200 -0
  128. package/desktop/src/pages/ComputerUseSettings.tsx +420 -0
  129. package/desktop/src/pages/EmptySession.tsx +518 -0
  130. package/desktop/src/pages/NewTaskModal.tsx +346 -0
  131. package/desktop/src/pages/ScheduledTasks.tsx +66 -0
  132. package/desktop/src/pages/ScheduledTasksEmpty.tsx +152 -0
  133. package/desktop/src/pages/ScheduledTasksList.tsx +416 -0
  134. package/desktop/src/pages/SessionControls.tsx +460 -0
  135. package/desktop/src/pages/Settings.tsx +1448 -0
  136. package/desktop/src/pages/ToolInspection.tsx +235 -0
  137. package/desktop/src/stores/adapterStore.ts +106 -0
  138. package/desktop/src/stores/agentStore.ts +34 -0
  139. package/desktop/src/stores/chatStore.test.ts +505 -0
  140. package/desktop/src/stores/chatStore.ts +850 -0
  141. package/desktop/src/stores/cliTaskStore.ts +152 -0
  142. package/desktop/src/stores/hahaOAuthStore.test.ts +77 -0
  143. package/desktop/src/stores/hahaOAuthStore.ts +97 -0
  144. package/desktop/src/stores/providerStore.ts +101 -0
  145. package/desktop/src/stores/sessionStore.test.ts +63 -0
  146. package/desktop/src/stores/sessionStore.ts +102 -0
  147. package/desktop/src/stores/settingsStore.ts +120 -0
  148. package/desktop/src/stores/skillStore.ts +51 -0
  149. package/desktop/src/stores/tabStore.ts +169 -0
  150. package/desktop/src/stores/taskStore.ts +68 -0
  151. package/desktop/src/stores/teamStore.ts +344 -0
  152. package/desktop/src/stores/uiStore.ts +100 -0
  153. package/desktop/src/stores/updateStore.test.ts +71 -0
  154. package/desktop/src/stores/updateStore.ts +221 -0
  155. package/desktop/src/theme/globals.css +465 -0
  156. package/desktop/src/types/adapter.ts +33 -0
  157. package/desktop/src/types/chat.ts +152 -0
  158. package/desktop/src/types/cliTask.ts +24 -0
  159. package/desktop/src/types/provider.ts +62 -0
  160. package/desktop/src/types/session.ts +27 -0
  161. package/desktop/src/types/settings.ts +22 -0
  162. package/desktop/src/types/skill.ts +38 -0
  163. package/desktop/src/types/task.ts +56 -0
  164. package/desktop/src/types/team.ts +38 -0
  165. package/desktop/src-tauri/Cargo.lock +5549 -0
  166. package/desktop/src-tauri/Cargo.toml +20 -0
  167. package/desktop/src-tauri/app-icon.svg +13 -0
  168. package/desktop/src-tauri/build.rs +3 -0
  169. package/desktop/src-tauri/capabilities/default.json +106 -0
  170. package/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  171. package/desktop/src-tauri/icons/android/values/ic_launcher_background.xml +4 -0
  172. package/desktop/src-tauri/icons/icon.icns +0 -0
  173. package/desktop/src-tauri/icons/icon.ico +0 -0
  174. package/desktop/src-tauri/src/lib.rs +408 -0
  175. package/desktop/src-tauri/src/main.rs +6 -0
  176. package/desktop/src-tauri/tauri.conf.json +78 -0
  177. package/desktop/src-tauri/tauri.macos.conf.json +18 -0
  178. package/desktop/src-tauri/tauri.release-ci.json +5 -0
  179. package/desktop/src-tauri/tauri.windows.conf.json +16 -0
  180. package/desktop/src-tauri/windows-installer-hooks.nsh +17 -0
  181. package/desktop/tsconfig.json +25 -0
  182. package/desktop/vite.config.ts +26 -0
  183. package/desktop/vitest.config.ts +18 -0
  184. package/package.json +1 -1
  185. package/src/commands/desktop/desktop.tsx +9 -0
  186. package/src/commands/desktop/index.ts +26 -0
@@ -0,0 +1,313 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
3
+ import { MessageList, buildRenderItems } from './MessageList'
4
+ import { useChatStore } from '../../stores/chatStore'
5
+ import { useTabStore } from '../../stores/tabStore'
6
+ import type { UIMessage } from '../../types/chat'
7
+ import type { PerSessionState } from '../../stores/chatStore'
8
+
9
+ const ACTIVE_TAB = 'active-tab'
10
+
11
+ function makeSessionState(overrides: Partial<PerSessionState> = {}): PerSessionState {
12
+ return {
13
+ messages: [],
14
+ chatState: 'idle',
15
+ connectionState: 'connected',
16
+ streamingText: '',
17
+ streamingToolInput: '',
18
+ activeToolUseId: null,
19
+ activeToolName: null,
20
+ activeThinkingId: null,
21
+ pendingPermission: null,
22
+ pendingComputerUsePermission: null,
23
+ tokenUsage: { input_tokens: 0, output_tokens: 0 },
24
+ elapsedSeconds: 0,
25
+ statusVerb: '',
26
+ slashCommands: [],
27
+ agentTaskNotifications: {},
28
+ elapsedTimer: null,
29
+ ...overrides,
30
+ }
31
+ }
32
+
33
+ describe('MessageList nested tool calls', () => {
34
+ beforeEach(() => {
35
+ useTabStore.setState({ activeTabId: ACTIVE_TAB, tabs: [{ sessionId: ACTIVE_TAB, title: 'Test', type: 'session' as const, status: 'idle' }] })
36
+ useChatStore.setState({ sessions: { [ACTIVE_TAB]: makeSessionState() } })
37
+ })
38
+
39
+ it('renders sub-agent tool calls inline beneath the parent agent tool call', () => {
40
+ useChatStore.setState({
41
+ sessions: {
42
+ [ACTIVE_TAB]: makeSessionState({
43
+ messages: [
44
+ {
45
+ id: 'tool-agent',
46
+ type: 'tool_use',
47
+ toolName: 'Agent',
48
+ toolUseId: 'agent-1',
49
+ input: { description: 'Inspect src/components' },
50
+ timestamp: 1,
51
+ },
52
+ {
53
+ id: 'tool-read',
54
+ type: 'tool_use',
55
+ toolName: 'Read',
56
+ toolUseId: 'read-1',
57
+ input: { file_path: '/tmp/example.ts' },
58
+ timestamp: 2,
59
+ parentToolUseId: 'agent-1',
60
+ },
61
+ {
62
+ id: 'result-read',
63
+ type: 'tool_result',
64
+ toolUseId: 'read-1',
65
+ content: 'const answer = 42',
66
+ isError: false,
67
+ timestamp: 3,
68
+ parentToolUseId: 'agent-1',
69
+ },
70
+ ],
71
+ }),
72
+ },
73
+ })
74
+
75
+ const { container } = render(<MessageList />)
76
+
77
+ expect(screen.getAllByText('Running').length).toBeGreaterThan(0)
78
+ expect(screen.getByText(/Read .*example\.ts.*done/i)).toBeTruthy()
79
+ expect(container.textContent).toContain('Agent')
80
+ })
81
+
82
+ it('keeps root tool runs split when nested child tool calls appear between them', () => {
83
+ const messages: UIMessage[] = [
84
+ {
85
+ id: 'tool-agent',
86
+ type: 'tool_use',
87
+ toolName: 'Agent',
88
+ toolUseId: 'agent-1',
89
+ input: { description: 'Inspect src/components' },
90
+ timestamp: 1,
91
+ },
92
+ {
93
+ id: 'tool-read',
94
+ type: 'tool_use',
95
+ toolName: 'Read',
96
+ toolUseId: 'read-1',
97
+ input: { file_path: '/tmp/example.ts' },
98
+ timestamp: 2,
99
+ parentToolUseId: 'agent-1',
100
+ },
101
+ {
102
+ id: 'result-read',
103
+ type: 'tool_result',
104
+ toolUseId: 'read-1',
105
+ content: 'const answer = 42',
106
+ isError: false,
107
+ timestamp: 3,
108
+ parentToolUseId: 'agent-1',
109
+ },
110
+ {
111
+ id: 'tool-write',
112
+ type: 'tool_use',
113
+ toolName: 'Write',
114
+ toolUseId: 'write-1',
115
+ input: { file_path: '/tmp/out.ts', content: 'export const value = 1' },
116
+ timestamp: 4,
117
+ },
118
+ ]
119
+
120
+ const toolUseIds = new Set(messages.filter((message) => message.type === 'tool_use').map((message) => message.toolUseId))
121
+ const renderItems = buildRenderItems(messages, toolUseIds)
122
+ const toolGroups = renderItems.filter((item) => item.kind === 'tool_group')
123
+
124
+ expect(toolGroups).toHaveLength(2)
125
+ expect(toolGroups.map((item) => item.toolCalls[0]?.toolUseId)).toEqual(['agent-1', 'write-1'])
126
+ })
127
+
128
+ it('shows failed agent status and compact unavailable summary for Explore launch errors', () => {
129
+ useChatStore.setState({
130
+ sessions: {
131
+ [ACTIVE_TAB]: makeSessionState({
132
+ messages: [
133
+ {
134
+ id: 'tool-agent',
135
+ type: 'tool_use',
136
+ toolName: 'Agent',
137
+ toolUseId: 'agent-1',
138
+ input: { description: '探索整体架构', subagent_type: 'Explore' },
139
+ timestamp: 1,
140
+ },
141
+ {
142
+ id: 'result-agent',
143
+ type: 'tool_result',
144
+ toolUseId: 'agent-1',
145
+ content: `Agent type 'Explore' not found. Available agents: general-purpose`,
146
+ isError: true,
147
+ timestamp: 2,
148
+ },
149
+ ],
150
+ }),
151
+ },
152
+ })
153
+
154
+ render(<MessageList />)
155
+
156
+ expect(screen.getByText('Failed')).toBeTruthy()
157
+ expect(screen.getByText('Explore agent unavailable in this session')).toBeTruthy()
158
+ })
159
+
160
+ it('shows completed agent output when no nested tool activity is available', () => {
161
+ const longResult = '探索完成。让我将结果整合写入计划文件。第二段补充内容用于验证 dialog 展示的是完整结果而不是截断摘要。'
162
+
163
+ useChatStore.setState({
164
+ sessions: {
165
+ [ACTIVE_TAB]: makeSessionState({
166
+ messages: [
167
+ {
168
+ id: 'tool-agent',
169
+ type: 'tool_use',
170
+ toolName: 'Agent',
171
+ toolUseId: 'agent-1',
172
+ input: { description: '探索整体架构' },
173
+ timestamp: 1,
174
+ },
175
+ {
176
+ id: 'result-agent',
177
+ type: 'tool_result',
178
+ toolUseId: 'agent-1',
179
+ content: {
180
+ status: 'completed',
181
+ content: [{ type: 'text', text: longResult }],
182
+ },
183
+ isError: false,
184
+ timestamp: 2,
185
+ },
186
+ ],
187
+ }),
188
+ },
189
+ })
190
+
191
+ render(<MessageList />)
192
+
193
+ expect(screen.getByText('Done')).toBeTruthy()
194
+ expect(screen.getByRole('button', { name: 'View result' })).toBeTruthy()
195
+
196
+ fireEvent.click(screen.getByRole('button', { name: 'View result' }))
197
+
198
+ const dialog = screen.getByRole('dialog')
199
+ expect(within(dialog).getByText(/第二段补充内容用于验证 dialog 展示的是完整结果而不是截断摘要。/)).toBeTruthy()
200
+ expect(screen.getByRole('button', { name: 'Close dialog' })).toBeTruthy()
201
+ })
202
+
203
+ it('keeps async launched agents in running state until a terminal notification arrives', () => {
204
+ useChatStore.setState({
205
+ sessions: {
206
+ [ACTIVE_TAB]: makeSessionState({
207
+ messages: [
208
+ {
209
+ id: 'tool-agent',
210
+ type: 'tool_use',
211
+ toolName: 'Agent',
212
+ toolUseId: 'agent-1',
213
+ input: { description: '修复临时文件泄漏' },
214
+ timestamp: 1,
215
+ },
216
+ {
217
+ id: 'result-agent',
218
+ type: 'tool_result',
219
+ toolUseId: 'agent-1',
220
+ content:
221
+ "Async agent launched successfully.\nagentId: a29934b04b20ed564 (internal ID - do not mention to user. Use SendMessage with to: 'a29934b04b20ed564' to continue this agent.)\nThe agent is working in the background. You will be notified automatically when it completes.",
222
+ isError: false,
223
+ timestamp: 2,
224
+ },
225
+ ],
226
+ }),
227
+ },
228
+ })
229
+
230
+ render(<MessageList />)
231
+
232
+ expect(screen.getAllByText('Running').length).toBeGreaterThan(0)
233
+ expect(screen.queryByText('Done')).toBeNull()
234
+ expect(screen.queryByRole('button', { name: 'View result' })).toBeNull()
235
+ })
236
+
237
+ it('renders copy controls for user messages and scopes assistant copy to a single reply', async () => {
238
+ const writeText = vi.fn().mockResolvedValue(undefined)
239
+ Object.assign(navigator, {
240
+ clipboard: {
241
+ writeText,
242
+ },
243
+ })
244
+
245
+ useChatStore.setState({
246
+ sessions: {
247
+ [ACTIVE_TAB]: makeSessionState({
248
+ messages: [
249
+ {
250
+ id: 'user-1',
251
+ type: 'user_text',
252
+ content: '请帮我探索整体架构',
253
+ timestamp: 1,
254
+ },
255
+ {
256
+ id: 'assistant-1',
257
+ type: 'assistant_text',
258
+ content: '先看 CLI 和服务端入口。',
259
+ timestamp: 2,
260
+ },
261
+ {
262
+ id: 'assistant-2',
263
+ type: 'assistant_text',
264
+ content: '再看 desktop 前后端边界。',
265
+ timestamp: 3,
266
+ },
267
+ ],
268
+ }),
269
+ },
270
+ })
271
+
272
+ render(<MessageList />)
273
+
274
+ expect(screen.getByRole('button', { name: 'Copy prompt' })).toBeTruthy()
275
+
276
+ fireEvent.click(screen.getAllByRole('button', { name: 'Copy reply' })[1]!)
277
+
278
+ await waitFor(() => {
279
+ expect(writeText).toHaveBeenCalledWith('再看 desktop 前后端边界。')
280
+ })
281
+ expect(writeText).not.toHaveBeenCalledWith(
282
+ '先看 CLI 和服务端入口。\n再看 desktop 前后端边界。'
283
+ )
284
+ })
285
+
286
+ it('shows raw startup details under translated CLI startup errors', () => {
287
+ useChatStore.setState({
288
+ sessions: {
289
+ [ACTIVE_TAB]: makeSessionState({
290
+ messages: [
291
+ {
292
+ id: 'error-1',
293
+ type: 'error',
294
+ code: 'CLI_START_FAILED',
295
+ message:
296
+ 'CLI exited during startup (code 1): Claude Code on Windows requires git-bash (https://git-scm.com/downloads/win).',
297
+ timestamp: 1,
298
+ },
299
+ ],
300
+ }),
301
+ },
302
+ })
303
+
304
+ render(<MessageList />)
305
+
306
+ expect(screen.getByText('Failed to start CLI process.')).toBeTruthy()
307
+ expect(
308
+ screen.getByText(
309
+ 'CLI exited during startup (code 1): Claude Code on Windows requires git-bash (https://git-scm.com/downloads/win).',
310
+ ),
311
+ ).toBeTruthy()
312
+ })
313
+ })
@@ -0,0 +1,249 @@
1
+ import { useRef, useEffect, useMemo, memo } from 'react'
2
+ import { useChatStore } from '../../stores/chatStore'
3
+ import { useTabStore } from '../../stores/tabStore'
4
+ import { useTranslation } from '../../i18n'
5
+ import type { TranslationKey } from '../../i18n/locales/en'
6
+ import { UserMessage } from './UserMessage'
7
+ import { AssistantMessage } from './AssistantMessage'
8
+ import { ThinkingBlock } from './ThinkingBlock'
9
+ import { ToolCallBlock } from './ToolCallBlock'
10
+ import { ToolCallGroup } from './ToolCallGroup'
11
+ import { ToolResultBlock } from './ToolResultBlock'
12
+ import { PermissionDialog } from './PermissionDialog'
13
+ import { AskUserQuestion } from './AskUserQuestion'
14
+ import { StreamingIndicator } from './StreamingIndicator'
15
+ import { InlineTaskSummary } from './InlineTaskSummary'
16
+ import type { AgentTaskNotification, UIMessage } from '../../types/chat'
17
+
18
+ type ToolCall = Extract<UIMessage, { type: 'tool_use' }>
19
+ type ToolResult = Extract<UIMessage, { type: 'tool_result' }>
20
+
21
+ type RenderItem =
22
+ | { kind: 'tool_group'; toolCalls: ToolCall[]; id: string }
23
+ | { kind: 'message'; message: UIMessage }
24
+
25
+ export function buildRenderItems(messages: UIMessage[], toolUseIds: Set<string>): RenderItem[] {
26
+ const items: RenderItem[] = []
27
+ let pendingToolCalls: ToolCall[] = []
28
+
29
+ const flushGroup = () => {
30
+ if (pendingToolCalls.length > 0) {
31
+ items.push({
32
+ kind: 'tool_group',
33
+ toolCalls: [...pendingToolCalls],
34
+ id: `group-${pendingToolCalls[0]!.id}`,
35
+ })
36
+ pendingToolCalls = []
37
+ }
38
+ }
39
+
40
+ for (const msg of messages) {
41
+ if (msg.type === 'tool_result' && toolUseIds.has(msg.toolUseId)) {
42
+ continue
43
+ }
44
+
45
+ if (msg.type === 'tool_use') {
46
+ if (msg.parentToolUseId) {
47
+ flushGroup()
48
+ continue
49
+ }
50
+ if (msg.toolName === 'AskUserQuestion') {
51
+ flushGroup()
52
+ items.push({ kind: 'message', message: msg })
53
+ } else {
54
+ pendingToolCalls.push(msg)
55
+ }
56
+ } else {
57
+ flushGroup()
58
+ items.push({ kind: 'message', message: msg })
59
+ }
60
+ }
61
+
62
+ flushGroup()
63
+ return items
64
+ }
65
+
66
+ export function MessageList() {
67
+ const activeTabId = useTabStore((s) => s.activeTabId)
68
+ const sessionState = useChatStore((s) => activeTabId ? s.sessions[activeTabId] : undefined)
69
+ const messages = sessionState?.messages ?? []
70
+ const chatState = sessionState?.chatState ?? 'idle'
71
+ const streamingText = sessionState?.streamingText ?? ''
72
+ const activeThinkingId = sessionState?.activeThinkingId ?? null
73
+ const agentTaskNotifications = sessionState?.agentTaskNotifications ?? {}
74
+ const bottomRef = useRef<HTMLDivElement>(null)
75
+
76
+ useEffect(() => {
77
+ bottomRef.current?.scrollIntoView?.({ behavior: 'smooth' })
78
+ }, [messages.length, streamingText])
79
+
80
+ const { toolResultMap, childToolCallsByParent, renderItems } = useMemo(() => {
81
+ const toolUseIds = new Set<string>()
82
+ const toolResultMap = new Map<string, ToolResult>()
83
+ const childToolCallsByParent = new Map<string, ToolCall[]>()
84
+
85
+ for (const msg of messages) {
86
+ if (msg.type === 'tool_use') {
87
+ toolUseIds.add(msg.toolUseId)
88
+ if (msg.parentToolUseId) {
89
+ const siblings = childToolCallsByParent.get(msg.parentToolUseId)
90
+ if (siblings) {
91
+ siblings.push(msg)
92
+ } else {
93
+ childToolCallsByParent.set(msg.parentToolUseId, [msg])
94
+ }
95
+ }
96
+ }
97
+ if (msg.type === 'tool_result' && msg.toolUseId) {
98
+ toolResultMap.set(msg.toolUseId, msg)
99
+ }
100
+ }
101
+
102
+ const renderItems = buildRenderItems(messages, toolUseIds)
103
+ return { toolUseIds, toolResultMap, childToolCallsByParent, renderItems }
104
+ }, [messages])
105
+
106
+ return (
107
+ <div className="flex-1 overflow-y-auto px-4 py-4">
108
+ <div className="mx-auto max-w-[860px]">
109
+ {renderItems.map((item) => {
110
+ if (item.kind === 'tool_group') {
111
+ return (
112
+ <ToolCallGroup
113
+ key={item.id}
114
+ toolCalls={item.toolCalls}
115
+ resultMap={toolResultMap}
116
+ childToolCallsByParent={childToolCallsByParent}
117
+ agentTaskNotifications={agentTaskNotifications}
118
+ isStreaming={
119
+ chatState === 'tool_executing' &&
120
+ item.toolCalls.some((tc) => !toolResultMap.has(tc.toolUseId))
121
+ }
122
+ />
123
+ )
124
+ }
125
+
126
+ const msg = item.message
127
+ return (
128
+ <MessageBlock
129
+ key={msg.id}
130
+ message={msg}
131
+ activeThinkingId={activeThinkingId}
132
+ agentTaskNotifications={agentTaskNotifications}
133
+ toolResult={
134
+ msg.type === 'tool_use'
135
+ ? (() => {
136
+ const r = toolResultMap.get(msg.toolUseId)
137
+ return r ? { content: r.content, isError: r.isError } : null
138
+ })()
139
+ : null
140
+ }
141
+ />
142
+ )
143
+ })}
144
+
145
+ {streamingText && (
146
+ <AssistantMessage content={streamingText} isStreaming={chatState === 'streaming'} />
147
+ )}
148
+
149
+ {/* Show StreamingIndicator when:
150
+ - tool_executing: tool is running
151
+ - thinking but no active ThinkingBlock yet: the gap between
152
+ sending a message and receiving the first thinking delta */}
153
+ {(chatState === 'tool_executing' || (chatState === 'thinking' && !activeThinkingId)) && (
154
+ <StreamingIndicator />
155
+ )}
156
+
157
+ <div ref={bottomRef} />
158
+ </div>
159
+ </div>
160
+ )
161
+ }
162
+
163
+ export const MessageBlock = memo(function MessageBlock({
164
+ message,
165
+ activeThinkingId,
166
+ agentTaskNotifications,
167
+ toolResult,
168
+ }: {
169
+ message: UIMessage
170
+ activeThinkingId: string | null
171
+ agentTaskNotifications: Record<string, AgentTaskNotification>
172
+ toolResult?: { content: unknown; isError: boolean } | null
173
+ }) {
174
+ const t = useTranslation()
175
+
176
+ switch (message.type) {
177
+ case 'user_text':
178
+ return <UserMessage content={message.content} attachments={message.attachments} />
179
+ case 'assistant_text':
180
+ return <AssistantMessage content={message.content} />
181
+ case 'thinking':
182
+ return <ThinkingBlock content={message.content} isActive={message.id === activeThinkingId} />
183
+ case 'tool_use':
184
+ if (message.toolName === 'AskUserQuestion') {
185
+ return (
186
+ <AskUserQuestion
187
+ toolUseId={message.toolUseId}
188
+ input={message.input}
189
+ />
190
+ )
191
+ }
192
+ return (
193
+ <ToolCallBlock
194
+ toolName={message.toolName}
195
+ input={message.input}
196
+ result={toolResult}
197
+ agentTaskNotification={
198
+ message.toolName === 'Agent'
199
+ ? agentTaskNotifications[message.toolUseId]
200
+ : undefined
201
+ }
202
+ />
203
+ )
204
+ case 'tool_result':
205
+ return (
206
+ <ToolResultBlock
207
+ content={message.content}
208
+ isError={message.isError}
209
+ standalone
210
+ />
211
+ )
212
+ case 'permission_request':
213
+ return (
214
+ <PermissionDialog
215
+ requestId={message.requestId}
216
+ toolName={message.toolName}
217
+ input={message.input}
218
+ description={message.description}
219
+ />
220
+ )
221
+ case 'error': {
222
+ const errorKey = message.code ? `error.${message.code}` as TranslationKey : null
223
+ const errorText = errorKey ? t(errorKey) : null
224
+ const displayMessage = (errorText && errorText !== errorKey) ? errorText : message.message
225
+ const showRawDetail =
226
+ Boolean(message.message) &&
227
+ message.message.trim() !== '' &&
228
+ message.message !== displayMessage
229
+ return (
230
+ <div className="mb-3 px-4 py-2.5 rounded-lg border border-[var(--color-error)]/20 bg-[var(--color-error-container)]/28 text-sm text-[var(--color-error)]">
231
+ <strong>Error:</strong> {displayMessage}
232
+ {showRawDetail && (
233
+ <div className="mt-1 whitespace-pre-wrap text-xs text-[var(--color-on-error-container)]/85">
234
+ {message.message}
235
+ </div>
236
+ )}
237
+ </div>
238
+ )
239
+ }
240
+ case 'task_summary':
241
+ return <InlineTaskSummary tasks={message.tasks} />
242
+ case 'system':
243
+ return (
244
+ <div className="mb-3 text-center text-xs text-[var(--color-text-tertiary)]">
245
+ {message.content}
246
+ </div>
247
+ )
248
+ }
249
+ })