lark-remote 0.1.5 → 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 (55) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.en.md +0 -4
  3. package/README.md +0 -4
  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 +402 -21
  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 +77 -6
  15. package/dist/card/run-state.d.ts +25 -3
  16. package/dist/card/run-state.js +203 -2
  17. package/dist/config/dir.js +1 -1
  18. package/dist/config/index.d.ts +42 -0
  19. package/dist/config/index.js +23 -1
  20. package/dist/connector/index.d.ts +5 -0
  21. package/dist/connector/index.js +12 -0
  22. package/dist/index.js +44 -22
  23. package/dist/router/config/codex.d.ts +1 -1
  24. package/dist/router/config/codex.js +49 -0
  25. package/dist/router/config/common/render.js +4 -0
  26. package/dist/router/config/types.d.ts +1 -1
  27. package/dist/router/index.d.ts +27 -2
  28. package/dist/router/index.js +109 -28
  29. package/dist/router/utils.d.ts +2 -0
  30. package/dist/router/utils.js +5 -2
  31. package/dist/runner/codex/app-server/client.d.ts +82 -0
  32. package/dist/runner/codex/app-server/client.js +234 -0
  33. package/dist/runner/codex/app-server/connection-manager.d.ts +56 -0
  34. package/dist/runner/codex/app-server/connection-manager.js +170 -0
  35. package/dist/runner/codex/app-server/protocol-types.d.ts +527 -0
  36. package/dist/runner/codex/app-server/protocol-types.js +67 -0
  37. package/dist/runner/codex/app-server/runner.d.ts +121 -0
  38. package/dist/runner/codex/app-server/runner.js +554 -0
  39. package/dist/runner/codex/app-server/translator.d.ts +109 -0
  40. package/dist/runner/codex/app-server/translator.js +734 -0
  41. package/dist/runner/codex/app-server/transport.d.ts +49 -0
  42. package/dist/runner/codex/app-server/transport.js +231 -0
  43. package/dist/runner/codex/factory.d.ts +29 -0
  44. package/dist/runner/codex/factory.js +39 -0
  45. package/dist/runner/codex/index.d.ts +2 -0
  46. package/dist/runner/codex/index.js +2 -0
  47. package/dist/runner/codex/runner.d.ts +3 -0
  48. package/dist/runner/codex/runner.js +5 -0
  49. package/dist/runner/common/usage.d.ts +2 -0
  50. package/dist/runner/common/usage.js +2 -0
  51. package/dist/runner/index.d.ts +2 -2
  52. package/dist/runner/index.js +1 -1
  53. package/dist/runner/types.d.ts +123 -2
  54. package/dist/session/codex/rollout-reader.js +43 -0
  55. package/package.json +1 -1
