lark-remote 0.4.5 → 0.5.0

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 (124) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/bridge/approval-coordinator.d.ts +1 -1
  3. package/dist/bridge/approval-coordinator.js +3 -3
  4. package/dist/bridge/inbound-media.js +10 -7
  5. package/dist/bridge/index.d.ts +9 -9
  6. package/dist/bridge/index.js +41 -41
  7. package/dist/bridge/queue-manager.d.ts +12 -12
  8. package/dist/bridge/queue-manager.js +19 -19
  9. package/dist/card/approval-render.d.ts +1 -1
  10. package/dist/card/approval-render.js +6 -6
  11. package/dist/card/bash-card-session.d.ts +3 -3
  12. package/dist/card/bash-card-session.js +3 -3
  13. package/dist/card/bash-renderer.js +1 -1
  14. package/dist/card/card-session.d.ts +4 -4
  15. package/dist/card/card-session.js +6 -6
  16. package/dist/card/run-card-session.js +2 -2
  17. package/dist/card/run-renderer.d.ts +1 -1
  18. package/dist/card/run-renderer.js +14 -14
  19. package/dist/card/run-state.d.ts +2 -2
  20. package/dist/card/run-state.js +3 -3
  21. package/dist/card/tool-render.d.ts +1 -1
  22. package/dist/card/tool-render.js +6 -6
  23. package/dist/clone.d.ts +5 -3
  24. package/dist/clone.js +8 -4
  25. package/dist/common/truncate.d.ts +1 -1
  26. package/dist/common/truncate.js +2 -2
  27. package/dist/common/ttl-cache.d.ts +2 -2
  28. package/dist/common/ttl-cache.js +2 -2
  29. package/dist/config/codex-config.d.ts +9 -9
  30. package/dist/config/codex-config.js +18 -18
  31. package/dist/config/dir.js +5 -2
  32. package/dist/config/index.d.ts +3 -3
  33. package/dist/config/index.js +6 -6
  34. package/dist/config/opencode-config.js +3 -3
  35. package/dist/connector/index.d.ts +5 -5
  36. package/dist/connector/index.js +24 -11
  37. package/dist/inbound/event.d.ts +1 -1
  38. package/dist/inbound/event.js +1 -1
  39. package/dist/inbound/placeholder.d.ts +2 -2
  40. package/dist/inbound/placeholder.js +2 -2
  41. package/dist/index.js +9 -9
  42. package/dist/instance-lock.js +4 -3
  43. package/dist/logger/index.d.ts +1 -1
  44. package/dist/logger/index.js +4 -4
  45. package/dist/persistence/atomic-write.d.ts +1 -1
  46. package/dist/persistence/atomic-write.js +6 -2
  47. package/dist/platform/agent-stopper.d.ts +1 -1
  48. package/dist/platform/identity.d.ts +7 -1
  49. package/dist/platform/identity.js +2 -2
  50. package/dist/platform/path.d.ts +6 -0
  51. package/dist/platform/path.js +14 -8
  52. package/dist/platform/select.d.ts +1 -1
  53. package/dist/platform/select.js +1 -1
  54. package/dist/platform/shell.d.ts +2 -3
  55. package/dist/platform/spawn.d.ts +2 -2
  56. package/dist/platform/spawn.js +2 -2
  57. package/dist/platform/terminator-posix.js +1 -1
  58. package/dist/platform/terminator-win32.js +2 -2
  59. package/dist/platform/terminator.d.ts +3 -3
  60. package/dist/platform/terminator.js +2 -2
  61. package/dist/platform/types.d.ts +1 -1
  62. package/dist/restart.d.ts +1 -1
  63. package/dist/restart.js +5 -3
  64. package/dist/router/config/codex.d.ts +1 -1
  65. package/dist/router/config/codex.js +2 -2
  66. package/dist/router/config/opencode.js +1 -1
  67. package/dist/router/index.d.ts +9 -9
  68. package/dist/router/index.js +34 -34
  69. package/dist/router/order-exec-dispatch.js +2 -2
  70. package/dist/runner/bash/runner.d.ts +2 -2
  71. package/dist/runner/bash/runner.js +6 -6
  72. package/dist/runner/claude/runner.js +1 -1
  73. package/dist/runner/claude/session.d.ts +2 -2
  74. package/dist/runner/claude/session.js +14 -14
  75. package/dist/runner/codex/app-server/runner.js +3 -3
  76. package/dist/runner/codex/app-server/translator.d.ts +2 -2
  77. package/dist/runner/codex/app-server/translator.js +5 -5
  78. package/dist/runner/common/acp/acp-approval.d.ts +2 -2
  79. package/dist/runner/common/acp/acp-approval.js +3 -3
  80. package/dist/runner/common/acp/base-acp-runner.js +1 -1
  81. package/dist/runner/common/acp/base-translator.d.ts +3 -3
  82. package/dist/runner/common/acp/base-translator.js +4 -4
  83. package/dist/runner/common/acp/protocol-types.js +1 -1
  84. package/dist/runner/common/connection-based-runner.d.ts +1 -1
  85. package/dist/runner/common/connection-based-runner.js +2 -2
  86. package/dist/runner/common/jsonl-stream.d.ts +2 -2
  87. package/dist/runner/common/jsonl-stream.js +12 -12
  88. package/dist/runner/common/jsonrpc/client.d.ts +1 -1
  89. package/dist/runner/common/jsonrpc/client.js +1 -1
  90. package/dist/runner/common/jsonrpc/connection-manager.d.ts +1 -1
  91. package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
  92. package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
  93. package/dist/runner/common/jsonrpc/transport.js +2 -2
  94. package/dist/runner/common/spawning-runner.d.ts +21 -8
  95. package/dist/runner/common/spawning-runner.js +41 -15
  96. package/dist/runner/dsh/runner.js +2 -2
  97. package/dist/runner/kimi/acp/runner.d.ts +3 -3
  98. package/dist/runner/kimi/acp/runner.js +9 -9
  99. package/dist/runner/kimi/acp/translator.d.ts +2 -2
  100. package/dist/runner/kimi/acp/translator.js +4 -4
  101. package/dist/runner/opencode/acp/runner.d.ts +2 -2
  102. package/dist/runner/opencode/acp/runner.js +3 -3
  103. package/dist/runner/opencode/acp/translator.js +1 -1
  104. package/dist/runner/pi/rpc/runner.js +2 -2
  105. package/dist/runner/pi/rpc/translator.d.ts +1 -1
  106. package/dist/runner/pi/rpc/translator.js +4 -4
  107. package/dist/runner/types.d.ts +1 -1
  108. package/dist/session/claude/session-index.d.ts +12 -0
  109. package/dist/session/claude/session-index.js +20 -5
  110. package/dist/session/claude/sessions.js +10 -10
  111. package/dist/session/codex/rollout-reader.d.ts +4 -4
  112. package/dist/session/codex/rollout-reader.js +12 -12
  113. package/dist/session/common/two-pass.js +2 -2
  114. package/dist/session/common/usage-accumulator.d.ts +2 -2
  115. package/dist/session/common/usage-accumulator.js +2 -2
  116. package/dist/session/dsh/sessions.js +2 -2
  117. package/dist/session/kimi/sessions.d.ts +2 -2
  118. package/dist/session/kimi/sessions.js +11 -11
  119. package/dist/session/opencode/sessions.d.ts +3 -3
  120. package/dist/session/opencode/sessions.js +12 -10
  121. package/dist/session/pi/sessions.js +13 -12
  122. package/dist/session/session-store.d.ts +1 -1
  123. package/dist/session/session-store.js +1 -1
  124. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { getLogger } from '../logger/index.js';
