lark-remote 0.1.4 → 0.1.6

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 (75) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.en.md +0 -1
  3. package/README.md +0 -1
  4. package/dist/bridge/approval-coordinator.d.ts +108 -0
  5. package/dist/bridge/approval-coordinator.js +258 -0
  6. package/dist/bridge/index.d.ts +62 -0
  7. package/dist/bridge/index.js +423 -20
  8. package/dist/bridge/queue-manager.d.ts +9 -0
  9. package/dist/bridge/queue-manager.js +19 -11
  10. package/dist/card/approval-render.d.ts +16 -0
  11. package/dist/card/approval-render.js +247 -0
  12. package/dist/card/card-shared.d.ts +14 -0
  13. package/dist/card/card-shared.js +28 -0
  14. package/dist/card/run-renderer.js +78 -7
  15. package/dist/card/run-state.d.ts +25 -3
  16. package/dist/card/run-state.js +210 -2
  17. package/dist/config/dir.d.ts +1 -0
  18. package/dist/config/dir.js +4 -0
  19. package/dist/config/index.d.ts +43 -0
  20. package/dist/config/index.js +25 -1
  21. package/dist/connector/index.d.ts +5 -0
  22. package/dist/connector/index.js +32 -0
  23. package/dist/index.js +95 -23
  24. package/dist/router/config/codex.d.ts +1 -1
  25. package/dist/router/config/codex.js +49 -0
  26. package/dist/router/config/common/render.js +4 -0
  27. package/dist/router/config/index.d.ts +0 -10
  28. package/dist/router/config/index.js +0 -17
  29. package/dist/router/config/types.d.ts +1 -1
  30. package/dist/router/index.d.ts +61 -1
  31. package/dist/router/index.js +323 -48
  32. package/dist/router/utils.d.ts +2 -0
  33. package/dist/router/utils.js +5 -2
  34. package/dist/runner/codex/app-server/client.d.ts +82 -0
  35. package/dist/runner/codex/app-server/client.js +234 -0
  36. package/dist/runner/codex/app-server/connection-manager.d.ts +56 -0
  37. package/dist/runner/codex/app-server/connection-manager.js +170 -0
  38. package/dist/runner/codex/app-server/protocol-types.d.ts +527 -0
  39. package/dist/runner/codex/app-server/protocol-types.js +67 -0
  40. package/dist/runner/codex/app-server/runner.d.ts +121 -0
  41. package/dist/runner/codex/app-server/runner.js +554 -0
  42. package/dist/runner/codex/app-server/translator.d.ts +109 -0
  43. package/dist/runner/codex/app-server/translator.js +734 -0
  44. package/dist/runner/codex/app-server/transport.d.ts +49 -0
  45. package/dist/runner/codex/app-server/transport.js +231 -0
  46. package/dist/runner/codex/argv.d.ts +1 -1
  47. package/dist/runner/codex/argv.js +1 -1
  48. package/dist/runner/codex/factory.d.ts +29 -0
  49. package/dist/runner/codex/factory.js +39 -0
  50. package/dist/runner/codex/index.d.ts +2 -0
  51. package/dist/runner/codex/index.js +2 -0
  52. package/dist/runner/codex/runner.d.ts +3 -0
  53. package/dist/runner/codex/runner.js +5 -0
  54. package/dist/runner/common/runner-utils.d.ts +12 -0
  55. package/dist/runner/common/runner-utils.js +21 -0
  56. package/dist/runner/common/spawning-runner.js +9 -1
  57. package/dist/runner/common/usage.d.ts +2 -0
  58. package/dist/runner/common/usage.js +2 -0
  59. package/dist/runner/index.d.ts +2 -2
  60. package/dist/runner/index.js +1 -1
  61. package/dist/runner/types.d.ts +123 -2
  62. package/dist/session/claude/session-index.d.ts +93 -0
  63. package/dist/session/claude/session-index.js +374 -0
  64. package/dist/session/claude/sessions.d.ts +0 -18
  65. package/dist/session/claude/sessions.js +102 -319
  66. package/dist/session/codex/rollout-reader.js +46 -3
  67. package/dist/update/index.d.ts +10 -0
  68. package/dist/update/index.js +11 -0
  69. package/dist/update/install.d.ts +29 -0
  70. package/dist/update/install.js +93 -0
  71. package/dist/update/startup-hint.d.ts +5 -0
  72. package/dist/update/startup-hint.js +11 -0
  73. package/dist/update/version-check.d.ts +31 -0
  74. package/dist/update/version-check.js +149 -0
  75. package/package.json +1 -14