@@ -0,0 +1,16 @@
1
+ /**
2
+ * renderApprovalArea: renders the approval request area in a run card.
3
+ *
4
+ * Renders command/file/permissions approval areas with button mappings per §8.3.
5
+ * Uses CardKit 2.0 (no tag:"action" container — 200861 rule).
6
+ */
7
+ import type { ApprovalView } from '../runner/types.js';
8
+ /**
9
+ * Render the approval area for a run card.
10
+ * Returns an array of CardKit 2.0 elements to append to the card body.
11
+ */
12
+ export declare function renderApprovalArea(approval: ApprovalView | undefined, opts?: {
13
+ expired?: boolean;
14
+ runId?: string;
15
+ terminal?: string;
16
+ }): object[];
@@ -0,0 +1,247 @@
1
+ /**
2
+ * renderApprovalArea: renders the approval request area in a run card.
3
+ *
4
+ * Renders command/file/permissions approval areas with button mappings per §8.3.
5
+ * Uses CardKit 2.0 (no tag:"action" container — 200861 rule).
6
+ */
7
+ /** Unique nonce for card button callbacks (SDK dedup + router validation). */
8
+ function mkNonce() {
9
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
10
+ }
11
+ /**
12
+ * Render the approval area for a run card.
13
+ * Returns an array of CardKit 2.0 elements to append to the card body.
14
+ */
15
+ export function renderApprovalArea(approval, opts) {
16
+ if (!approval)
17
+ return [];
18
+ // 终态(done/error/interrupted/idle_timeout)下 run 已结束:coordinator 已
19
+ // 释放,按钮点击无处响应,隐藏整个审批区避免"点了没反应"(P2-7)。
20
+ if (opts?.terminal && opts.terminal !== 'running' && opts.terminal !== 'finalizing') {
21
+ return [];
22
+ }
23
+ const elements = [];
24
+ if (approval.kind === 'command') {
25
+ elements.push(...renderCommandApproval(approval, opts?.expired, opts?.runId));
26
+ }
27
+ else if (approval.kind === 'file') {
28
+ elements.push(...renderFileApproval(approval, opts?.expired, opts?.runId));
29
+ }
30
+ else if (approval.kind === 'permissions') {
31
+ elements.push(...renderPermissionsApproval(approval, opts?.expired, opts?.runId));
32
+ }
33
+ return elements;
34
+ }
35
+ /**
36
+ * Render approval action buttons from the real protocol decision list.
37
+ * accept and decline are always offered (decline is a universal safety
38
+ * affordance even when the server omits it); acceptForSession and
39
+ * acceptWithExecpolicyAmendment appear only when the server offers them.
40
+ */
41
+ function renderDecisionButtons(approval, expired, runId) {
42
+ if (expired) {
43
+ return [
44
+ {
45
+ tag: 'div',
46
+ text: { tag: 'lark_md', content: '⏰ **审批已过期**' },
47
+ },
48
+ ];
49
+ }
50
+ const BUTTONS = {
51
+ accept: { label: '✅ 允许', type: 'primary' },
52
+ acceptForSession: { label: '✅ 允许本次会话', type: 'primary' },
53
+ acceptWithExecpolicyAmendment: { label: '🔒 允许并记住命令', type: 'primary' },
54
+ decline: { label: '❌ 拒绝', type: 'danger' },
55
+ };
56
+ // Fixed presentation order; only offer decisions the protocol listed
57
+ // (accept/decline are always available).
58
+ const offered = ['accept', 'acceptForSession', 'acceptWithExecpolicyAmendment', 'decline'].filter((d) => d === 'accept' || d === 'decline' || approval.availableDecisions.includes(d));
59
+ // 决策按钮逐个作为 body 直接元素返回(CardKit body 纵向堆叠 = 上下排列)。
60
+ // 不能用 column_set width:auto 横排:手机窄屏下多个按钮挤在一行,按钮文字
61
+ // 被截断成省略号,用户无法辨认审批动作。
62
+ return offered.map((d) => ({
63
+ tag: 'button',
64
+ text: { tag: 'plain_text', content: BUTTONS[d].label },
65
+ type: BUTTONS[d].type,
66
+ behaviors: [
67
+ {
68
+ type: 'callback',
69
+ value: {
70
+ cmd: 'approval.respond',
71
+ decision: d,
72
+ requestId: approval.requestId,
73
+ runId,
74
+ nonce: mkNonce(),
75
+ },
76
+ },
77
+ ],
78
+ }));
79
+ }
80
+ // =============================================================================
81
+ // Command Approval
82
+ // =============================================================================
83
+ function renderCommandApproval(approval, expired, runId) {
84
+ const elements = [];
85
+ // Command display
86
+ const cmdContent = approval.command ? `\`${approval.command.slice(0, 500)}\`` : '(no command)';
87
+ elements.push({
88
+ tag: 'div',
89
+ text: { tag: 'lark_md', content: `**⚡ 命令审批**\n${cmdContent}` },
90
+ });
91
+ if (approval.commandCwd) {
92
+ elements.push({
93
+ tag: 'div',
94
+ text: { tag: 'lark_md', content: `📂 \`${approval.commandCwd}\`` },
95
+ });
96
+ }
97
+ if (approval.reason) {
98
+ elements.push({
99
+ tag: 'div',
100
+ text: { tag: 'lark_md', content: `💡 ${approval.reason}` },
101
+ });
102
+ }
103
+ // Action buttons — derived from the real protocol decision list (see
104
+ // renderDecisionButtons): accept/decline always, acceptForSession and
105
+ // acceptWithExecpolicyAmendment only when the server offers them.
106
+ elements.push(...renderDecisionButtons(approval, expired, runId));
107
+ // Pending count
108
+ if (approval.pendingTotal > 1) {
109
+ elements.push({
110
+ tag: 'div',
111
+ text: { tag: 'lark_md', content: `📋 还有 ${approval.pendingTotal - 1} 个待审批请求` },
112
+ });
113
+ }
114
+ return elements;
115
+ }
116
+ // =============================================================================
117
+ // File Approval
118
+ // =============================================================================
119
+ function renderFileApproval(approval, expired, runId) {
120
+ const elements = [];
121
+ elements.push({
122
+ tag: 'div',
123
+ text: { tag: 'lark_md', content: '**📄 文件变更审批**' },
124
+ });
125
+ if (approval.reason) {
126
+ elements.push({
127
+ tag: 'div',
128
+ text: { tag: 'lark_md', content: `💡 ${approval.reason}` },
129
+ });
130
+ }
131
+ // File changes list
132
+ if (approval.fileChanges && approval.fileChanges.length > 0) {
133
+ // 审批区 diff 总预算:估算函数不计审批区,多文件大 diff 可能顶爆 28KB 卡
134
+ // (ErrCode 11310)。与 tool-render 的 OUTPUT_MAX/BODY_TOTAL_MAX 同思路。
135
+ const TOTAL_DIFF_CHARS = 2400;
136
+ let renderedDiffChars = 0;
137
+ let omittedDiffs = 0;
138
+ for (const change of approval.fileChanges) {
139
+ const icon = change.kind === 'add' ? '🆕' : change.kind === 'delete' ? '🗑️' : '✏️';
140
+ elements.push({
141
+ tag: 'div',
142
+ text: { tag: 'lark_md', content: `${icon} \`${change.path}\` (${change.kind})` },
143
+ });
144
+ if (change.diff) {
145
+ if (renderedDiffChars < TOTAL_DIFF_CHARS) {
146
+ elements.push({
147
+ tag: 'div',
148
+ text: { tag: 'lark_md', content: renderFileDiff(change.diff) },
149
+ });
150
+ renderedDiffChars += Math.min(change.diff.length, MAX_DIFF_CHARS);
151
+ }
152
+ else {
153
+ omittedDiffs += 1;
154
+ }
155
+ }
156
+ }
157
+ if (omittedDiffs > 0) {
158
+ elements.push({
159
+ tag: 'div',
160
+ text: { tag: 'lark_md', content: `_…${omittedDiffs} 个 diff 已省略(卡片预算)_` },
161
+ });
162
+ }
163
+ }
164
+ // Action buttons
165
+ // Derived from the real protocol decision list (see renderDecisionButtons).
166
+ elements.push(...renderDecisionButtons(approval, expired, runId));
167
+ if (approval.pendingTotal > 1) {
168
+ elements.push({
169
+ tag: 'div',
170
+ text: { tag: 'lark_md', content: `📋 还有 ${approval.pendingTotal - 1} 个待审批请求` },
171
+ });
172
+ }
173
+ return elements;
174
+ }
175
+ /**
176
+ * Render a file-change diff as a fenced code block.
177
+ * Truncates to protect the 28KB card budget and neutralizes triple backticks
178
+ * that would break the markdown fence (飞书 11311 类解析错误防线).
179
+ */
180
+ function renderFileDiff(diff) {
181
+ const sanitized = diff.replace(/```/g, '···');
182
+ const truncated = sanitized.length > MAX_DIFF_CHARS
183
+ ? `${sanitized.slice(0, MAX_DIFF_CHARS)}\n…(diff 已截断)`
184
+ : sanitized;
185
+ return `\`\`\`diff\n${truncated}\n\`\`\``;
186
+ }
187
+ /** Per-diff render cap (chars), see TOTAL_DIFF_CHARS for the approval-area cap. */
188
+ const MAX_DIFF_CHARS = 1200;
189
+ // =============================================================================
190
+ // Permissions Approval
191
+ // =============================================================================
192
+ function renderPermissionsApproval(approval, expired, runId) {
193
+ const elements = [];
194
+ elements.push({
195
+ tag: 'div',
196
+ text: { tag: 'lark_md', content: '**🔒 权限审批**' },
197
+ });
198
+ if (approval.reason) {
199
+ elements.push({
200
+ tag: 'div',
201
+ text: { tag: 'lark_md', content: `💡 ${approval.reason}` },
202
+ });
203
+ }
204
+ // Permission items with toggle buttons
205
+ if (approval.permissions?.items) {
206
+ for (const item of approval.permissions.items) {
207
+ const icon = item.selected ? '✅' : '⬜';
208
+ elements.push({
209
+ tag: 'div',
210
+ text: { tag: 'lark_md', content: `${icon} ${item.label}` },
211
+ });
212
+ elements.push({
213
+ tag: 'button',
214
+ text: {
215
+ tag: 'plain_text',
216
+ content: item.selected ? '取消授予' : '授予',
217
+ },
218
+ type: item.selected ? 'default' : 'primary',
219
+ size: 'small',
220
+ behaviors: [
221
+ {
222
+ type: 'callback',
223
+ value: {
224
+ cmd: 'approval.toggle',
225
+ requestId: approval.requestId,
226
+ permId: item.id,
227
+ selected: !item.selected,
228
+ runId,
229
+ nonce: mkNonce(),
230
+ },
231
+ },
232
+ ],
233
+ });
234
+ }
235
+ }
236
+ // Action buttons
237
+ // Derived from the real protocol decision list (see renderDecisionButtons).
238
+ elements.push(...renderDecisionButtons(approval, expired, runId));
239
+ if (approval.pendingTotal > 1) {
240
+ elements.push({
241
+ tag: 'div',
242
+ text: { tag: 'lark_md', content: `📋 还有 ${approval.pendingTotal - 1} 个待审批请求` },
243
+ });
244
+ }
245
+ return elements;
246
+ }
247
+ //# sourceMappingURL=approval-render.js.map
@@ -32,5 +32,19 @@ export declare function newSessionButton(): object;
32
32
  * agent's session reader (P2 fix: completion cards from non-default agents
33
33
  * must carry agent to avoid wrong-reader fallback).