2
2
  import { displayName } from '../platform/path.js';
3
3
  /**
4
- * Per-workspace cap on the number of tasks WAITING to execute (review P2-5).
4
+ * Per-workspace cap on the number of tasks WAITING to execute.
5
5
  * Without a bound, a message flood grows `queuedTasks` / the promise chain /
6
6
  * each task's captured `msg.content` closure without limit, and every enqueued
7
7
  * task fires a queue card (amplifying outbound API calls). When the waiting
@@ -21,7 +21,7 @@ export class QueueManager {
21
21
  /** Per-workspace queued tasks for queue display cards. Key = cwd, value = task list. */
22
22
  queuedTasks = new Map();
23
23
  /**
24
- * P3-5: per-workspace `messageId → QueuedTask` index mirroring `queuedTasks`.
24
+ * per-workspace `messageId → QueuedTask` index mirroring `queuedTasks`.
25
25
  * O(1) lookup for the task-start removal path (replacing the old double
26
26
  * `find`+`findIndex` O(N) scan) and for `getQueuedTask`/`removeFromQueue`/
27
27
  * `updateQueuedTaskMessage`. The ordered array remains the source of truth
@@ -48,7 +48,7 @@ export class QueueManager {
48
48
  * identifies one task's execution period for the interrupt bookkeeping in
49
49
  * `executingSlot`/`interruptedSlots` — replacing the unowned skip-credit
50
50
  * counter, which any settle could consume and which therefore leaked a
51
- * re-armed count after repeated resets (see A4 anchor).
51
+ * re-armed count after repeated resets.
52
52
  */
53
53
  slotCounter = 0;