@@ -0,0 +1,734 @@
1
+ /**
2
+ * CodexAppServerTranslator: translates protocol notifications and server
3
+ * requests into AgentEvents.
4
+ *
5
+ * The translator is stateful per turn — it tracks the current turn's state,
6
+ * accumulates deltas, and synthesizes the final ResultEvent when the turn
7
+ * completes. Method names and parameter shapes follow the real Codex
8
+ * app-server v2 protocol (see protocol-types.ts).
9
+ */
10
+ import { NotificationMethod, ServerRequestMethod, UNSUPPORTED_SERVER_REQUEST_METHODS, } from './protocol-types.js';
11
+ // =============================================================================
12
+ // Translator
13
+ // =============================================================================
14
+ export class CodexAppServerTranslator {
15
+ turnId = '';
16
+ threadId = '';
17
+ turnStatus = 'pending';
18
+ /** Full agentMessage text per item id (item-scoped snapshot). */
19
+ textByItem = new Map();
20
+ /**
21
+ * Reasoning content per item id: paragraphs keyed by `contentIndex`
22
+ * (textDelta) plus the summary stream (summaryTextDelta). Each reasoning
23
+ * item is isolated so interleaved items keep their own chronology.
24
+ */
25
+ reasoningByItem = new Map();
26
+ /** Full command output per item id (item-scoped snapshot). */
27
+ toolOutputByItem = new Map();
28
+ /** Full plan text per item id (item-scoped snapshot). */
29
+ planByItem = new Map();
30
+ /** commandExecution items that emitted item/started (tool block position anchor). */
31
+ startedToolItems = new Set();
32
+ /** fileChange items seen this turn, keyed by item id (item/started). */
33
+ fileChangeItems = new Map();
34
+ /** File approvals whose item/started has not arrived yet (out-of-order flow). */
35
+ pendingFileApprovalByItemId = new Map();
36
+ usage = { inputTokens: 0, outputTokens: 0 };
37
+ operationKind = 'exec';
38
+ /**
39
+ * Handle a notification from the server and return events.
40
+ */
41
+ handleNotification(method, params) {
42
+ switch (method) {
43
+ case NotificationMethod.TURN_STARTED:
44
+ return this.handleTurnStarted(params);
45
+ case NotificationMethod.ITEM_STARTED:
46
+ return this.handleItemStarted(params);
47
+ case NotificationMethod.AGENT_MESSAGE_DELTA:
48
+ return this.handleAgentMessageDelta(params);
49
+ case NotificationMethod.REASONING_SUMMARY_TEXT_DELTA:
50
+ return this.handleReasoningDelta(params);
51
+ case NotificationMethod.REASONING_SUMMARY_PART_ADDED:
52
+ return this.handleReasoningPartAdded();
53
+ case NotificationMethod.REASONING_TEXT_DELTA:
54
+ return this.handleReasoningTextDelta(params);
55
+ case NotificationMethod.ITEM_COMPLETED:
56
+ return this.handleItemCompleted(params);
57
+ case NotificationMethod.COMMAND_EXECUTION_OUTPUT_DELTA:
58
+ return this.handleCommandOutputDelta(params);
59
+ case NotificationMethod.PLAN_DELTA:
60
+ return this.handlePlanUpdated(params);
61
+ case NotificationMethod.TOKEN_USAGE_UPDATED:
62
+ return this.handleTokenUsageUpdated(params);
63
+ case NotificationMethod.TURN_COMPLETED:
64
+ return this.handleTurnCompleted(params);
65
+ case NotificationMethod.ERROR:
66
+ return this.handleError(params);
67
+ case NotificationMethod.THREAD_COMPACTED:
68
+ return this.handleThreadCompacted();
69
+ case NotificationMethod.SERVER_REQUEST_RESOLVED:
70
+ return this.handleServerRequestResolved(params);
71
+ default:
72
+ return [];
73
+ }
74
+ }
75
+ /**
76
+ * Handle a server request (incoming JSON-RPC request from server, e.g. approval).
77
+ */
78
+ handleServerRequest(id, method, params) {
79
+ if (UNSUPPORTED_SERVER_REQUEST_METHODS.has(method)) {
80
+ return [];
81
+ }
82
+ switch (method) {
83
+ case ServerRequestMethod.COMMAND_EXECUTION_APPROVAL:
84
+ return this.handleCommandApproval(id, params);
85
+ case ServerRequestMethod.FILE_CHANGE_APPROVAL:
86
+ return this.handleFileApproval(id, params);
87
+ case ServerRequestMethod.PERMISSIONS_APPROVAL:
88
+ return this.handlePermissionsApproval(id, params);
89
+ default:
90
+ return [];
91
+ }
92
+ }
93
+ /**
94
+ * Synthesize a ResultEvent from the current turn state.
95
+ * Called when the turn completes.
96
+ */
97
+ handleTurnCompleted(params) {
98
+ const turn = params?.turn;
99
+ this.turnStatus = turn?.status ?? 'completed';
100
+ const events = [];
101
+ // 兜底:turn.items 里权威 agentMessage / plan item 与已流式内容不一致或
102
+ // 未发过(如 itemsView 摘要只含部分 item)时,补发 item 级快照。已一致则
103
+ // 跳过,避免冗余事件(run-state 按 itemId 原地替换、幂等)。
104
+ for (const item of turn?.items ?? []) {
105
+ if (item.type === 'agentMessage') {
106
+ const text = item.text ?? '';
107
+ if (text && this.textByItem.get(item.id) !== text) {
108
+ this.textByItem.set(item.id, text);
109
+ events.push({
110
+ type: 'turn_diff',
111
+ text,
112
+ itemId: item.id,
113
+ complete: true,
114
+ threadId: params?.threadId ?? this.threadId,
115
+ turnId: params?.turn?.id ?? this.turnId,
116
+ timestamp: this.now(),
117
+ });
118
+ }
119
+ }
120
+ else if (item.type === 'plan') {
121
+ // plan 是 EXPERIMENTAL 文本增量,不保证与最终 plan item 一致;
122
+ // turn/completed 的权威 plan item 以此兜底校正。
123
+ const planText = item.text ?? '';
124
+ if (planText && this.planByItem.get(item.id) !== planText) {
125
+ this.planByItem.set(item.id, planText);
126
+ events.push({
127
+ type: 'turn_diff',
128
+ plan: planText,
129
+ itemId: item.id,
130
+ complete: true,
131
+ threadId: params?.threadId ?? this.threadId,
132
+ turnId: params?.turn?.id ?? this.turnId,
133
+ timestamp: this.now(),
134
+ });
135
+ }
136
+ }
137
+ }
138
+ const usage = {
139
+ input_tokens: this.usage.inputTokens,
140
+ output_tokens: this.usage.outputTokens,
141
+ cache_read_tokens: this.usage.cachedInputTokens,
142
+ total_tokens: this.usage.totalTokens,
143
+ context_limit: this.usage.contextLimit,
144
+ };
145
+ // 语义保真:interrupted(审批超时/取消、/stop)与 failed(Agent 真失败)
146
+ // 是性质不同的终态。interrupted 走独立 subtype,卡片不再误报「Agent 返回
147
+ // 错误结果」;failed 保持 error。
148
+ const subtype = turn?.status === 'failed'
149
+ ? 'error'
150
+ : turn?.status === 'interrupted'
151
+ ? 'interrupted'
152
+ : 'success';
153
+ events.push({
154
+ type: 'result',
155
+ subtype,
156
+ session_id: this.threadId,
157
+ usage,
158
+ errorMessage: turn?.error?.message,
159
+ });
160
+ return events;
161
+ }
162
+ /**
163
+ * Get the current usage stats.
164
+ */
165
+ getUsage() {
166
+ return {
167
+ inputTokens: this.usage.inputTokens,
168
+ outputTokens: this.usage.outputTokens,
169
+ };
170
+ }
171
+ /**
172
+ * Get the current turn status.
173
+ */
174
+ getTurnStatus() {
175
+ return this.turnStatus;
176
+ }
177
+ /**
178
+ * Set the operation kind (exec vs compact).
179
+ */
180
+ setOperationKind(kind) {
181
+ this.operationKind = kind;
182
+ }
183
+ // =========================================================================
184
+ // Private notification handlers
185
+ // =========================================================================
186
+ handleTurnStarted(params) {
187
+ this.threadId = params.threadId;
188
+ this.turnId = params.turn.id;
189
+ this.turnStatus = params.turn.status ?? 'inProgress';
190
+ this.textByItem.clear();
191
+ this.reasoningByItem.clear();
192
+ this.toolOutputByItem.clear();
193
+ this.planByItem.clear();
194
+ this.startedToolItems.clear();
195
+ this.fileChangeItems.clear();
196
+ this.pendingFileApprovalByItemId.clear();
197
+ return [
198
+ {
199
+ type: 'turn_started',
200
+ threadId: params.threadId,
201
+ turnId: params.turn.id,
202
+ operationKind: this.operationKind === 'compact' ? 'compaction' : 'turn',
203
+ timestamp: this.now(),
204
+ },
205
+ ];
206
+ }
207
+ handleAgentMessageDelta(params) {
208
+ const prev = this.textByItem.get(params.itemId) ?? '';
209
+ const next = prev + params.delta;
210
+ this.textByItem.set(params.itemId, next);
211
+ return [
212
+ {
213
+ type: 'turn_diff',
214
+ text: next,
215
+ itemId: params.itemId,
216
+ threadId: params.threadId,
217
+ turnId: params.turnId,
218
+ timestamp: this.now(),
219
+ },
220
+ ];
221
+ }
222
+ handleReasoningDelta(params) {
223
+ // summaryTextDelta 流的是摘要文本;与 content 段落分开累积,避免段落交错。
224
+ const entry = this.reasoningEntry(params.itemId);
225
+ entry.summary += params.delta;
226
+ return [
227
+ {
228
+ type: 'turn_diff',
229
+ reasoning: this.reasoningSnapshotFor(entry),
230
+ itemId: params.itemId,
231
+ threadId: params.threadId,
232
+ turnId: params.turnId,
233
+ timestamp: this.now(),
234
+ },
235
+ ];
236
+ }
237
+ handleReasoningPartAdded() {
238
+ // 真实协议 `summaryPartAdded` 只携带 itemId/summaryIndex,不携带文本;
239
+ // 摘要内容在 item/completed 的 reasoning item 里(handleItemCompleted)。
240
+ // 此处保持 no-op,避免把 undefined 拼进推理快照。
241
+ return [];
242
+ }
243
+ handleItemCompleted(params) {
244
+ const item = params.item;
245
+ if (item.type === 'fileChange' && Array.isArray(item.changes)) {
246
+ // 权威文件变更(含聚合后的 diff);以快照语义替换本 item 的变更列表。
247
+ this.fileChangeItems.set(item.id, item.changes);
248
+ return [
249
+ {
250
+ type: 'turn_diff',
251
+ fileChanges: normalizeFileChanges(item.changes),
252
+ itemId: item.id,
253
+ complete: true,
254
+ threadId: params.threadId,
255
+ turnId: params.turnId,
256
+ timestamp: this.now(),
257
+ },
258
+ ];
259
+ }
260
+ if (item.type === 'reasoning') {
261
+ const content = Array.isArray(item.content) ? item.content : [];
262
+ const summary = Array.isArray(item.summary) ? item.summary : [];
263
+ if (content.length > 0 || summary.length > 0) {
264
+ // 权威内容:summary 优先展示,content 段落保留(per-item 替换)。
265
+ const entry = this.reasoningEntry(item.id);
266
+ entry.content = new Map(content.map((c, i) => [i, c]));
267
+ entry.summary = summary.join('\n');
268
+ return [
269
+ {
270
+ type: 'turn_diff',
271
+ reasoning: this.reasoningSnapshotFor(entry),
272
+ itemId: item.id,
273
+ complete: true,
274
+ threadId: params.threadId,
275
+ turnId: params.turnId,
276
+ timestamp: this.now(),
277
+ },
278
+ ];
279
+ }
280
+ }
281
+ if (item.type === 'commandExecution') {
282
+ // 无 outputDelta 流时(短命令),用 item/completed 的聚合输出兜底;
283
+ // 已流式输出则以累积快照收尾(complete 语义标记工具完成)。
284
+ const accumulated = this.toolOutputByItem.get(item.id) ?? '';
285
+ const aggregated = item.aggregatedOutput;
286
+ const authoritative = aggregated != null ? String(aggregated) : accumulated;
287
+ // 从未 item/started 且无任何输出(如审批被拒的空命令)→ 不产生工具块,
288
+ // 避免完成时刻凭空冒出一个空命令块。
289
+ if (!authoritative && !this.startedToolItems.has(item.id)) {
290
+ return [];
291
+ }
292
+ this.toolOutputByItem.set(item.id, authoritative);
293
+ return [
294
+ {
295
+ type: 'turn_diff',
296
+ toolOutput: authoritative,
297
+ itemId: item.id,
298
+ complete: true,
299
+ toolStatus: commandExecutionStatusToToolStatus(item.status),
300
+ threadId: params.threadId,
301
+ turnId: params.turnId,
302
+ timestamp: this.now(),
303
+ },
304
+ ];
305
+ }
306
+ if (item.type === 'agentMessage') {
307
+ // item/completed 的 agentMessage 带权威全文(text 字段);以 item 级
308
+ // 快照收尾,run-state 按 itemId 原地替换,保证内容收敛到权威值。
309
+ const text = item.text ?? '';
310
+ const accumulated = this.textByItem.get(item.id) ?? '';
311
+ // 空最终文本但已有流式内容(异常边界)→ 用累积内容收尾,补齐 completedAt;
312
+ // 完全无内容则没有块可完成,不发事件。
313
+ if (text || accumulated) {
314
+ const finalText = text || accumulated;
315
+ this.textByItem.set(item.id, finalText);
316
+ return [
317
+ {
318
+ type: 'turn_diff',
319
+ text: finalText,
320
+ itemId: item.id,
321
+ complete: true,
322
+ threadId: params.threadId,
323
+ turnId: params.turnId,
324
+ timestamp: this.now(),
325
+ },
326
+ ];
327
+ }
328
+ }
329
+ if (item.type === 'plan') {
330
+ const text = item.text ?? '';
331
+ if (text) {
332
+ this.planByItem.set(item.id, text);
333
+ return [
334
+ {
335
+ type: 'turn_diff',
336
+ plan: text,
337
+ itemId: item.id,
338
+ complete: true,
339
+ threadId: params.threadId,
340
+ turnId: params.turnId,
341
+ timestamp: this.now(),
342
+ },
343
+ ];
344
+ }
345
+ }
346
+ return [];
347
+ }
348
+ handleReasoningTextDelta(params) {
349
+ const index = params.contentIndex ?? 0;
350
+ const entry = this.reasoningEntry(params.itemId);
351
+ const prev = entry.content.get(index) ?? '';
352
+ entry.content.set(index, prev + params.delta);
353
+ return [
354
+ {
355
+ type: 'turn_diff',
356
+ reasoning: this.reasoningSnapshotFor(entry),
357
+ itemId: params.itemId,
358
+ threadId: params.threadId,
359
+ turnId: params.turnId,
360
+ timestamp: this.now(),
361
+ },
362
+ ];
363
+ }
364
+ handleCommandOutputDelta(params) {
365
+ const prev = this.toolOutputByItem.get(params.itemId) ?? '';
366
+ const next = prev + params.delta;
367
+ this.toolOutputByItem.set(params.itemId, next);
368
+ return [
369
+ {
370
+ type: 'turn_diff',
371
+ toolOutput: next,
372
+ itemId: params.itemId,
373
+ threadId: params.threadId,
374
+ turnId: params.turnId,
375
+ timestamp: this.now(),
376
+ },
377
+ ];
378
+ }
379
+ handlePlanUpdated(params) {
380
+ // 真实协议 plan/delta 是纯文本增量(EXPERIMENTAL),不能假设拼接与最终
381
+ // plan item 一致;item/completed 会带权威 plan item(handleItemCompleted)。
382
+ const prev = this.planByItem.get(params.itemId) ?? '';
383
+ const next = prev + params.delta;
384
+ this.planByItem.set(params.itemId, next);
385
+ return [
386
+ {
387
+ type: 'turn_diff',
388
+ plan: next,
389
+ itemId: params.itemId,
390
+ threadId: params.threadId,
391
+ turnId: params.turnId,
392
+ timestamp: this.now(),
393
+ },
394
+ ];
395
+ }
396
+ handleTokenUsageUpdated(params) {
397
+ const last = params.tokenUsage.last;
398
+ this.usage = {
399
+ inputTokens: last.inputTokens,
400
+ outputTokens: last.outputTokens,
401
+ cachedInputTokens: last.cachedInputTokens,
402
+ totalTokens: last.totalTokens,
403
+ contextLimit: params.tokenUsage.modelContextWindow,
404
+ };
405
+ return [];
406
+ }
407
+ handleError(params) {
408
+ if (params.willRetry) {
409
+ // 非终态错误:服务端会重试该 turn。提前结束 run 会造成桥与 server
410
+ // 状态脱节(server 继续跑、卡片却已收场)。仅记录,等待后续通知。
411
+ return [];
412
+ }
413
+ return [
414
+ {
415
+ type: 'result',
416
+ subtype: 'error',
417
+ session_id: this.threadId,
418
+ errorMessage: params.error?.message ?? 'Codex app-server error',
419
+ },
420
+ ];
421
+ }
422
+ handleThreadCompacted() {
423
+ return [
424
+ {
425
+ type: 'result',
426
+ subtype: 'success',
427
+ session_id: this.threadId,
428
+ },
429
+ ];
430
+ }
431
+ handleServerRequestResolved(params) {
432
+ // 真实协议 serverRequest/resolved 只携带 requestId/threadId,无 outcome;
433
+ // 过期态由桥侧 ApprovalCoordinator 的 timer 单独产出(approval_expired)。
434
+ // requestId 保留原始 wire 值(schema: string | integer),不 Number() 归一,
435
+ // 避免非数字字符串 id 被转成 NaN 后与 pendingApprovals / 卡片对不上。
436
+ return [
437
+ {
438
+ type: 'approval_resolved',
439
+ requestId: params.requestId,
440
+ outcome: 'resolved',
441
+ timestamp: new Date().toISOString(),
442
+ },
443
+ ];
444
+ }
445
+ // =========================================================================
446
+ // Private approval handlers
447
+ // =========================================================================
448
+ handleCommandApproval(requestId, params) {
449
+ const { decisions, payloads } = normalizeCommandDecisions(params.availableDecisions);
450
+ const view = {
451
+ requestId,
452
+ kind: 'command',
453
+ threadShort: params.threadId.slice(0, 8),
454
+ turnShort: params.turnId.slice(0, 8),
455
+ workspace: params.cwd ?? '',
456
+ command: params.command ?? undefined,
457
+ commandCwd: params.cwd ?? undefined,
458
+ reason: params.reason ?? undefined,
459
+ network: params.networkApprovalContext
460
+ ? {
461
+ host: params.networkApprovalContext.host,
462
+ protocol: params.networkApprovalContext.protocol,
463
+ }
464
+ : undefined,
465
+ availableDecisions: decisions,
466
+ ...(payloads ? { decisionPayloads: payloads } : {}),
467
+ pendingTotal: 1,
468
+ };
469
+ return [
470
+ {
471
+ type: 'approval_requested',
472
+ requestId,
473
+ kind: 'command',
474
+ threadId: params.threadId,
475
+ turnId: params.turnId,
476
+ itemId: params.itemId,
477
+ view,
478
+ timestamp: new Date().toISOString(),
479
+ },
480
+ ];
481
+ }
482
+ handleFileApproval(requestId, params) {
483
+ // 真实协议:变更信息在 item/started 的 fileChange item(changes[])里,
484
+ // 审批请求只带 itemId(grantRoot/reason 实测为 null)。优先按 itemId 关联;
485
+ // 关联不到时退回 grantRoot 合成条目(旧协议/兜底)。
486
+ const storedChanges = this.fileChangeItems.get(params.itemId);
487
+ const fileChanges = storedChanges
488
+ ? normalizeFileChanges(storedChanges)
489
+ : params.grantRoot
490
+ ? [{ path: params.grantRoot, kind: 'update', diff: '' }]
491
+ : undefined;
492
+ const view = {
493
+ requestId,
494
+ kind: 'file',
495
+ threadShort: params.threadId.slice(0, 8),
496
+ turnShort: params.turnId.slice(0, 8),
497
+ workspace: '',
498
+ fileChanges,
499
+ reason: params.reason ?? undefined,
500
+ // 真实 FileChangeApprovalDecision 枚举:accept/acceptForSession/decline/cancel。
501
+ availableDecisions: ['accept', 'acceptForSession', 'decline', 'cancel'],
502
+ pendingTotal: 1,
503
+ };
504
+ // 乱序流:审批先到、item/started 后到。先出事件(回退信息),item 到达时
505
+ // 以 approval_view_updated 补全,避免卡片永久空白。
506
+ if (!storedChanges) {
507
+ this.pendingFileApprovalByItemId.set(params.itemId, {
508
+ requestId,
509
+ view,
510
+ });
511
+ }
512
+ return [
513
+ {
514
+ type: 'approval_requested',
515
+ requestId,
516
+ kind: 'file',
517
+ threadId: params.threadId,
518
+ turnId: params.turnId,
519
+ itemId: params.itemId,
520
+ view,
521
+ timestamp: new Date().toISOString(),
522
+ },
523
+ ];
524
+ }
525
+ handleItemStarted(params) {
526
+ const item = params.item;
527
+ if (item.type === 'fileChange' && Array.isArray(item.changes)) {
528
+ this.fileChangeItems.set(item.id, item.changes);
529
+ const pending = this.pendingFileApprovalByItemId.get(item.id);
530
+ if (pending) {
531
+ this.pendingFileApprovalByItemId.delete(item.id);
532
+ const fileChanges = normalizeFileChanges(item.changes);
533
+ if (fileChanges.length > 0) {
534
+ return [
535
+ {
536
+ type: 'approval_view_updated',
537
+ requestId: pending.requestId,
538
+ view: { ...pending.view, fileChanges },
539
+ timestamp: new Date().toISOString(),
540
+ },
541
+ {
542
+ type: 'turn_diff',
543
+ fileChanges,
544
+ itemId: item.id,
545
+ threadId: params.threadId,
546
+ turnId: params.turnId,
547
+ timestamp: this.now(),
548
+ },
549
+ ];
550
+ }
551
+ }
552
+ return [
553
+ {
554
+ type: 'turn_diff',
555
+ fileChanges: normalizeFileChanges(item.changes),
556
+ itemId: item.id,
557
+ threadId: params.threadId,
558
+ turnId: params.turnId,
559
+ timestamp: this.now(),
560
+ },
561
+ ];
562
+ }
563
+ if (item.type === 'commandExecution') {
564
+ // item/started 锚定工具块位置(真实时序):命令开始时即创建工具块,
565
+ // 而不是等到首个输出 delta——否则后续 reasoning item 会插到 command 之前。
566
+ this.startedToolItems.add(item.id);
567
+ return [
568
+ {
569
+ type: 'turn_diff',
570
+ toolOutput: '',
571
+ itemId: item.id,
572
+ threadId: params.threadId,
573
+ turnId: params.turnId,
574
+ timestamp: this.now(),
575
+ },
576
+ ];
577
+ }
578
+ return [];
579
+ }
580
+ handlePermissionsApproval(requestId, params) {
581
+ const permItems = this.buildPermissionItems(params.permissions);
582
+ const view = {
583
+ requestId,
584
+ kind: 'permissions',
585
+ threadShort: params.threadId.slice(0, 8),
586
+ turnShort: params.turnId.slice(0, 8),
587
+ workspace: params.cwd,
588
+ reason: params.reason ?? undefined,
589
+ availableDecisions: ['accept', 'decline', 'cancel'],
590
+ pendingTotal: 1,
591
+ permissions: {
592
+ items: permItems,
593
+ },
594
+ };
595
+ return [
596
+ {
597
+ type: 'approval_requested',
598
+ requestId,
599
+ kind: 'permissions',
600
+ threadId: params.threadId,
601
+ turnId: params.turnId,
602
+ itemId: params.itemId,
603
+ view,
604
+ timestamp: new Date().toISOString(),
605
+ },
606
+ ];
607
+ }
608
+ buildPermissionItems(profile) {
609
+ const items = [];
610
+ // entries 是结构化形式([{ path, access }]),read/write 是 legacy 字符串数组。
611
+ for (const entry of profile.fileSystem?.entries ?? []) {
612
+ if (entry.access === 'read' || entry.access === 'write') {
613
+ items.push({
614
+ id: `fs-${entry.access}:${entry.path}`,
615
+ label: `${entry.access === 'read' ? 'Read' : 'Write'}: ${entry.path}`,
616
+ target: { kind: entry.access === 'read' ? 'fsRead' : 'fsWrite', path: entry.path },
617
+ selected: false,
618
+ });
619
+ }
620
+ }
621
+ for (const path of profile.fileSystem?.read ?? []) {
622
+ if (!items.some((i) => i.id === `fs-read:${path}`)) {
623
+ items.push({
624
+ id: `fs-read:${path}`,
625
+ label: `Read: ${path}`,
626
+ target: { kind: 'fsRead', path },
627
+ selected: false,
628
+ });
629
+ }
630
+ }
631
+ for (const path of profile.fileSystem?.write ?? []) {
632
+ if (!items.some((i) => i.id === `fs-write:${path}`)) {
633
+ items.push({
634
+ id: `fs-write:${path}`,
635
+ label: `Write: ${path}`,
636
+ target: { kind: 'fsWrite', path },
637
+ selected: false,
638
+ });
639
+ }
640
+ }
641
+ if (profile.network?.enabled) {
642
+ // 真实 AdditionalNetworkPermissions 只有 enabled 布尔开关(整体网络访问)。
643
+ items.push({
644
+ id: 'net:all',
645
+ label: '网络访问',
646
+ target: { kind: 'network' },
647
+ selected: false,
648
+ });
649
+ }
650
+ return items;
651
+ }
652
+ /** Get (or lazily create) the reasoning accumulator for one item. */
653
+ reasoningEntry(itemId) {
654
+ let entry = this.reasoningByItem.get(itemId);
655
+ if (!entry) {
656
+ entry = { content: new Map(), summary: '' };
657
+ this.reasoningByItem.set(itemId, entry);
658
+ }
659
+ return entry;
660
+ }
661
+ /** Compose the display snapshot for one reasoning item (content + summary). */
662
+ reasoningSnapshotFor(entry) {
663
+ const content = [...entry.content.entries()]
664
+ .sort((a, b) => a[0] - b[0])
665
+ .map(([, text]) => text)
666
+ .join('\n');
667
+ return entry.summary ? `${content}\n${entry.summary}` : content;
668
+ }
669
+ /** Receive-time stamp, aligned with the exec JSONL parse path. */
670
+ now() {
671
+ return new Date().toISOString();
672
+ }
673
+ }
674
+ /** Normalize protocol change kind to the ApprovalView kind vocabulary. */
675
+ function normalizeChangeKind(kind) {
676
+ if (kind.type === 'add')
677
+ return 'add';
678
+ if (kind.type === 'delete')
679
+ return 'delete';
680
+ // 'move' and 'update' both surface as an update in the approval view.
681
+ return 'update';
682
+ }
683
+ /**
684
+ * Map protocol commandExecution item.status to the card tool status.
685
+ * 真实枚举值未在协议 schema 中约束;对已知失败语义做防御性映射,
686
+ * 未知/空值一律视为成功(与旧行为一致)。
687
+ */
688
+ function commandExecutionStatusToToolStatus(status) {
689
+ if (!status)
690
+ return 'ok';
691
+ const s = status.toLowerCase();
692
+ return s === 'failed' ||
693
+ s === 'error' ||
694
+ s === 'cancelled' ||
695
+ s === 'canceled' ||
696
+ s === 'interrupted'
697
+ ? 'error'
698
+ : 'ok';
699
+ }
700
+ function normalizeFileChanges(changes) {
701
+ return changes.map((change) => ({
702
+ path: change.path,
703
+ kind: normalizeChangeKind(change.kind),
704
+ diff: change.diff,
705
+ }));
706
+ }
707
+ /**
708
+ * Normalize the real protocol decision list into canonical string keys plus
709
+ * raw payloads for structured decisions (e.g. acceptWithExecpolicyAmendment).
710
+ * Falls back to the legacy default when the server sends no list.
711
+ */
712
+ function normalizeCommandDecisions(list) {
713
+ if (!list || list.length === 0)
714
+ return { decisions: ['accept', 'decline', 'cancel'] };
715
+ const decisions = [];
716
+ const payloads = {};
717
+ for (const decision of list) {
718
+ if (typeof decision === 'string') {
719
+ decisions.push(decision);
720
+ }
721
+ else if (decision && typeof decision === 'object') {
722
+ const key = Object.keys(decision)[0];
723
+ if (key) {
724
+ decisions.push(key);
725
+ payloads[key] = decision[key];
726
+ }
727
+ }
728
+ }
729
+ return {
730
+ decisions,
731
+ ...(Object.keys(payloads).length > 0 ? { payloads } : {}),
732
+ };
733
+ }
734
+ //# sourceMappingURL=translator.js.map