34
34
  */
35
+ /**
36
+ * Create a CardKit 2.0 Compact button for Codex app-server mode.
37
+ * Triggers thread/compact/start via the codex.compact callback.
38
+ */
39
+ export declare function compactButton(runId: string): object;
40
+ /**
41
+ * Create a CardKit 2.0 Compact button for resume cards (auto-resume / `/resume <id>`).
42
+ * Unlike compactButton (which targets a finished run's runId), this targets a
43
+ * sessionId directly: the bridge resolves the session and calls the app-server
44
+ * runner's runCompact() without a runId. Carries agent so the handler routes
45
+ * to the correct session reader + runner when the card was rendered for a
46
+ * non-default agent.
47
+ */
48
+ export declare function resumeCompactButton(sessionId: string, agent: string): object;
35
49
  export declare function resumeUseButton(sessionId: string, agent: AgentKind): object;
36
50
  export {};
@@ -102,6 +102,34 @@ export function newSessionButton() {
102
102
  * agent's session reader (P2 fix: completion cards from non-default agents
103
103
  * must carry agent to avoid wrong-reader fallback).
104
104
  */
105
+ /**
106
+ * Create a CardKit 2.0 Compact button for Codex app-server mode.
107
+ * Triggers thread/compact/start via the codex.compact callback.
108
+ */
109
+ export function compactButton(runId) {
110
+ return {
111
+ tag: 'button',
112
+ text: { content: '🗜 Compact' },
113
+ type: 'primary',
114
+ behaviors: [{ type: 'callback', value: { cmd: 'codex.compact', runId } }],
115
+ };
116
+ }
117
+ /**
118
+ * Create a CardKit 2.0 Compact button for resume cards (auto-resume / `/resume <id>`).
119
+ * Unlike compactButton (which targets a finished run's runId), this targets a
120
+ * sessionId directly: the bridge resolves the session and calls the app-server
121
+ * runner's runCompact() without a runId. Carries agent so the handler routes
122
+ * to the correct session reader + runner when the card was rendered for a
123
+ * non-default agent.
124
+ */
125
+ export function resumeCompactButton(sessionId, agent) {
126
+ return {
127
+ tag: 'button',
128
+ text: { content: '🗜 Compact' },
129
+ type: 'primary',
130
+ behaviors: [{ type: 'callback', value: { cmd: 'resume.compact', sessionId, agent } }],
131
+ };
132
+ }
105
133
  export function resumeUseButton(sessionId, agent) {
106
134
  return {
107
135
  tag: 'button',
@@ -1,9 +1,10 @@
1
- import { terminalToColor, terminalToLabel, newSessionButton, stopButton, agentDisplayName, } from './card-shared.js';
1
+ import { terminalToColor, terminalToLabel, newSessionButton, stopButton, compactButton, agentDisplayName, } from './card-shared.js';
2
2
  import { collapsibleMarkdownPanel, markdownDiv } from './collapsible.js';
3
3
  import { toolBodyMd, toolHeaderText } from './tool-render.js';
4
4
  import { truncateUtf8, truncateMarkdownTables } from './text-truncate.js';
5
5
  import { formatTimestamp } from './time.js';
6
6
  import { formatUsageStats } from '../router/index.js';
7
+ import { renderApprovalArea } from './approval-render.js';
7
8
  const CARD_BUDGET_BYTES = 28_000;
8
9
  const REASONING_BYTES = 4_500;
9
10
  const TEXT_BYTES = 10_000;
@@ -24,6 +25,20 @@ function measureJson(obj) {
24
25
  }
25
26
  /** markdownDiv('', 'notation') 的序列化字节(空内容 div 的常量开销,估算时复用)。 */
26
27
  const EMPTY_MD_DIV_BYTES = measureJson(markdownDiv('', 'notation'));
28
+ /**
29
+ * Compact 按钮可见性:普通 run(operationKind='turn')到达任意终态即可压缩。
30
+ * 异常退出(error/interrupted/idle_timeout)时上下文往往更大,更需要压缩后
31
+ * 再继续,因此不再要求 resultSubtype==='success'。compaction 卡
32
+ * (operationKind='compaction')永不显示,防止递归 Compact。
33
+ */
34
+ function shouldShowCompactButton(state) {
35
+ if (state.operationKind !== 'turn')
36
+ return false;
37
+ return (state.terminal === 'done' ||
38
+ state.terminal === 'error' ||
39
+ state.terminal === 'interrupted' ||
40
+ state.terminal === 'idle_timeout');
41
+ }
27
42
  /**
28
43
  * Wrap text block in a collapsible panel with timestamp in the header.
29
44
  * This unifies timestamp position across all block types (thinking/plan/file_change/tool/text).
@@ -250,13 +265,17 @@ function buildDegradedElements(state, options) {
250
265
  }
251
266
  }
252
267
  elements.push(...buildSummaryContent(state));
253
- // Bottom action row: stop (if running) + new session (always)
268
+ // Bottom action row: stop (if running) + compact (if applicable) + new session (always)
254
269
  const actionButtons = [];
255
270
  // finalizing 也显示停止按钮(进程未退出,用户可 /stop)
256
271
  const showStop = state.terminal === 'running' || state.terminal === 'finalizing';
257
272
  if (showStop) {
258
273
  actionButtons.push(stopButton(state.runId));
259
274
  }
275
+ // Compact 按钮:普通 turn 到达任意终态即可压缩(含异常退出),compaction 卡除外
276
+ if (shouldShowCompactButton(state)) {
277
+ actionButtons.push(compactButton(state.runId));
278
+ }
260
279
  actionButtons.push(newSessionButton());
261
280
  if (actionButtons.length > 0) {
262
281
  elements.push({ tag: 'div', text: { content: '‎', tag: 'lark_md' } }, // spacer
@@ -269,6 +288,15 @@ function buildDegradedElements(state, options) {
269
288
  })),
270
289
  });
271
290
  }
291
+ // 审批区位于 body 最底部(底部操作行之后):待审批时决策按钮不遮挡内容流。
292
+ // 多槽渲染(review P2-3):并发审批全部展示,互不顶掉。
293
+ for (const slot of state.approvals ?? []) {
294
+ elements.push(...renderApprovalArea(slot.view, {
295
+ expired: slot.expired,
296
+ runId: state.runId,
297
+ terminal: state.terminal,
298
+ }));
299
+ }
272
300
  return elements;
273
301
  }
274
302
  /**
@@ -340,13 +368,17 @@ function buildExtremeFallbackElements(state, options) {
340
368
  elements.push(markdownDiv('_暂无输出_'));
341
369
  }
342
370
  elements.push(...buildSummaryContent(state));
343
- // Bottom action row: stop (if running) + new session (always)
371
+ // Bottom action row: stop (if running) + compact (if applicable) + new session (always)
344
372
  const actionButtons = [];
345
373
  // finalizing 也显示停止按钮(进程未退出,用户可 /stop)
346
374
  const showStop = state.terminal === 'running' || state.terminal === 'finalizing';
347
375
  if (showStop) {
348
376
  actionButtons.push(stopButton(state.runId));
349
377
  }
378
+ // Compact 按钮:普通 turn 到达任意终态即可压缩(含异常退出),compaction 卡除外
379
+ if (shouldShowCompactButton(state)) {
380
+ actionButtons.push(compactButton(state.runId));
381
+ }
350
382
  actionButtons.push(newSessionButton());
351
383
  if (actionButtons.length > 0) {
352
384
  elements.push({ tag: 'div', text: { content: '‎', tag: 'lark_md' } }, // spacer
@@ -359,6 +391,15 @@ function buildExtremeFallbackElements(state, options) {
359
391
  })),
360
392
  });
361
393
  }
394
+ // 审批区位于 body 最底部(底部操作行之后):待审批时决策按钮不遮挡内容流。
395
+ // 多槽渲染(review P2-3):并发审批全部展示,互不顶掉。
396
+ for (const slot of state.approvals ?? []) {
397
+ elements.push(...renderApprovalArea(slot.view, {
398
+ expired: slot.expired,
399
+ runId: state.runId,
400
+ terminal: state.terminal,
401
+ }));
402
+ }
362
403
  return elements;
363
404
  }
364
405
  /** Build the status row element (shared across full/degraded/extreme). */
@@ -399,13 +440,16 @@ function buildSkeletonElements(state) {
399
440
  elements.push(markdownDiv('_⚠️ 输出过大且含大量转义字符,已省略全部内容以避免超限;完整内容请查日志_'));
400
441
  // Summary (token stats etc.) — static, bounded size
401
442
  elements.push(...buildSummaryContent(state));
402
- // Bottom action row: stop (if running/finalizing) + new session (always).
443
+ // Bottom action row: stop (if running/finalizing) + compact (if applicable) + new session (always).
403
444
  // Degraded paths must keep the action buttons reachable (design constraint).
404
445
  const actionButtons = [];
405
446
  const showStop = state.terminal === 'running' || state.terminal === 'finalizing';
406
447
  if (showStop) {
407
448
  actionButtons.push(stopButton(state.runId));
408
449
  }
450
+ if (shouldShowCompactButton(state)) {
451
+ actionButtons.push(compactButton(state.runId));
452
+ }
409
453
  actionButtons.push(newSessionButton());
410
454
  elements.push({ tag: 'div', text: { content: '‎', tag: 'lark_md' } }, // spacer
411
455
  {
@@ -588,11 +632,14 @@ export function renderRunCard(state, options = {}) {
588
632
  ...buildChronologicalContent(state, options, prepared, groups),
589
633
  ...buildSummaryContent(state),
590
634
  ];
591
- // Bottom action row: stop (if running) + new session (always)
635
+ // Bottom action row: stop (if running) + compact (if applicable) + new session (always)
592
636
  const actionButtons = [];
593
637
  if (showStop) {
594
638
  actionButtons.push(stopButton(runId));
595
639
  }
640
+ if (shouldShowCompactButton(state)) {
641
+ actionButtons.push(compactButton(runId));
642
+ }
596
643
  actionButtons.push(newSessionButton());
597
644
  if (actionButtons.length > 0) {
598
645
  elements.push({ tag: 'div', text: { content: '‎', tag: 'lark_md' } }, // spacer
@@ -605,6 +652,15 @@ export function renderRunCard(state, options = {}) {
605
652
  })),
606
653
  });
607
654
  }
655
+ // 审批区位于 body 最底部(底部操作行之后):待审批时决策按钮不遮挡内容流。
656
+ // 多槽渲染(review P2-3):并发审批全部展示,互不顶掉。
657
+ for (const slot of state.approvals ?? []) {
658
+ elements.push(...renderApprovalArea(slot.view, {
659
+ expired: slot.expired,
660
+ runId: state.runId,
661
+ terminal: state.terminal,
662
+ }));
663
+ }
608
664
  const card = {
609
665
  schema: '2.0',
610
666
  config: { wide_screen_mode: true, update_multi: true },
@@ -765,7 +821,10 @@ function buildSummaryContent(state) {
765
821
  elements.push(markdownDiv(`⚠️ **运行出错**\n\n${state.errorMsg ?? '未知错误'}`));
766
822
  }
767
823
  else if (state.terminal === 'interrupted') {
768
- elements.push(markdownDiv('⏹ **已被用户终止**'));
824
+ // 审批超时(approval_expired 已标记)是「无人响应被自动取消」,不是用户
825
+ // 主动终止;如实展示原因,避免用户误判为 Agent 出错或自己操作过。
826
+ const expiredApproval = (state.approvals ?? []).some((a) => a.expired);
827
+ elements.push(markdownDiv(expiredApproval ? '⏰ **审批超时未响应,已自动取消**' : '⏹ **已被用户终止**'));
769
828
  }
770
829
  else if (state.terminal === 'idle_timeout') {
771
830
  elements.push(markdownDiv(`⏱ **已超时**\n\n${state.idleTimeoutMinutes ?? 0} 分钟无响应,已自动终止`));
@@ -778,6 +837,7 @@ function buildSummaryContent(state) {
778
837
  contextLength: state.contextLength,
779
838
  contextLimit: state.contextLimit,
780
839
  compactCount: state.compactCount,
840
+ compactPreContextLength: state.compactPreContextLength,
781
841
  cacheReadTokens: state.cacheReadTokens,
782
842
  cacheCreationTokens: state.cacheCreationTokens,
783
843
  totalTokens: state.totalTokens,
@@ -801,6 +861,9 @@ function buildDurationInfo(state) {
801
861
  state.terminal === 'idle_timeout') {
802
862
  return `⏱ ${state.footer || '已完成'}`;
803
863
  }
864
+ // 与标题一致:审批等待期间状态行显示「等待审批中」,而不是「工具调用中」。
865
+ if (hasPendingApproval(state))
866
+ return '✋ 等待审批中';
804
867
  return footerText2(state.footer);
805
868
  }
806
869
  function footerText2(footer) {
@@ -827,6 +890,10 @@ function headerTitle2(state, options = {}) {
827
890
  return `⏱ ${agent} · 已超时`;
828
891
  if (state.terminal === 'finalizing')
829
892
  return `⏳ ${agent} · 完成中`;
893
+ // 审批等待期间 server 暂停 turn,命令工具停在 tool_running——标题必须提示
894
+ // 正在等人工决策,而不是「调用工具」(2026-08-14 UX)。
895
+ if (hasPendingApproval(state))
896
+ return `✋ ${agent} · 等待审批`;
830
897
  if (state.footer === 'thinking')
831
898
  return `💭 ${agent} · 思考中`;
832
899
  if (state.footer === 'streaming')
@@ -835,6 +902,10 @@ function headerTitle2(state, options = {}) {
835
902
  return `🔧 ${agent} · 调用工具`;
836
903
  return `🔴 ${agent} · 运行中`;
837
904
  }
905
+ /** 是否有未过期(仍可操作)的待审批请求。 */
906
+ function hasPendingApproval(state) {
907
+ return (state.approvals ?? []).some((a) => !a.expired);
908
+ }
838
909
  /** CardKit 2.0 header template color */
839
910
  function headerTemplate2(state) {
840
911
  return terminalToColor(state.terminal);
@@ -1,4 +1,4 @@
1
- import type { AgentEvent } from '../runner/index.js';
1
+ import type { AgentEvent, ApprovalView } from '../runner/index.js';
2
2
  export type RunTerminal = 'running' | 'finalizing' | 'done' | 'error' | 'interrupted' | 'idle_timeout';
3
3
  export type RunFooter = 'thinking' | 'tool_running' | 'streaming' | null;
4
4
  type ToolStatus = 'running' | 'ok' | 'error';
@@ -23,27 +23,36 @@ export interface ToolEntry {
23
23
  }
24
24
  export type RunBlock = {
25
25
  kind: 'thinking';
26
+ /** app-server item id; exec-mode blocks leave it undefined. */
27
+ itemId?: string;
26
28
  content: string;
27
29
  active: boolean;
28
30
  timestamp?: string;
31
+ completedAt?: string;
29
32
  } | {
30
33
  kind: 'text';
34
+ itemId?: string;
31
35
  content: string;
32
36
  timestamp?: string;
37
+ completedAt?: string;
33
38
  } | {
34
39
  kind: 'tool';
35
40
  tool: ToolEntry;
36
41
  } | {
37
42
  kind: 'plan';
43
+ itemId?: string;
38
44
  content: string;
39
45
  active: boolean;
40
46
  timestamp?: string;
47
+ completedAt?: string;
41
48
  } | {
42
49
  kind: 'file_change';
50
+ itemId?: string;
43
51
  path: string;
44
52
  operation: 'create' | 'edit' | 'delete' | 'read';
45
53
  diff?: string;
46
54
  timestamp?: string;
55
+ completedAt?: string;
47
56
  };
48
57
  export interface RunState {
49
58
  runId: string;
@@ -51,7 +60,7 @@ export interface RunState {
51
60
  footer: RunFooter;
52
61
  blocks: RunBlock[];
53
62
  sessionId?: string;
54
- resultSubtype?: 'success' | 'error';
63
+ resultSubtype?: 'success' | 'error' | 'interrupted';
55
64
  contextLength?: number;
56
65
  /** 当前模型 context window 上限(codex jsonl 提供);卡片据此渲染百分比。 */
57
66
  contextLimit?: number;
@@ -77,15 +86,28 @@ export interface RunState {
77
86
  /** 会话累计 cache creation token(所有 run 之和)。 */
78
87
  cumulativeCacheCreationTokens?: number;
79
88
  plan?: string;
89
+ /** Codex 操作类型:'turn' 表示普通 run,'compaction' 表示 compact 运行。 */
90
+ operationKind?: 'turn' | 'compaction';
91
+ /** 压缩前上下文水位(codex compact 卡展示「压缩前 X → 压缩后 Y」)。 */
92
+ compactPreContextLength?: number;
93
+ /**
94
+ * 待审批请求列表(app-server 模式,review P2-3)。同一 turn 内并发多个审批
95
+ * 时全部渲染,避免后到者顶掉先到者的按钮(单槽会丢 UI,只能等超时 cancel)。
96
+ */
97
+ approvals?: Array<{
98
+ view: ApprovalView;
99
+ expired?: boolean;
100
+ }>;
80
101
  }
81
102
  export interface FinishMeta {
82
- resultSubtype?: 'success' | 'error';
103
+ resultSubtype?: 'success' | 'error' | 'interrupted';
83
104
  contextLength?: number;
84
105
  /** 当前模型 context window 上限;透传到 RunState 供 done 卡片显示百分比。 */
85
106
  contextLimit?: number;
86
107
  errorMsg?: string;
87
108
  idleTimeoutMinutes?: number;
88
109
  compactCount?: number;
110
+ compactPreContextLength?: number;
89
111
  cacheReadTokens?: number;
90
112
  cacheCreationTokens?: number;
91
113
  /** Agent-declared total; threaded to RunState for the done card. */