54
54
  /**
@@ -72,7 +72,7 @@ export class QueueManager {
72
72
  /**
73
73
  * MessageIds of tasks that have actually begun executing (cancellation check
74
74
  * passed, task removed from the queue). Entries are sticky: they remain
75
- * after the task settles (A21). `handleQueueImmediate` step 6 uses this to
75
+ * after the task settles. `handleQueueImmediate` step 6 uses this to
76
76
  * distinguish "target was cancelled (never began)" from "target began while
77
77
  * the interrupt was in flight" so the final feedback can report the true
78
78
  * state instead of telling a running task's user that nothing was
@@ -95,7 +95,7 @@ export class QueueManager {
95
95
  this.sendCard = sendCard;
96
96
  this.updateCard = updateCard;
97
97
  }
98
- /** P3-5: register a queued task in the per-workspace `messageId → task` index. */
98
+ /** register a queued task in the per-workspace `messageId → task` index. */
99
99
  indexAdd(cwd, task) {
100
100
  let idx = this.taskIndex.get(cwd);
101
101
  if (!idx) {
@@ -104,11 +104,11 @@ export class QueueManager {
104
104
  }
105
105
  idx.set(task.messageId, task);
106
106
  }
107
- /** P3-5: drop a task from the per-workspace index by messageId. */
107
+ /** drop a task from the per-workspace index by messageId. */
108
108
  indexRemove(cwd, messageId) {
109
109
  this.taskIndex.get(cwd)?.delete(messageId);
110
110
  }
111
- /** P3-5: O(1) lookup of a queued task by messageId via the index. */
111
+ /** O(1) lookup of a queued task by messageId via the index. */
112
112
  indexGet(cwd, messageId) {
113
113
  return this.taskIndex.get(cwd)?.get(messageId);
114
114
  }
@@ -147,7 +147,7 @@ export class QueueManager {
147
147
  const currentExecutingCount = this.pendingOrExecutingCount.get(cwd) ?? 0;
148
148
  const currentQueueLength = this.queuedTasks.get(cwd)?.length ?? 0;
149
149
  const hasWaitingTasks = currentExecutingCount > 0 || currentQueueLength > 0;
150
- // P2-5: bound the waiting backlog. When the queue is full, reject the
150
+ // bound the waiting backlog. When the queue is full, reject the
151
151
  // task with a visible card instead of appending it (which would grow
152
152
  // the promise chain + closures unboundedly under a message flood and
153
153
  // fire another queue card). Do NOT increment pendingOrExecutingCount —
@@ -217,7 +217,7 @@ export class QueueManager {
217
217
  let livePreview = taskMeta?.messagePreview ?? '';
218
218
  // Check if this task was cancelled before executing
219
219
  if (messageId) {
220
- // P3-5: O(1) index lookup replaces the old `find` + `findIndex`
220
+ // O(1) index lookup replaces the old `find` + `findIndex`
221
221
  // double O(N) scan. The ordered array still drives removal (splice
222
222
  // preserves queue order for position display); the index stays in
223
223
  // sync via indexRemove.
@@ -290,7 +290,7 @@ export class QueueManager {
290
290
  .then(() => {
291
291
  getLogger().debug(`[queue-manager] task end cwd=${cwd}`);
292
292
  // beganMessageIds entry intentionally retained: sticky by design
293
- // (A21) — the marker records "has ever begun", not "is currently
293
+ // the marker records "has ever begun", not "is currently
294
294
  // running", so a task that began and settled quickly still reports
295
295
  // "已开始执行" to queue.immediate.
296
296
  this.decrementExecutingCount(cwd, slotId);
@@ -298,7 +298,7 @@ export class QueueManager {
298
298
  .catch((err) => {
299
299
  getLogger().error('[queue-manager] queue task error:', err);
300
300
  // Same sticky rationale as the success settle: the marker survives
301
- // even when the task errors after beginning (A21).
301
+ // even when the task errors after beginning.
302
302
  this.decrementExecutingCount(cwd, slotId);
303
303
  });
304
304
  this.queues.set(cwd, newQueue);
@@ -435,7 +435,7 @@ export class QueueManager {
435
435
  * state — pending cards enable both, executing/cancelled cards disable
436
436
  * both. Centralizing the pair here eliminates the 3-way duplication
437
437
  * between `buildQueueStatusCardElements`, `updateQueueCardToExecuting`,
438
- * and `updateQueueCardToCancelled` (Clean Code P2-1).
438
+ * and `updateQueueCardToCancelled` (Clean Code).
439
439
  */
440
440
  buildQueueActionButtons(cwd, messageId, disabled) {
441
441
  return [
@@ -476,8 +476,8 @@ export class QueueManager {
476
476
  try {
477
477
  // Prefer the live preview: an edited task's messagePreview is updated in
478
478
  // place, and the passed-in snapshot may be stale if the card send was in
479
- // flight while the user edited (A19). The begin path (started=true) already
480
- // passes the live preview captured at begin (A12), so the fallback is only
479
+ // flight while the user edited. The begin path (started=true) already
480
+ // passes the live preview captured at begin, so the fallback is only
481
481
  // exercised when the task is no longer queued.
482
482
  const liveTask = this.indexGet(cwd, messageId);
483
483
  const stillQueued = liveTask !== undefined;
@@ -527,7 +527,7 @@ export class QueueManager {
527
527
  const taskList = this.queuedTasks.get(cwd);
528
528
  if (!taskList)
529
529
  return false;
530
- // P3-5: index is the O(1) presence check; array splice keeps order.
530
+ // index is the O(1) presence check; array splice keeps order.
531
531
  if (!this.indexGet(cwd, messageId))
532
532
  return false;
533
533
  const index = taskList.findIndex((t) => t.messageId === messageId);
@@ -582,7 +582,7 @@ export class QueueManager {
582
582
  }
583
583
  /** Get task metadata from queue. */
584
584
  getQueuedTask(cwd, messageId) {
585
- // P3-5: O(1) index lookup instead of array `find`.
585
+ // O(1) index lookup instead of array `find`.
586
586
  return this.indexGet(cwd, messageId);
587
587
  }
588
588
  /**
@@ -600,7 +600,7 @@ export class QueueManager {
600
600
  }
601
601
  /** Update the messagePreview for a queued task. Returns true if found and updated. */
602
602
  updateQueuedTaskMessage(cwd, messageId, newMessage) {
603
- // P3-5: O(1) index lookup instead of array `find`.
603
+ // O(1) index lookup instead of array `find`.
604
604
  const task = this.indexGet(cwd, messageId);
605
605
  if (!task)
606
606
  return false;
@@ -697,7 +697,7 @@ export class QueueManager {
697
697
  * slot no longer matches the interrupted task's slot, the interrupted task
698
698
  * already decremented normally — resetting now would zero the count of the
699
699
  * running successor and mark ITS slot interrupted, hiding it from the
700
- * queue card (A22).
700
+ * queue card.
701
701
  */
702
702
  resetExecutingCount(cwd, expectedSlot) {
703
703
  const currentSlot = this.executingSlot.get(cwd);
@@ -705,7 +705,7 @@ export class QueueManager {
705
705
  getLogger().debug(`[queue-manager] resetExecutingCount skip (stopped task settled, slot advanced) cwd=${cwd} expectedSlot=${expectedSlot} currentSlot=${currentSlot ?? 'none'}`);
706
706
  return;
707
707
  }
708
- // P3#9: only reset and grant an interrupt slot when there is actually a
708
+ // only reset and grant an interrupt slot when there is actually a
709
709
  // pending/executing task.
710
710
  const currentCount = this.pendingOrExecutingCount.get(cwd) ?? 0;
711
711
  if (currentCount === 0) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * renderApprovalArea: renders the approval request area in a run card.
3
3
  *
4
- * Renders command/file/permissions approval areas with button mappings per §8.3.
4
+ * Renders command/file/permissions approval areas with button mappings.
5
5
  * Uses CardKit 2.0 (no tag:"action" container — 200861 rule).
6
6
  */
7
7
  import type { ApprovalView } from '../runner/types.js';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * renderApprovalArea: renders the approval request area in a run card.
3
3
  *
4
- * Renders command/file/permissions approval areas with button mappings per §8.3.
4
+ * Renders command/file/permissions approval areas with button mappings.
5
5
  * Uses CardKit 2.0 (no tag:"action" container — 200861 rule).
6
6
  */
7
7
  import { collapsibleMarkdownPanel } from './collapsible.js';
@@ -13,7 +13,7 @@ function mkNonce() {
13
13
  }
14
14
  /** 计划面板正文字节预算(折叠展示 + 截断保护 28KB 卡片预算)。 */
15
15
  const PLAN_PANEL_BYTES = 6000;
16
- /** reason div(3 处共用);反引号中和防 lark_md 解析错误(review P2-2)。 */
16
+ /** reason div(3 处共用);反引号中和防 lark_md 解析错误。 */
17
17
  function reasonDiv(approval) {
18
18
  if (!approval.reason)
19
19
  return null;
@@ -39,7 +39,7 @@ export function renderApprovalArea(approval, opts) {
39
39
  if (!approval)
40
40
  return [];
41
41
  // 终态(done/error/interrupted/idle_timeout)下 run 已结束:coordinator 已
42
- // 释放,按钮点击无处响应,隐藏整个审批区避免"点了没反应"(P2-7)。
42
+ // 释放,按钮点击无处响应,隐藏整个审批区避免"点了没反应"。
43
43
  if (opts?.terminal && opts.terminal !== 'running' && opts.terminal !== 'finalizing') {
44
44
  return [];
45
45
  }
@@ -238,7 +238,7 @@ function renderPlanFeedbackInput(requestId, runId) {
238
238
  function renderCommandApproval(approval, expired, runId) {
239
239
  const elements = [];
240
240
  // Command display
241
- // review P2-2:命令里的反引号会提前终止 lark_md 行内代码 span,奇数次
241
+ // 命令里的反引号会提前终止 lark_md 行内代码 span,奇数次
242
242
  // 反引号极易触发 11311 解析错误导致整卡失败(同 renderFileDiff 的中和策略)。
243
243
  const neutralizedCommand = (approval.command ?? '').replace(/`/g, '·');
244
244
  const cmdContent = neutralizedCommand
@@ -422,7 +422,7 @@ function renderQuestionApproval(approval, expired, runId) {
422
422
  elements.push(renderOptionRow(approval.requestId, i, option, isSelected, icon, runId));
423
423
  }
424
424
  // 自定义答案(Other)的选中态展示:选项按钮无法表示自由文本,单独显示
425
- // 已选文本,避免「点了没反应」的困惑(review P3)。
425
+ // 已选文本,避免「点了没反应」的困惑。
426
426
  const customSelected = q.selected?.find((s) => !q.options.some((o) => o.label === s));
427
427
  if (customSelected) {
428
428
  elements.push({
@@ -430,7 +430,7 @@ function renderQuestionApproval(approval, expired, runId) {
430
430
  text: { tag: 'lark_md', content: `✍️ 自定义答案:${customSelected}` },
431
431
  });
432
432
  }
433
- // review P3-4:单选问题提供自定义答案(Other)输入——输入文本后提交
433
+ // 单选问题提供自定义答案(Other)输入——输入文本后提交
434
434
  // (移动端键盘回车/完成键,桌面端输入框右侧提交图标),input_value 经
435
435
  // connector raw 事件回传。
436
436
  // 显隐按 isOther !== false:Kimi form 会丢弃非声明选项值,翻译时置
@@ -1,7 +1,7 @@
1
1
  import { CardSession, type CardChannel } from './card-session.js';
2
2
  import { type BashState, type BashRenderOptions } from './bash-renderer.js';
3
3
  /**
4
- * Store-time 输出上限(字符,P1-3 层①)。§P1-3 建议「store-time 截断,
4
+ * Store-time 输出上限(字符,层①)。建议「store-time 截断,
5
5
  * 保留尾部,上限如 24~64KB」——取区间下界 24_000 字符(字符上限 ×4 字节最坏
6
6
  * ~96KB/字段)。渲染层本就只展示尾部(bash-renderer 截断到 12KB 字节),store 截断
7
7
  * 不影响显示内容,只保证内存有界:!yes / !cat 大文件不再把全部输出驻留在 session
@@ -9,7 +9,7 @@ import { type BashState, type BashRenderOptions } from './bash-renderer.js';
9
9
  */
10
10
  export declare const BASH_OUTPUT_STORE_CAP = 24000;
11
11
  /**
12
- * Store-time 截断入口(P1-3 层①/层④共用)。bridge 本地 output/stderr 与 session
12
+ * Store-time 截断入口(层①/层④共用)。bridge 本地 output/stderr 与 session
13
13
  * state 都走它,保证任意路径驻留的字符串 ≤ BASH_OUTPUT_STORE_CAP 字符。
14
14
  * 调用方传入的 value 通常已 ≤ CAP(增量场景 output 恒 ≤ CAP,+chunk 后 ≤ CAP+chunk),
15
15
  * slice(-CAP) 成本 O(CAP+chunk),不会把 O(n²) 截断问题带回桥接循环。
@@ -49,7 +49,7 @@ export declare class BashCardSession extends CardSession<BashState, BashRenderOp
49
49
  output?: string;
50
50
  }): Promise<void>;
51
51
  /**
52
- * 合并 patch 并对 output/stderr 做 store-time 截断(P1-3 层①)。
52
+ * 合并 patch 并对 output/stderr 做 store-time 截断(层①)。
53
53
  * 截断发生在入口(store 时),渲染层无需再为大 state 付代价。
54
54
  */
55
55
  private applyCap;
@@ -2,7 +2,7 @@ import { CardSession } from './card-session.js';
2
2
  import { renderBashCard } from './bash-renderer.js';
3
3
  import { keepTail } from '../common/truncate.js';
4
4
  /**
5
- * Store-time 输出上限(字符,P1-3 层①)。§P1-3 建议「store-time 截断,
5
+ * Store-time 输出上限(字符,层①)。建议「store-time 截断,
6
6
  * 保留尾部,上限如 24~64KB」——取区间下界 24_000 字符(字符上限 ×4 字节最坏
7
7
  * ~96KB/字段)。渲染层本就只展示尾部(bash-renderer 截断到 12KB 字节),store 截断
8
8
  * 不影响显示内容,只保证内存有界:!yes / !cat 大文件不再把全部输出驻留在 session
@@ -10,7 +10,7 @@ import { keepTail } from '../common/truncate.js';
10
10
  */
11
11
  export const BASH_OUTPUT_STORE_CAP = 24_000;
12
12
  /**
13
- * Store-time 截断入口(P1-3 层①/层④共用)。bridge 本地 output/stderr 与 session
13
+ * Store-time 截断入口(层①/层④共用)。bridge 本地 output/stderr 与 session
14
14
  * state 都走它,保证任意路径驻留的字符串 ≤ BASH_OUTPUT_STORE_CAP 字符。
15
15
  * 调用方传入的 value 通常已 ≤ CAP(增量场景 output 恒 ≤ CAP,+chunk 后 ≤ CAP+chunk),
16
16
  * slice(-CAP) 成本 O(CAP+chunk),不会把 O(n²) 截断问题带回桥接循环。
@@ -92,7 +92,7 @@ export class BashCardSession extends CardSession {
92
92
  this.release();
93
93
  }
94
94
  /**
95
- * 合并 patch 并对 output/stderr 做 store-time 截断(P1-3 层①)。
95
+ * 合并 patch 并对 output/stderr 做 store-time 截断(层①)。
96
96
  * 截断发生在入口(store 时),渲染层无需再为大 state 付代价。
97
97
  */
98
98
  applyCap(patch) {
@@ -78,7 +78,7 @@ function assembleCard(state, elements) {
78
78
  };
79
79
  }
80
80
  /**
81
- * Build output/stderr elements shared by normal & degraded paths (W2.3 单源):
81
+ * Build output/stderr elements shared by normal & degraded paths (单源):
82
82
  * 除截断预算(OUTPUT_MAX_BYTES / DEGRADED_OUTPUT_BYTES)外逐行同构。
83
83
  */
84
84
  function buildOutputElements(state, maxBytes) {
@@ -59,7 +59,7 @@ export declare abstract class CardSession<S, RO = unknown> {
59
59
  * 调度一次延迟 flush。已有 pending 调度则复用(合批核心);否则起一个
60
60
  * coalesceMs 定时器。flushInFlight 期间不再起 timer(避免重入叠加),但置
61
61
  * pendingReschedule 以便 in-flight flush 完成后重新调度——否则 in-flight 期间
62
- * 到达的 push/update 会停留为 stale 中间帧(P2-1)。
62
+ * 到达的 push/update 会停留为 stale 中间帧。
63
63
  */
64
64
  protected scheduleFlush(): void;
65
65
  /**
@@ -87,14 +87,14 @@ export declare abstract class CardSession<S, RO = unknown> {
87
87
  /** The messageId of the streaming card, available after `start()` resolves. */
88
88
  get cardMessageId(): string | undefined;
89
89
  /**
90
- * P1-3 合批窗口(ms)。连续事件各触发一次全卡 render + patch 是流式 CPU 主要
90
+ * 合批窗口(ms)。连续事件各触发一次全卡 render + patch 是流式 CPU 主要
91
91
  * 来源;窗口内多个事件复用同一次延迟 flush。窗口大小可通过 constructor 覆盖
92
92
  * (测试用小窗口/0 禁用)。run/bash 子类共用的 flush 骨架(scheduleFlush/flush/
93
93
  * cancelPendingFlush/settle)下沉到基类。
94
94
  */
95
95
  protected readonly coalesceMs: number;
96
96
  private flushTimer;
97
- private flushInFlight; /** P2-1:in-flight flush 期间到达的 push/update 标记。scheduleFlush 在
97
+ private flushInFlight; /** in-flight flush 期间到达的 push/update 标记。scheduleFlush 在
98
98
  * flushInFlight 时早退不调度新 timer,但事件已 reduce 进 state;若无此标志,
99
99
  * in-flight flush 完成后该事件无 follow-up render,停留为 stale 中间帧。
100
100
  * finally 检查此标志后重新 scheduleFlush,保证 in-flight 期间累积的事件最终被渲染。 */
@@ -102,7 +102,7 @@ export declare abstract class CardSession<S, RO = unknown> {
102
102
  /** 当前 in-flight flush 的 promise(若 flush 正在 await updateCard)。
103
103
  * finish() 在终态 patch 前 await 它,保证 in-flight 的 pre-terminal patch 先落地、
104
104
  * terminal patch 后落地——否则两条 controller.update 并发无 FIFO,
105
- * pre-terminal 可能在 terminal 之后 resolve 留下"思考中"终帧(P2)。 */
105
+ * pre-terminal 可能在 terminal 之后 resolve 留下"思考中"终帧。 */
106
106
  protected flushP: Promise<void> | undefined;
107
107
  start(): Promise<void>;
108
108
  settle(): Promise<StreamSettleResult>;
@@ -46,7 +46,7 @@ export class CardSession {
46
46
  * 调度一次延迟 flush。已有 pending 调度则复用(合批核心);否则起一个
47
47
  * coalesceMs 定时器。flushInFlight 期间不再起 timer(避免重入叠加),但置
48
48
  * pendingReschedule 以便 in-flight flush 完成后重新调度——否则 in-flight 期间
49
- * 到达的 push/update 会停留为 stale 中间帧(P2-1)。
49
+ * 到达的 push/update 会停留为 stale 中间帧。
50
50
  */
51
51
  scheduleFlush() {
52
52
  if (this.flushTimer)
@@ -82,7 +82,7 @@ export class CardSession {
82
82
  this.flushInFlight = false;
83
83
  // flushInFlight 守卫保证同一时刻只有一个 in-flight flush,故可无条件清空。
84
84
  this.flushP = undefined;
85
- // P2-1:in-flight 期间若有 push/update 到达(pendingReschedule),重新调度一次
85
+ // in-flight 期间若有 push/update 到达(pendingReschedule),重新调度一次
86
86
  // flush 渲染累积事件,避免 stale 中间帧。终态守卫在 flush() 入口已拦截,
87
87
  // 故 finish 后不会误触发。
88
88
  if (this.pendingReschedule) {
@@ -117,14 +117,14 @@ export class CardSession {
117
117
  return this.messageId;
118
118
  }
119
119
  /**
120
- * P1-3 合批窗口(ms)。连续事件各触发一次全卡 render + patch 是流式 CPU 主要
120
+ * 合批窗口(ms)。连续事件各触发一次全卡 render + patch 是流式 CPU 主要
121
121
  * 来源;窗口内多个事件复用同一次延迟 flush。窗口大小可通过 constructor 覆盖
122
122
  * (测试用小窗口/0 禁用)。run/bash 子类共用的 flush 骨架(scheduleFlush/flush/
123
123
  * cancelPendingFlush/settle)下沉到基类。
124
124
  */
125
125
  coalesceMs;
126
126
  flushTimer;
127
- flushInFlight = false; /** P2-1:in-flight flush 期间到达的 push/update 标记。scheduleFlush 在
127
+ flushInFlight = false; /** in-flight flush 期间到达的 push/update 标记。scheduleFlush 在
128
128
  * flushInFlight 时早退不调度新 timer,但事件已 reduce 进 state;若无此标志,
129
129
  * in-flight flush 完成后该事件无 follow-up render,停留为 stale 中间帧。
130
130
  * finally 检查此标志后重新 scheduleFlush,保证 in-flight 期间累积的事件最终被渲染。 */
@@ -132,7 +132,7 @@ export class CardSession {
132
132
  /** 当前 in-flight flush 的 promise(若 flush 正在 await updateCard)。
133
133
  * finish() 在终态 patch 前 await 它,保证 in-flight 的 pre-terminal patch 先落地、
134
134
  * terminal patch 后落地——否则两条 controller.update 并发无 FIFO,
135
- * pre-terminal 可能在 terminal 之后 resolve 留下"思考中"终帧(P2)。 */
135
+ * pre-terminal 可能在 terminal 之后 resolve 留下"思考中"终帧。 */
136
136
  flushP;
137
137
  async start() {
138
138
  if (this.streamOutcome) {
@@ -158,7 +158,7 @@ export class CardSession {
158
158
  // the controller is ready — by which time events pushed before the
159
159
  // producer ran (see test_anchor_events_before_controller_ready_...) have
160
160
  // already updated `this.state`. Reusing the initial card here would drop
161
- // those early events. The P3-7 "reuse first render" optimization was
161
+ // those early events. The"reuse first render" optimization was
162
162
  // rejected for this reason (premise "two renders with identical input"
163
163
  // does not hold).
164
164
  const stream = this.connector.streamCard(this.chatId, this.safeRenderCard(), async (controller) => {
@@ -36,7 +36,7 @@ export class RunCardSession extends CardSession {
36
36
  async push(event) {
37
37
  // state 每事件立即 reduce(始终最新),但 updateCard 延迟到合批窗口末尾。
38
38
  // 窗口内多次 push 复用同一 flushTimer,只 render + controller.update 一次
39
- // (P1-3)。push 立即 resolve(fire-and-forget render)—— bridge 串行
39
+ // push 立即 resolve(fire-and-forget render)—— bridge 串行
40
40
  // await 不被 render 阻塞,循环快速灌入事件,多个事件落进同一窗口被合并。
41
41
  //
42
42
  // 首 push 也走窗口(无 immediate-first-flush):start() 已发初始卡(streaming
@@ -55,7 +55,7 @@ export class RunCardSession extends CardSession {
55
55
  this.cancelPendingFlush();
56
56
  // 若有 in-flight flush 正在 await updateCard,必须等它完成后再发终态 patch。
57
57
  // 否则两条并发 controller.update 无 FIFO 保证,in-flight 的 pre-terminal
58
- // patch 可能在终态 patch 之后 resolve → 用户看到"思考中"终帧(P2)。
58
+ // patch 可能在终态 patch 之后 resolve → 用户看到"思考中"终帧。
59
59
  // await 保证顺序:pre-terminal 先落地 → terminal 后落地 → terminal 胜出。
60
60
  if (this.flushP)
61
61
  await this.flushP;
@@ -37,7 +37,7 @@ type BlockGroup = {
37
37
  diff?: string;
38
38
  timestamp?: string;
39
39
  };
40
- /** 每个块的截断后内容(thinking/plan/text),测量与渲染共用(P1-2 建议项)。 */
40
+ /** 每个块的截断后内容(thinking/plan/text),测量与渲染共用(建议项)。 */
41
41
  type GroupContentPrepared = Map<BlockGroup, string>;
42
42
  /** 导出供测试断言估算精度(估算 ≈ 实际渲染字节,见 tests/anchor/run-card/run-card.test.ts)。 */
43
43
  export declare function estimateCardBytes(state: RunState, options?: RunCardRenderOptions, shared?: {
@@ -16,7 +16,7 @@ const EXTREME_TOOL_KEEP = 1; // Extreme: keep last 1 tool panel
16
16
  // 影子测量:估算 = 精确复刻 normal 路径将要渲染的 JSON 字节。面板外壳、statusRow、
17
17
  // summary、header、按钮全部实测(measureJson),块内容经与渲染相同的变换
18
18
  // (truncateUtf8 截断 + markdownDiv 的 escapeMarkdown + JSON.stringify)精确测量,
19
- // 不依赖任何转义因子或拍脑袋常数(P1-2 第二轮:CJK/ASCII 转义双向精确)。
19
+ // 不依赖任何转义因子或拍脑袋常数(第二轮:CJK/ASCII 转义双向精确)。
20
20
  const DEGRADED_THRESHOLD = 24_000; // 估算低于此值走正常路径(留 4KB 裕度,相对 CARD_BUDGET_BYTES=28000);否则直接 degraded。调整时同步评估裕度是否仍覆盖估算偏差
21
21
  /** 精确测量对象经 JSON.stringify 后的 UTF-8 字节数。 */
22
22
  function measureJson(obj) {
@@ -275,7 +275,7 @@ function actionButtonList(state, options) {
275
275
  actionButtons.push(newSessionButton());
276
276
  return actionButtons;
277
277
  }
278
- /** Bottom action row: spacer + column_set of actionButtonList (估算与渲染共用,W2.2)。 */
278
+ /** Bottom action row: spacer + column_set of actionButtonList (估算与渲染共用)。 */
279
279
  function actionRow(state, options) {
280
280
  const actionButtons = actionButtonList(state, options);
281
281
  if (actionButtons.length === 0)
@@ -293,7 +293,7 @@ function actionRow(state, options) {
293
293
  ];
294
294
  }
295
295
  /** 审批区位于 body 最底部(底部操作行之后):待审批时决策按钮不遮挡内容流。
296
- * 例外:skeleton 兜底路径中 approvalArea 在 actionRow 之前(信息保真 C4.3),
296
+ * 例外:skeleton 兜底路径中 approvalArea 在 actionRow 之前(信息保真),
297
297
  * 保证极端降级时审批入口与 stop/new-session 按钮同时可见。 */
298
298
  function approvalArea(state) {
299
299
  const elements = [];
@@ -306,7 +306,7 @@ function approvalArea(state) {
306
306
  }
307
307
  return elements;
308
308
  }
309
- /** 底部操作行之前的内容区提示:被 MAX_BLOCKS 截掉的早期块计数(信息保真 C4.1)。 */
309
+ /** 底部操作行之前的内容区提示:被 MAX_BLOCKS 截掉的早期块计数(信息保真)。 */
310
310
  function omittedBlocksHint(state) {
311
311
  return state.omittedBlocks !== undefined && state.omittedBlocks > 0
312
312
  ? [markdownDiv(`_💡 已省略 ${state.omittedBlocks} 个早期步骤_`)]
@@ -346,7 +346,7 @@ function buildFallbackElements(state, options, tier) {
346
346
  if (toolsOmitted > 0) {
347
347
  elements.push(markdownDiv(`_💡 另外 ${toolsOmitted} 个工具调用已省略_`));
348
348
  }
349
- // P2-6: use object references (like the extreme path) instead of timestamps.
349
+ // use object references (like the extreme path) instead of timestamps.
350
350
  // RunBlock.timestamp has millisecond precision; ≥2 thinking blocks sharing a
351
351
  // timestamp collapse to one Set entry, so `has(block.timestamp)` lets ALL
352
352
  // same-timestamp blocks (including ones meant to be omitted) pass through —
@@ -364,7 +364,7 @@ function buildFallbackElements(state, options, tier) {
364
364
  }
365
365
  return true; // keep all non-thinking, non-tool blocks
366
366
  });
367
- // 信息保真 C4.2:plan/file_change 块在降级路径不渲染内容(正常路径已可见,
367
+ // 信息保真 plan/file_change 块在降级路径不渲染内容(正常路径已可见,
368
368
  // 降级预算紧张),但必须计数留痕,禁止静默丢弃。
369
369
  // 计数在渲染前完成,提示与 thinking/tool 省略提示一并置于卡片顶部——不能等
370
370
  // 内容循环结束再追加,否则提示会落在最后一条输出之后,打乱「输出收尾」观感。
@@ -467,7 +467,7 @@ function buildSkeletonElements(state, options) {
467
467
  elements.push(markdownDiv('_⚠️ 输出过大且含大量转义字符,已省略全部内容以避免超限;完整内容请查日志_'));
468
468
  // Summary (token stats etc.) — static, bounded size
469
469
  elements.push(...buildSummaryContent(state));
470
- // 信息保真 C4.3:skeleton 也渲染审批区——极端兜底不丢审批入口
470
+ // 信息保真 skeleton 也渲染审批区——极端兜底不丢审批入口
471
471
  //(待审批被静默吞掉 = 用户永远看不到审批请求)。
472
472
  elements.push(...approvalArea(state));
473
473
  // Bottom action row: stop (if running/finalizing) + compact (if applicable) + new session (always).
@@ -514,7 +514,7 @@ function bucketThinkingAndTool(blocks) {
514
514
  * 到 degraded(安全网不丢)。
515
515
  */
516
516
  /**
517
- * W2.2 镜像对单源:thinking/plan/file_change 三个 collapsible 面板组的
517
+ * 镜像对单源:thinking/plan/file_change 三个 collapsible 面板组的
518
518
  * { title, expanded, border, content } 描述,估算(measurePanelBytes)与渲染
519
519
  * (collapsibleMarkdownPanel)共用——此前同一套标题/opIcon/内容模板写两遍。
520
520
  * text(renderTextBlock)与 tool(renderTool)形态特殊,由调用方各自处理;
@@ -552,19 +552,19 @@ function describePanelGroup(group, prepared) {
552
552
  /** 导出供测试断言估算精度(估算 ≈ 实际渲染字节,见 tests/anchor/run-card/run-card.test.ts)。 */
553
553
  export function estimateCardBytes(state, options = {}, shared) {
554
554
  const finalized = state.terminal !== 'running';
555
- // P1-2:renderRunCard 传入共享 groups/prepared 时直接复用(截断只做一次);
555
+ // renderRunCard 传入共享 groups/prepared 时直接复用(截断只做一次);
556
556
  // 独立调用(测试/预算探针)时自建,行为与原先一致。
557
557
  const groups = shared?.groups ?? groupBlocks(state.blocks);
558
558
  const prepared = shared?.prepared ?? prepareGroupContent(groups);
559
559
  // 卡片外壳直接实测 assembleRunCard 空元素产物(schema/config/header/body 结构
560
- // 与正式渲染完全一致,W2.2);status/summary/操作行同样实测渲染结构。
560
+ // 与正式渲染完全一致);status/summary/操作行同样实测渲染结构。
561
561
  // 注:审批区(approvalArea)不参与估算——待审批通常在途且体积小,超预算由
562
562
  // renderRunCard 的 stringify 兜底捕获。
563
563
  let total = measureJson(assembleRunCard(state, options, [])) +
564
564
  measureJson(statusRow(state)) +
565
565
  measureJson(buildSummaryContent(state, { includeNotices: false })) +
566
566
  measureJson(actionRow(state, options));
567
- // 块内容:与 buildChronologicalContent 同源(describePanelGroup 单源,W2.2)
567
+ // 块内容:与 buildChronologicalContent 同源(describePanelGroup 单源)
568
568
  let renderedAny = false;
569
569
  for (const group of groups) {
570
570
  if (group.kind === 'tool') {
@@ -620,7 +620,7 @@ function measurePanelBytes(title, expanded, border, content) {
620
620
  */
621
621
  export function renderRunCard(state, options = {}) {
622
622
  // 先估后建:用廉价估算预测是否超 28KB 预算,明显超预算直接跳过完整卡构建走
623
- // degraded。P1-2:groups/prepared 只算一次,测量与正式渲染共用同一份截断结果,
623
+ // degraded。groups/prepared 只算一次,测量与正式渲染共用同一份截断结果,
624
624
  // 避免影子测量 + 正式渲染对每个超限块双倍截断。
625
625
  const groups = groupBlocks(state.blocks);
626
626
  const prepared = prepareGroupContent(groups);
@@ -680,9 +680,9 @@ function buildChronologicalContent(state, prepared, groups) {
680
680
  const elements = [];
681
681
  // finalized = 非 running(含 finalizing:主结果已出,thinking 折叠)
682
682
  const finalized = state.terminal !== 'running';
683
- // P1-2:renderRunCard 传入同一 groups 数组时直接复用(prepared 的键是组对象
683
+ // renderRunCard 传入同一 groups 数组时直接复用(prepared 的键是组对象
684
684
  // 引用,重建数组会让 prepared.get 永远 miss,退化为二次截断)。
685
- // W2.2:面板组描述经 describePanelGroup 与估算共享同一份模板。
685
+ // 面板组描述经 describePanelGroup 与估算共享同一份模板。
686
686
  // notices 作为普通消息按到达时间插入内容流(有内容时不再沉到 summary)。
687
687
  const contentItems = mergeNoticesIntoContent(groups, state.notices);
688
688
  for (const item of contentItems) {
@@ -9,7 +9,7 @@ export interface ToolEntry {
9
9
  name: string;
10
10
  input: unknown;
11
11
  /**
12
- * P3-6: cached parse of `input` (the truncated string) into a record, so
12
+ * cached parse of `input` (the truncated string) into a record, so
13
13
  * `tool-render.ts`'s `asRecord` does not re-`JSON.parse` on every render.
14
14
  * Computed once at store time in `reduceAssistantEvent` from the SAME
15
15
  * truncated string that the old per-render `asRecord` parsed — so a
@@ -121,7 +121,7 @@ export interface RunState {
121
121
  /** 压缩前上下文水位(codex compact 卡展示「压缩前 X → 压缩后 Y」)。 */
122
122
  compactPreContextLength?: number;
123
123
  /**
124
- * 待审批请求列表(app-server 模式,review P2-3)。同一 turn 内并发多个审批
124
+ * 待审批请求列表(app-server 模式)。同一 turn 内并发多个审批
125
125
  * 时全部渲染,避免后到者顶掉先到者的按钮(单槽会丢 UI,只能等超时 cancel)。
126
126
  */
127
127
  approvals?: Array<{
@@ -382,7 +382,7 @@ function reduceAssistantEvent(state, event) {
382
382
  };
383
383
  }
384
384
  else {
385
- // P3-6: parse the truncated stored string ONCE here (not on every render)
385
+ // parse the truncated stored string ONCE here (not on every render)
386
386
  // and cache the result on `parsedInput`. tool-render's asRecord then hits
387
387
  // the object branch with zero parsing. The parse uses the SAME truncated
388
388
  // string as the old per-render parse, so truncation-broken JSON still
@@ -450,7 +450,7 @@ function mergeToolUse(existing, incoming) {
450
450
  };
451
451
  }
452
452
  /** Handle tool_result content blocks (user message with tool results).
453
- * P3-4: rebuild ONLY the block matching `tool_use_id`, copying every other
453
+ * rebuild ONLY the block matching `tool_use_id`, copying every other
454
454
  * block by reference. The old `.map()` recreated all block objects per
455
455
  * tool_result (O(N) allocations on the run-card hot path). Footer still
456
456
  * advances to 'streaming' on every content block (parity with old behavior);
@@ -605,7 +605,7 @@ function stringifyUnknown(value) {
605
605
  }
606
606
  }
607
607
  /**
608
- * P3-6: parse a (possibly truncated) string into a record, mirroring what
608
+ * parse a (possibly truncated) string into a record, mirroring what
609
609
  * `tool-render.ts`'s `asRecord` did on every render. Returns `null` when the
610
610
  * string is empty, non-JSON, or parses to a non-object — so a truncation-broken
611
611
  * JSON yields `null` exactly as the old per-render parse did. Computed once at
@@ -20,7 +20,7 @@ export declare function toolHeaderText(tool: ToolEntry): string;
20
20
  /**
21
21
  * Structured body markdown for a tool call. Renders input fields by tool name
22
22
  * (Bash → command, Read/Edit/Write → file_path, etc.) and output in a code
23
- * block. An explicit `toolHint` (declared by the runner, 信息保真 C5) selects
23
+ * block. An explicit `toolHint` (declared by the runner, 信息保真) selects
24
24
  * the same rendering by family instead of name. There is no raw-JSON fallback
25
25
  * for unknown tools: `default` returns '' and unknown tools only surface their
26
26
  * args through the panel header's `summarizeInput` fallback.
@@ -29,13 +29,13 @@ const BODY_TOTAL_MAX = 2500;
29
29
  */
30
30
  export function toolHeaderText(tool) {
31
31
  const icon = tool.status === 'ok' ? '✅' : tool.status === 'error' ? '❌' : '⏳';
32
- // P3-6: use the cached parsed record when available (avoids per-render parse).
32
+ // use the cached parsed record when available (avoids per-render parse).
33
33
  const input = tool.parsedInput !== undefined ? tool.parsedInput : tool.input;
34
34
  const summary = summarizeInput(tool.name, input);
35
- // 信息保真 C3.1:ACP kind 映射后 name 与原始 title 不同时,title 以
35
+ // 信息保真 ACP kind 映射后 name 与原始 title 不同时,title 以
36
36
  // ` — {summary}` 追加在标题末尾(截断沿用 HEADER_SUMMARY_MAX)。
37
37
  // 用 truncate(suffix 为空)而非裸 slice:裸 slice 按 UTF-16 码元切,
38
- // 切点落在 emoji 代理对中间会留下孤立代理项(P2-31 同族)。
38
+ // 切点落在 emoji 代理对中间会留下孤立代理项(同族)。
39
39
  const blockSummary = tool.summary
40
40
  ? truncate(tool.summary.replace(/\s+/g, ' ').trim(), HEADER_SUMMARY_MAX, { suffix: '' })
41
41
  : '';
@@ -51,7 +51,7 @@ export function toolHeaderText(tool) {
51
51
  /**
52
52
  * Structured body markdown for a tool call. Renders input fields by tool name
53
53
  * (Bash → command, Read/Edit/Write → file_path, etc.) and output in a code
54
- * block. An explicit `toolHint` (declared by the runner, 信息保真 C5) selects
54
+ * block. An explicit `toolHint` (declared by the runner, 信息保真) selects
55
55
  * the same rendering by family instead of name. There is no raw-JSON fallback
56
56
  * for unknown tools: `default` returns '' and unknown tools only surface their
57
57
  * args through the panel header's `summarizeInput` fallback.
@@ -156,13 +156,13 @@ function renderWebSearchInput(str) {
156
156
  return str('query') ? `**Query** \`${truncate(str('query'), BODY_FIELD_MAX)}\`` : '';
157
157
  }
158
158
  function renderInput(tool) {
159
- // P3-6: use the cached parsed record when available (avoids per-render parse).
159
+ // use the cached parsed record when available (avoids per-render parse).
160
160
  const input = tool.parsedInput !== undefined ? tool.parsedInput : tool.input;
161
161
  const rec = asRecord(input);
162
162
  if (!rec)
163
163
  return '';
164
164
  const str = (k) => (typeof rec[k] === 'string' ? rec[k] : '');
165
- // 信息保真 C5:hint 优先(runner 自愿声明,只消费不推断);缺失时与现状
165
+ // 信息保真 hint 优先(runner 自愿声明,只消费不推断);缺失时与现状
166
166
  // 完全一致。hint 分支与 name case 共用同一渲染函数,无复制粘贴。
167
167
  switch (tool.toolHint) {
168
168
  case 'shell':