@muyichengshayu/promptx 0.2.16 → 0.2.18

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 (57) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/apps/runner/src/codex.js +114 -16
  3. package/apps/runner/src/engines/claudeCodeRunner.test.js +467 -0
  4. package/apps/runner/src/engines/kimiCodeRunner.test.js +127 -0
  5. package/apps/runner/src/engines/openCodeRunner.test.js +236 -0
  6. package/apps/runner/src/engines/runnerContract.test.js +510 -0
  7. package/apps/runner/src/engines/shellRunner.test.js +46 -0
  8. package/apps/runner/src/runManager.test.js +913 -0
  9. package/apps/runner/src/serverClient.test.js +93 -0
  10. package/apps/server/src/agentSessionDiscovery.test.js +572 -0
  11. package/apps/server/src/appPaths.test.js +52 -0
  12. package/apps/server/src/assetRoutes.test.js +168 -0
  13. package/apps/server/src/codex.test.js +518 -0
  14. package/apps/server/src/codexRoutes.js +12 -0
  15. package/apps/server/src/codexRoutes.test.js +376 -0
  16. package/apps/server/src/codexRuns.test.js +160 -0
  17. package/apps/server/src/codexSessions.test.js +369 -0
  18. package/apps/server/src/db.test.js +182 -0
  19. package/apps/server/src/gitDiff.test.js +542 -0
  20. package/apps/server/src/gitDiffClient.test.js +140 -0
  21. package/apps/server/src/index.js +2 -0
  22. package/apps/server/src/internalRoutes.test.js +134 -0
  23. package/apps/server/src/maintenance.test.js +154 -0
  24. package/apps/server/src/processControl.test.js +147 -0
  25. package/apps/server/src/relayClient.test.js +478 -0
  26. package/apps/server/src/relayConfig.test.js +73 -0
  27. package/apps/server/src/relayProtocol.test.js +49 -0
  28. package/apps/server/src/relayServer.test.js +798 -0
  29. package/apps/server/src/relayTenants.test.js +137 -0
  30. package/apps/server/src/relayUsageStore.test.js +65 -0
  31. package/apps/server/src/repository.test.js +150 -0
  32. package/apps/server/src/runDispatchService.test.js +563 -0
  33. package/apps/server/src/runEventIngest.test.js +225 -0
  34. package/apps/server/src/runRecovery.test.js +73 -0
  35. package/apps/server/src/runnerClient.test.js +80 -0
  36. package/apps/server/src/runnerDispatch.test.js +136 -0
  37. package/apps/server/src/systemConfig.test.js +112 -0
  38. package/apps/server/src/systemRoutes.test.js +319 -0
  39. package/apps/server/src/taskRoutes.test.js +775 -0
  40. package/apps/server/src/upload.test.js +30 -0
  41. package/apps/server/src/webAppRoutes.test.js +67 -0
  42. package/apps/server/src/workspaceFiles.js +170 -0
  43. package/apps/server/src/workspaceFiles.test.js +327 -0
  44. package/apps/web/dist/assets/{CodexSessionManagerDialog-D1PwOD4T.js → CodexSessionManagerDialog-D_72LIMv.js} +1 -1
  45. package/apps/web/dist/assets/{TaskDiffReviewDialog-GKKv-IkZ.js → TaskDiffReviewDialog-CoQgN73y.js} +1 -1
  46. package/apps/web/dist/assets/{WorkbenchSettingsDialog-IWlkg3kU.js → WorkbenchSettingsDialog-Bw-fcsXi.js} +1 -1
  47. package/apps/web/dist/assets/WorkbenchView-CebqJlAz.css +1 -0
  48. package/apps/web/dist/assets/WorkbenchView-CsZnW4Q7.js +58 -0
  49. package/apps/web/dist/assets/index-Bt9T2IKp.js +2 -0
  50. package/apps/web/dist/assets/{index-BAfqUG7o.css → index-DaJU954y.css} +1 -1
  51. package/apps/web/dist/index.html +2 -2
  52. package/package.json +14 -21
  53. package/packages/shared/src/dailyLogStream.test.js +29 -0
  54. package/packages/shared/src/shellCommands.test.js +45 -0
  55. package/apps/web/dist/assets/WorkbenchView-CK1snPBz.css +0 -1
  56. package/apps/web/dist/assets/WorkbenchView-dXHPTH_M.js +0 -58
  57. package/apps/web/dist/assets/index-DaIoquOV.js +0 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.18
4
+
5
+ - 源码浏览新增行作者提示:查看代码时默认启用 Git blame,鼠标悬停到源码行会在本行下方靠右显示最近修改作者、提交摘要、短 hash 与时间;非 Git 仓库、未跟踪文件、大文件和二进制文件会平稳降级为不可用提示。
6
+
7
+ ## 0.2.17
8
+
9
+ - 修复 Windows 上 Codex 任务实际完成后工作台仍显示运行中、任务卡片持续转圈的问题:runner 在收到 `turn.completed` 后会进入短暂收尾等待;如果 Codex 子进程没有及时退出,会按完成状态落库并回收子进程,避免成功任务长时间卡在 `running`。
10
+
3
11
  ## 0.2.16
4
12
 
5
13
  - 修复中栏历史 Prompt 卡片点击“插入”时图片丢失的问题:现在会优先按结构化 `promptBlocks` 插入文本、导入块和图片,图文混合 Prompt 会完整进入右侧编辑区;仅图片的 Prompt 也会插入为图片块,不再退化成 URL 文本。
@@ -21,6 +21,14 @@ const STATE_DB_PATH = path.join(CODEX_HOME, 'state_5.sqlite')
21
21
  const TMP_DIR = path.join(CODEX_HOME, 'tmp')
22
22
  const MAX_THREAD_COUNT = 120
23
23
  const MAX_OUTPUT_TAIL_LENGTH = 64 * 1024
24
+ const CODEX_RESULT_EXIT_GRACE_MS = Math.max(
25
+ 0,
26
+ Number(process.env.PROMPTX_CODEX_RESULT_EXIT_GRACE_MS) || 3000
27
+ )
28
+ const CODEX_RESULT_FORCE_STOP_GRACE_MS = Math.max(
29
+ 200,
30
+ Number(process.env.PROMPTX_CODEX_RESULT_FORCE_STOP_GRACE_MS) || 1500
31
+ )
24
32
  const CODEX_DEFAULT_ARGS = ['--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check']
25
33
  const RESOLVED_CODEX_BIN = resolveCodexBinary()
26
34
  const require = createRequire(import.meta.url)
@@ -335,6 +343,25 @@ function trackThreadId(event, setThreadId) {
335
343
  }
336
344
  }
337
345
 
346
+ function extractCodexCompletionMessage(event = {}) {
347
+ if (event?.type === AGENT_RUN_EVENT_TYPES.TURN_COMPLETED) {
348
+ return extractTextFromUnknownError(event.result)
349
+ || extractTextFromUnknownError(event.message)
350
+ || extractTextFromUnknownError(event.response)
351
+ }
352
+
353
+ if (
354
+ event?.type === AGENT_RUN_EVENT_TYPES.ITEM_COMPLETED
355
+ && event?.item?.type === 'agent_message'
356
+ ) {
357
+ return extractTextFromUnknownError(event.item.text)
358
+ || extractTextFromUnknownError(event.item.message)
359
+ || extractTextFromUnknownError(event.item.content)
360
+ }
361
+
362
+ return ''
363
+ }
364
+
338
365
  function parseThreadIdFromStdout(stdout = '') {
339
366
  const lines = String(stdout || '')
340
367
  .replace(/\r\n/g, '\n')
@@ -430,6 +457,74 @@ export function streamPromptToCodexSession(sessionInput, prompt, callbacks = {})
430
457
  let stderrRaw = ''
431
458
  let finalMessage = ''
432
459
  let finalThreadId = session.codexThreadId || ''
460
+ let resultExitGraceTimer = null
461
+ let settled = false
462
+ let resolveResult = null
463
+ let rejectResult = null
464
+
465
+ const clearResultExitGraceTimer = () => {
466
+ if (resultExitGraceTimer) {
467
+ clearTimeout(resultExitGraceTimer)
468
+ resultExitGraceTimer = null
469
+ }
470
+ }
471
+
472
+ const readOutputFileMessage = () => {
473
+ if (!fs.existsSync(outputFile)) {
474
+ return ''
475
+ }
476
+
477
+ return repairPossibleMojibake(fs.readFileSync(outputFile, 'utf8').trim())
478
+ }
479
+
480
+ const refreshFinalMessageFromOutputFile = () => {
481
+ finalMessage = readOutputFileMessage() || finalMessage
482
+ }
483
+
484
+ const settleCompleted = (options = {}) => {
485
+ if (settled) {
486
+ return
487
+ }
488
+
489
+ settled = true
490
+ clearResultExitGraceTimer()
491
+ refreshFinalMessageFromOutputFile()
492
+ if (!finalThreadId) {
493
+ finalThreadId = parseThreadIdFromStdout(stdoutRaw)
494
+ }
495
+ emit(createCompletedEnvelopeEvent(finalMessage))
496
+ resolveResult?.({
497
+ sessionId: session.id,
498
+ message: finalMessage,
499
+ threadId: finalThreadId,
500
+ })
501
+
502
+ if (options.stopChild && child.exitCode === null && child.signalCode === null) {
503
+ forceStopChildProcess(child, { graceMs: CODEX_RESULT_FORCE_STOP_GRACE_MS })
504
+ }
505
+ }
506
+
507
+ const settleError = (error) => {
508
+ if (settled) {
509
+ return
510
+ }
511
+
512
+ settled = true
513
+ clearResultExitGraceTimer()
514
+ rejectResult?.(error)
515
+ }
516
+
517
+ const scheduleResultExitGrace = () => {
518
+ if (settled || resultExitGraceTimer || CODEX_RESULT_EXIT_GRACE_MS <= 0) {
519
+ return
520
+ }
521
+
522
+ resultExitGraceTimer = setTimeout(() => {
523
+ resultExitGraceTimer = null
524
+ settleCompleted({ stopChild: true })
525
+ }, CODEX_RESULT_EXIT_GRACE_MS)
526
+ resultExitGraceTimer.unref?.()
527
+ }
433
528
 
434
529
  const emit = (event) => {
435
530
  try {
@@ -471,6 +566,10 @@ export function streamPromptToCodexSession(sessionInput, prompt, callbacks = {})
471
566
  if (event) {
472
567
  trackThreadId(event, rememberThreadId)
473
568
  emit(createAgentEventEnvelopeEvent(sanitizeCodexPayload(event)))
569
+ finalMessage = extractCodexCompletionMessage(event) || finalMessage
570
+ if (event.type === AGENT_RUN_EVENT_TYPES.TURN_COMPLETED) {
571
+ scheduleResultExitGrace()
572
+ }
474
573
  continue
475
574
  }
476
575
 
@@ -494,11 +593,18 @@ export function streamPromptToCodexSession(sessionInput, prompt, callbacks = {})
494
593
  child.stdin.end()
495
594
 
496
595
  const result = new Promise((resolve, reject) => {
596
+ resolveResult = resolve
597
+ rejectResult = reject
598
+
497
599
  child.on('error', (error) => {
498
- reject(normalizeSpawnError(error))
600
+ settleError(normalizeSpawnError(error))
499
601
  })
500
602
 
501
603
  child.on('close', (code) => {
604
+ if (settled) {
605
+ return
606
+ }
607
+
502
608
  const stdoutTail = flushBufferedText(stdoutBuffer)
503
609
  const stderrTail = flushBufferedText(stderrBuffer)
504
610
 
@@ -507,6 +613,10 @@ export function streamPromptToCodexSession(sessionInput, prompt, callbacks = {})
507
613
  if (event) {
508
614
  trackThreadId(event, rememberThreadId)
509
615
  emit(createAgentEventEnvelopeEvent(sanitizeCodexPayload(event)))
616
+ finalMessage = extractCodexCompletionMessage(event) || finalMessage
617
+ if (event.type === AGENT_RUN_EVENT_TYPES.TURN_COMPLETED) {
618
+ scheduleResultExitGrace()
619
+ }
510
620
  } else {
511
621
  emit(createStdoutEnvelopeEvent(repairPossibleMojibake(line)))
512
622
  }
@@ -516,26 +626,14 @@ export function streamPromptToCodexSession(sessionInput, prompt, callbacks = {})
516
626
  emit(createStderrEnvelopeEvent(repairPossibleMojibake(line)))
517
627
  })
518
628
 
519
- if (fs.existsSync(outputFile)) {
520
- finalMessage = repairPossibleMojibake(fs.readFileSync(outputFile, 'utf8').trim())
521
- }
522
-
523
- if (!finalThreadId) {
524
- finalThreadId = parseThreadIdFromStdout(stdoutRaw)
525
- }
629
+ refreshFinalMessageFromOutputFile()
526
630
 
527
631
  if (code !== 0) {
528
- reject(new Error(repairPossibleMojibake(extractCodexError(stderrRaw, stdoutRaw))))
632
+ settleError(new Error(repairPossibleMojibake(extractCodexError(stderrRaw, stdoutRaw))))
529
633
  return
530
634
  }
531
635
 
532
- emit(createCompletedEnvelopeEvent(finalMessage))
533
-
534
- resolve({
535
- sessionId: session.id,
536
- message: finalMessage,
537
- threadId: finalThreadId,
538
- })
636
+ settleCompleted()
539
637
  })
540
638
  }).finally(() => {
541
639
  fs.rmSync(outputFile, { force: true })
@@ -0,0 +1,467 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ createClaudeNormalizationState,
6
+ extractClaudeAssistantText,
7
+ extractClaudeResultText,
8
+ extractClaudeSessionId,
9
+ normalizeClaudeEvent,
10
+ normalizeClaudeEvents,
11
+ } from './claudeCodeRunner.js'
12
+
13
+ test('runner claudeCodeRunner maps fatal auth api_retry to error event', () => {
14
+ assert.deepEqual(
15
+ normalizeClaudeEvents({
16
+ type: 'system',
17
+ subtype: 'api_retry',
18
+ attempt: 1,
19
+ max_retries: 10,
20
+ error_status: 401,
21
+ error: 'authentication_failed',
22
+ }),
23
+ [{
24
+ type: 'error',
25
+ message: 'Claude Code 认证失败(HTTP 401 authentication_failed)。请重新登录 Claude Code,或检查当前环境中的认证令牌配置。',
26
+ }]
27
+ )
28
+ })
29
+
30
+ test('runner claudeCodeRunner maps transient api_retry to reconnecting error event', () => {
31
+ assert.deepEqual(
32
+ normalizeClaudeEvents({
33
+ type: 'system',
34
+ subtype: 'api_retry',
35
+ attempt: 2,
36
+ max_retries: 10,
37
+ error_status: 503,
38
+ error: 'overloaded',
39
+ }),
40
+ [{
41
+ type: 'error',
42
+ message: 'Reconnecting... 2/10 (HTTP 503 overloaded)',
43
+ }]
44
+ )
45
+ })
46
+
47
+ test('runner claudeCodeRunner maps Agent sub-agents into collaboration events', () => {
48
+ const state = createClaudeNormalizationState()
49
+
50
+ assert.deepEqual(
51
+ normalizeClaudeEvents({
52
+ type: 'assistant',
53
+ message: {
54
+ content: [
55
+ {
56
+ type: 'tool_use',
57
+ id: 'agent-tool-1',
58
+ name: 'Agent',
59
+ input: {
60
+ description: 'Analyze a.js exports',
61
+ subagent_type: 'general-purpose',
62
+ prompt: 'Analyze a.js in the current directory.',
63
+ model: 'sonnet',
64
+ },
65
+ },
66
+ ],
67
+ },
68
+ }, state),
69
+ [{
70
+ type: 'item.started',
71
+ item: {
72
+ type: 'collab_tool_call',
73
+ tool: 'spawn_agent',
74
+ receiver_thread_ids: [],
75
+ prompt: 'Analyze a.js in the current directory.',
76
+ agents_states: {},
77
+ },
78
+ }]
79
+ )
80
+
81
+ assert.deepEqual(
82
+ normalizeClaudeEvents({
83
+ type: 'system',
84
+ subtype: 'task_started',
85
+ tool_use_id: 'agent-tool-1',
86
+ task_id: 'task-a',
87
+ description: 'Analyze a.js exports',
88
+ }, state),
89
+ [{
90
+ type: 'item.completed',
91
+ item: {
92
+ type: 'collab_tool_call',
93
+ tool: 'spawn_agent',
94
+ receiver_thread_ids: ['task-a'],
95
+ prompt: 'Analyze a.js in the current directory.',
96
+ agents_states: {
97
+ 'task-a': {
98
+ status: 'running',
99
+ message: '',
100
+ title: 'Analyze a.js exports',
101
+ role: 'general-purpose',
102
+ target: 'a.js',
103
+ model: 'sonnet',
104
+ task_id: 'task-a',
105
+ },
106
+ },
107
+ },
108
+ }]
109
+ )
110
+ })
111
+
112
+ test('runner claudeCodeRunner maps task_completed and ignores duplicate tool_result for Agent sub-agents', () => {
113
+ const state = createClaudeNormalizationState()
114
+
115
+ normalizeClaudeEvents({
116
+ type: 'assistant',
117
+ message: {
118
+ content: [
119
+ {
120
+ type: 'tool_use',
121
+ id: 'agent-tool-2',
122
+ name: 'Agent',
123
+ input: {
124
+ description: 'Analyze b.js exports',
125
+ subagent_type: 'general-purpose',
126
+ prompt: 'Analyze b.js in the current directory.',
127
+ model: 'sonnet',
128
+ },
129
+ },
130
+ ],
131
+ },
132
+ }, state)
133
+
134
+ normalizeClaudeEvents({
135
+ type: 'system',
136
+ subtype: 'task_started',
137
+ tool_use_id: 'agent-tool-2',
138
+ task_id: 'task-b',
139
+ description: 'Analyze b.js exports',
140
+ }, state)
141
+
142
+ assert.deepEqual(
143
+ normalizeClaudeEvents({
144
+ type: 'system',
145
+ subtype: 'task_completed',
146
+ task_id: 'task-b',
147
+ result: 'found 2 exports',
148
+ description: 'Analyze b.js exports',
149
+ }, state),
150
+ [{
151
+ type: 'item.completed',
152
+ item: {
153
+ type: 'collab_tool_call',
154
+ tool: 'wait',
155
+ receiver_thread_ids: ['task-b'],
156
+ prompt: 'Analyze b.js in the current directory.',
157
+ agents_states: {
158
+ 'task-b': {
159
+ status: 'completed',
160
+ message: 'found 2 exports',
161
+ title: 'Analyze b.js exports',
162
+ role: 'general-purpose',
163
+ target: 'b.js',
164
+ model: 'sonnet',
165
+ task_id: 'task-b',
166
+ },
167
+ },
168
+ },
169
+ }]
170
+ )
171
+
172
+ assert.deepEqual(
173
+ normalizeClaudeEvents({
174
+ type: 'user',
175
+ message: {
176
+ content: [
177
+ {
178
+ type: 'tool_result',
179
+ tool_use_id: 'agent-tool-2',
180
+ content: 'duplicate result',
181
+ is_error: false,
182
+ },
183
+ ],
184
+ },
185
+ }, state),
186
+ []
187
+ )
188
+ })
189
+
190
+ test('runner claudeCodeRunner maps TodoWrite into todo_list events', () => {
191
+ const state = createClaudeNormalizationState()
192
+
193
+ assert.deepEqual(
194
+ normalizeClaudeEvents({
195
+ type: 'assistant',
196
+ message: {
197
+ content: [
198
+ {
199
+ type: 'tool_use',
200
+ id: 'todo-tool-1',
201
+ name: 'TodoWrite',
202
+ input: {
203
+ todos: [
204
+ {
205
+ content: 'Inspect relevant backend and admin codepaths',
206
+ activeForm: 'Inspecting relevant backend and admin codepaths',
207
+ status: 'in_progress',
208
+ },
209
+ {
210
+ content: 'Implement hospital database model and admin APIs',
211
+ status: 'pending',
212
+ },
213
+ {
214
+ content: 'Add tests',
215
+ status: 'completed',
216
+ },
217
+ ],
218
+ },
219
+ },
220
+ ],
221
+ },
222
+ }, state),
223
+ [{
224
+ type: 'item.started',
225
+ item: {
226
+ type: 'todo_list',
227
+ items: [
228
+ {
229
+ text: 'Inspecting relevant backend and admin codepaths',
230
+ status: 'in_progress',
231
+ completed: false,
232
+ },
233
+ {
234
+ text: 'Implement hospital database model and admin APIs',
235
+ status: 'pending',
236
+ completed: false,
237
+ },
238
+ {
239
+ text: 'Add tests',
240
+ status: 'completed',
241
+ completed: true,
242
+ },
243
+ ],
244
+ },
245
+ }]
246
+ )
247
+
248
+ assert.deepEqual(
249
+ normalizeClaudeEvents({
250
+ type: 'user',
251
+ message: {
252
+ content: [
253
+ {
254
+ type: 'tool_result',
255
+ tool_use_id: 'todo-tool-1',
256
+ content: 'Todos have been modified successfully.',
257
+ is_error: false,
258
+ },
259
+ ],
260
+ },
261
+ }, state),
262
+ [{
263
+ type: 'item.completed',
264
+ item: {
265
+ type: 'todo_list',
266
+ items: [
267
+ {
268
+ text: 'Inspecting relevant backend and admin codepaths',
269
+ status: 'in_progress',
270
+ completed: false,
271
+ },
272
+ {
273
+ text: 'Implement hospital database model and admin APIs',
274
+ status: 'pending',
275
+ completed: false,
276
+ },
277
+ {
278
+ text: 'Add tests',
279
+ status: 'completed',
280
+ completed: true,
281
+ },
282
+ ],
283
+ },
284
+ }]
285
+ )
286
+ })
287
+
288
+ test('extractClaudeAssistantText joins nested text parts', () => {
289
+ const text = extractClaudeAssistantText({
290
+ type: 'assistant',
291
+ message: {
292
+ content: [
293
+ { type: 'text', text: '第一段' },
294
+ { type: 'text', text: '第二段' },
295
+ ],
296
+ },
297
+ })
298
+
299
+ assert.equal(text, '第一段\n第二段')
300
+ })
301
+
302
+ test('extractClaudeSessionId reads common session id fields', () => {
303
+ assert.equal(extractClaudeSessionId({ session_id: 'claude-session-1' }), 'claude-session-1')
304
+ assert.equal(extractClaudeSessionId({ result: { session_id: 'claude-session-2' } }), 'claude-session-2')
305
+ })
306
+
307
+ test('normalizeClaudeEvent maps assistant output to agent message', () => {
308
+ assert.deepEqual(
309
+ normalizeClaudeEvent({
310
+ type: 'assistant',
311
+ message: {
312
+ content: [{ type: 'text', text: '已完成修改' }],
313
+ },
314
+ }),
315
+ {
316
+ type: 'item.completed',
317
+ item: {
318
+ type: 'agent_message',
319
+ text: '已完成修改',
320
+ },
321
+ }
322
+ )
323
+ })
324
+
325
+ test('normalizeClaudeEvent maps result output to turn completion', () => {
326
+ assert.deepEqual(
327
+ normalizeClaudeEvent({
328
+ type: 'result',
329
+ result: '最终回复',
330
+ }),
331
+ {
332
+ type: 'turn.completed',
333
+ result: '最终回复',
334
+ }
335
+ )
336
+
337
+ assert.equal(extractClaudeResultText({ result: '最终回复' }), '最终回复')
338
+ })
339
+
340
+ test('normalizeClaudeEvents maps system init to thread start', () => {
341
+ assert.deepEqual(
342
+ normalizeClaudeEvents({
343
+ type: 'system',
344
+ subtype: 'init',
345
+ session_id: 'claude-session-init',
346
+ }),
347
+ [{
348
+ type: 'thread.started',
349
+ thread_id: 'claude-session-init',
350
+ }]
351
+ )
352
+ })
353
+
354
+ test('normalizeClaudeEvents maps thinking, tool use and text blocks', () => {
355
+ const state = createClaudeNormalizationState()
356
+
357
+ assert.deepEqual(
358
+ normalizeClaudeEvents({
359
+ type: 'assistant',
360
+ message: {
361
+ content: [
362
+ { type: 'thinking', thinking: '先看看目录结构' },
363
+ { type: 'tool_use', id: 'tool-1', name: 'Bash', input: { command: 'ls -1' } },
364
+ { type: 'text', text: '已查看完成' },
365
+ ],
366
+ },
367
+ }, state),
368
+ [
369
+ {
370
+ type: 'item.started',
371
+ item: {
372
+ type: 'reasoning',
373
+ text: '先看看目录结构',
374
+ },
375
+ },
376
+ {
377
+ type: 'item.started',
378
+ item: {
379
+ type: 'command_execution',
380
+ command: 'Bash: ls -1',
381
+ status: 'in_progress',
382
+ },
383
+ },
384
+ {
385
+ type: 'item.completed',
386
+ item: {
387
+ type: 'agent_message',
388
+ text: '已查看完成',
389
+ },
390
+ },
391
+ ]
392
+ )
393
+ })
394
+
395
+ test('normalizeClaudeEvents maps tool results back to remembered tool call', () => {
396
+ const state = createClaudeNormalizationState()
397
+ normalizeClaudeEvents({
398
+ type: 'assistant',
399
+ message: {
400
+ content: [
401
+ { type: 'tool_use', id: 'tool-2', name: 'Bash', input: { command: 'pwd' } },
402
+ ],
403
+ },
404
+ }, state)
405
+
406
+ assert.deepEqual(
407
+ normalizeClaudeEvents({
408
+ type: 'user',
409
+ message: {
410
+ content: [
411
+ { type: 'tool_result', tool_use_id: 'tool-2', content: '/tmp/demo', is_error: false },
412
+ ],
413
+ },
414
+ }, state),
415
+ [{
416
+ type: 'item.completed',
417
+ item: {
418
+ type: 'command_execution',
419
+ command: 'Bash: pwd',
420
+ status: 'completed',
421
+ exit_code: 0,
422
+ aggregated_output: '/tmp/demo',
423
+ },
424
+ }]
425
+ )
426
+ })
427
+
428
+ test('normalizeClaudeEvents stringifies structured tool results', () => {
429
+ const state = createClaudeNormalizationState()
430
+ normalizeClaudeEvents({
431
+ type: 'assistant',
432
+ message: {
433
+ content: [
434
+ { type: 'tool_use', id: 'tool-3', name: 'Read', input: { file_path: '/tmp/demo.txt' } },
435
+ ],
436
+ },
437
+ }, state)
438
+
439
+ assert.deepEqual(
440
+ normalizeClaudeEvents({
441
+ type: 'user',
442
+ message: {
443
+ content: [
444
+ {
445
+ type: 'tool_result',
446
+ tool_use_id: 'tool-3',
447
+ content: [
448
+ { type: 'text', text: '<path>/tmp/demo.txt</path>' },
449
+ { type: 'text', text: '<type>file</type>' },
450
+ ],
451
+ },
452
+ ],
453
+ },
454
+ },
455
+ state),
456
+ [{
457
+ type: 'item.completed',
458
+ item: {
459
+ type: 'command_execution',
460
+ command: 'Read: /tmp/demo.txt',
461
+ status: 'completed',
462
+ exit_code: 0,
463
+ aggregated_output: '<path>/tmp/demo.txt</path>\n<type>file</type>',
464
+ },
465
+ }]
466
+ )
467
+ })