lark-remote 0.5.5 → 0.5.7

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 (58) hide show
  1. package/CHANGELOG.md +48 -7
  2. package/README.en.md +2 -2
  3. package/README.md +2 -2
  4. package/dist/bridge/approval-coordinator.d.ts +13 -0
  5. package/dist/bridge/approval-coordinator.js +34 -1
  6. package/dist/bridge/index.d.ts +48 -1
  7. package/dist/bridge/index.js +86 -14
  8. package/dist/card/approval-render.js +23 -1
  9. package/dist/card/card-budget.d.ts +37 -0
  10. package/dist/card/card-budget.js +128 -5
  11. package/dist/card/run-renderer.d.ts +24 -0
  12. package/dist/card/run-renderer.js +256 -60
  13. package/dist/card/run-state.d.ts +26 -0
  14. package/dist/card/run-state.js +45 -0
  15. package/dist/card/time.d.ts +14 -0
  16. package/dist/card/time.js +30 -0
  17. package/dist/card/tool-render.d.ts +15 -2
  18. package/dist/card/tool-render.js +40 -66
  19. package/dist/common/text-width.d.ts +24 -0
  20. package/dist/common/text-width.js +93 -0
  21. package/dist/common/tool-summary.d.ts +11 -0
  22. package/dist/common/tool-summary.js +153 -0
  23. package/dist/config/index.d.ts +1 -1
  24. package/dist/config/index.js +1 -1
  25. package/dist/connector/index.js +1 -1
  26. package/dist/index.js +63 -8
  27. package/dist/platform/command.js +1 -1
  28. package/dist/platform/shell.d.ts +2 -2
  29. package/dist/platform/spawn.d.ts +1 -1
  30. package/dist/platform/spawn.js +2 -2
  31. package/dist/platform/types.d.ts +1 -1
  32. package/dist/router/card-helpers.d.ts +18 -16
  33. package/dist/router/card-helpers.js +65 -4
  34. package/dist/router/index.d.ts +9 -0
  35. package/dist/router/index.js +57 -7
  36. package/dist/runner/claude/session.d.ts +1 -1
  37. package/dist/runner/claude/session.js +3 -3
  38. package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
  39. package/dist/runner/common/jsonrpc/transport.js +3 -3
  40. package/dist/runner/common/spawning-runner.d.ts +1 -1
  41. package/dist/runner/common/spawning-runner.js +2 -2
  42. package/dist/runner/dsh/auth.js +2 -1
  43. package/dist/runner/dsh/client.d.ts +124 -6
  44. package/dist/runner/dsh/client.js +398 -12
  45. package/dist/runner/dsh/exit-cancel.d.ts +32 -0
  46. package/dist/runner/dsh/exit-cancel.js +72 -0
  47. package/dist/runner/dsh/inflight.d.ts +62 -0
  48. package/dist/runner/dsh/inflight.js +140 -0
  49. package/dist/runner/dsh/runner.d.ts +117 -7
  50. package/dist/runner/dsh/runner.js +607 -21
  51. package/dist/runner/dsh/translator.d.ts +1 -1
  52. package/dist/runner/dsh/translator.js +103 -1
  53. package/dist/runner/dsh/types.d.ts +91 -1
  54. package/dist/runner/types.d.ts +86 -2
  55. package/dist/session/claude/sessions.js +2 -2
  56. package/dist/session/dsh/sessions.js +100 -15
  57. package/dist/update/install.js +1 -1
  58. package/package.json +1 -1
@@ -8,6 +8,8 @@ export function createInitialRunState(runId) {
8
8
  runId,
9
9
  terminal: 'running',
10
10
  footer: 'thinking',
11
+ // 状态行的耗时口径:从 run 状态建立那一刻算起(卡片首帧即开始计时)。
12
+ startedAt: Date.now(),
11
13
  blocks: [],
12
14
  };
13
15
  }
@@ -23,6 +25,10 @@ export function reduceRunState(state, event) {
23
25
  }
24
26
  if (event.type === 'system')
25
27
  return reduceSystemEvent(state, event);
28
+ if (event.type === 'run_status')
29
+ return reduceRunStatusEvent(state, event);
30
+ if (event.type === 'background_jobs')
31
+ return reduceBackgroundJobsEvent(state, event);
26
32
  if (event.type === 'result')
27
33
  return reduceResultEvent(state, event);
28
34
  if (event.type === 'assistant')
@@ -320,6 +326,42 @@ function reduceResultEvent(state, event) {
320
326
  : {}),
321
327
  };
322
328
  }
329
+ /**
330
+ * 后台任务名单 → RunState.backgroundJobs。
331
+ *
332
+ * 整份替换而非合并:事件本身就是"收尾那一刻仍在跑的完整名单",合并会把上一
333
+ * 轮已结束的任务留下来。用户点「停止」成功后由 bridge 直接从 state 里摘掉,
334
+ * 不再走事件(那时已经没有 run 在消费流了)。
335
+ */
336
+ function reduceBackgroundJobsEvent(state, event) {
337
+ if (event.type !== 'background_jobs')
338
+ return state;
339
+ return { ...state, backgroundJobs: event.jobs };
340
+ }
341
+ /**
342
+ * 运行期状态投影 → 状态行。
343
+ *
344
+ * 每帧只带一个投影 key,所以只合并"有值"的字段:用 undefined 覆盖会把另一
345
+ * 维度(上一步算出的上下文水位)擦掉。
346
+ */
347
+ function reduceRunStatusEvent(state, event) {
348
+ if (event.type !== 'run_status')
349
+ return state;
350
+ const prev = state.liveStatus ?? {};
351
+ const live = {
352
+ ...prev,
353
+ ...(event.contextUsed === undefined ? {} : { contextUsed: event.contextUsed }),
354
+ ...(event.contextLimit === undefined ? {} : { contextLimit: event.contextLimit }),
355
+ ...(event.steps === undefined ? {} : { steps: event.steps }),
356
+ ...(event.toolMs === undefined ? {} : { toolMs: event.toolMs }),
357
+ };
358
+ // 窗口上限同时写进 state:终态 summary 的百分比与状态条同源,即使
359
+ // result.usage 没带上限(DSH 的会话读取端不产出)也算得出来。
360
+ const next = { ...state, liveStatus: live };
361
+ if (live.contextLimit !== undefined)
362
+ next.contextLimit = live.contextLimit;
363
+ return next;
364
+ }
323
365
  /** Handle assistant thinking/text/tool_use content blocks. */
324
366
  function reduceAssistantEvent(state, event) {
325
367
  if (event.type !== 'assistant')
@@ -577,6 +619,9 @@ export function finishRun(state, terminal, meta = {}) {
577
619
  ...state,
578
620
  terminal,
579
621
  footer: null,
622
+ // 耗时定格:终态之后再渲染(补 token 统计、点「详情」重渲染)都沿用同一
623
+ // 结束时刻,否则每次 patch 的数字都会变大。
624
+ finishedAt: state.finishedAt ?? Date.now(),
580
625
  blocks: markThinkingInactive(state.blocks),
581
626
  ...sanitizeMeta(meta),
582
627
  ...(derivedReason !== undefined ? { interruptedReason: derivedReason } : {}),
@@ -1 +1,15 @@
1
1
  export declare function formatTimestamp(ts: string | number | undefined): string;
2
+ /**
3
+ * 时钟时间 `HH:mm`(同一张卡内的次要信息用)。
4
+ *
5
+ * 面板标题在手机上只有约 36 个显示宽度可用:`2026-10-19 21:23` 光时间戳就吃掉
6
+ * 一半,正文明明只需要知道"什么时候发生的"。完整的日期在会话历史/日志里。
7
+ */
8
+ export declare function formatClock(ts: string | number | undefined): string;
9
+ /**
10
+ * `MM-DD HH:mm`(面板标题里的历史事件时间)。
11
+ *
12
+ * 比 `HH:mm` 多带上日期(跨天/跨周的会话不会误导),比完整 `YYYY-MM-DD HH:mm`
13
+ * 省 7 个显示宽度——那 7 个单位在手机上正好是文件名能不能显示出来的差别。
14
+ */
15
+ export declare function formatMonthDayClock(ts: string | number | undefined): string;
package/dist/card/time.js CHANGED
@@ -11,4 +11,34 @@ export function formatTimestamp(ts) {
11
11
  const mi = String(d.getMinutes()).padStart(2, '0');
12
12
  return `${yyyy}-${mm}-${dd} ${hh}:${mi}`;
13
13
  }
14
+ /**
15
+ * 时钟时间 `HH:mm`(同一张卡内的次要信息用)。
16
+ *
17
+ * 面板标题在手机上只有约 36 个显示宽度可用:`2026-10-19 21:23` 光时间戳就吃掉
18
+ * 一半,正文明明只需要知道"什么时候发生的"。完整的日期在会话历史/日志里。
19
+ */
20
+ export function formatClock(ts) {
21
+ if (!ts)
22
+ return '';
23
+ const d = new Date(ts);
24
+ if (isNaN(d.getTime()))
25
+ return '';
26
+ return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
27
+ }
28
+ /**
29
+ * `MM-DD HH:mm`(面板标题里的历史事件时间)。
30
+ *
31
+ * 比 `HH:mm` 多带上日期(跨天/跨周的会话不会误导),比完整 `YYYY-MM-DD HH:mm`
32
+ * 省 7 个显示宽度——那 7 个单位在手机上正好是文件名能不能显示出来的差别。
33
+ */
34
+ export function formatMonthDayClock(ts) {
35
+ if (!ts)
36
+ return '';
37
+ const d = new Date(ts);
38
+ if (isNaN(d.getTime()))
39
+ return '';
40
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
41
+ const dd = String(d.getDate()).padStart(2, '0');
42
+ return `${mm}-${dd} ${formatClock(d.getTime())}`;
43
+ }
14
44
  //# sourceMappingURL=time.js.map
@@ -10,13 +10,24 @@
10
10
  * like `✅ **Bash** — pwd` so collapsed panels stay informative.
11
11
  */
12
12
  import type { ToolEntry } from './run-state.js';
13
+ export { displayWidth, truncateMiddleToDisplayWidth, truncateToDisplayWidth, } from '../common/text-width.js';
14
+ /**
15
+ * 面板标题的显示宽度上限(半角单位,CJK/emoji 记 2)。
16
+ *
17
+ * 手机(~390px 内容宽)上折叠面板的标题只有**一行**可用:减去图标与内边距约
18
+ * 320px,按 ~7.5px/半角算 ≈ 42 个半角宽度(iPhone SE 更窄,超出时按 `·`/空格
19
+ * 折行仍可读,但不能整段换行)。超过就折行,断点常常落在 URL 或命令
20
+ * 中间(2026-10-19 截图反馈)。完整参数在展开后的正文里,标题只负责"哪个工具、
21
+ * 动了什么",并带上 `HH:mm` 时间戳(一起计入预算)。
22
+ */
23
+ export declare const HEADER_DISPLAY_MAX = 42;
13
24
  /**
14
25
  * One-line header for a tool call:
15
26
  * `⏳ **Bash** — pwd`
16
27
  * `✅ **Read** — /repo/a.ts`
17
28
  * `❌ **Grep** — pattern in path`
18
29
  */
19
- export declare function toolHeaderText(tool: ToolEntry): string;
30
+ export declare function toolHeaderText(tool: ToolEntry, suffix?: string): string;
20
31
  /**
21
32
  * Structured body markdown for a tool call. Renders input fields by tool name
22
33
  * (Bash → command, Read/Edit/Write → file_path, etc.) and output in a code
@@ -25,4 +36,6 @@ export declare function toolHeaderText(tool: ToolEntry): string;
25
36
  * for unknown tools: `default` returns '' and unknown tools only surface their
26
37
  * args through the panel header's `summarizeInput` fallback.
27
38
  */
28
- export declare function toolBodyMd(tool: ToolEntry): string;
39
+ export declare function toolBodyMd(tool: ToolEntry, opts?: {
40
+ outputMax?: number;
41
+ }): string;
@@ -10,7 +10,21 @@
10
10
  * like `✅ **Bash** — pwd` so collapsed panels stay informative.
11
11
  */
12
12
  import { truncate } from '../common/truncate.js';
13
+ import { asRecord, summarizeToolInput } from '../common/tool-summary.js';
14
+ import { truncateMiddleToDisplayWidth } from '../common/text-width.js';
15
+ // 这两个宽度工具原本住在这里;resume 卡片也要用,已下沉到 common。
16
+ export { displayWidth, truncateMiddleToDisplayWidth, truncateToDisplayWidth, } from '../common/text-width.js';
13
17
  const HEADER_SUMMARY_MAX = 80;
18
+ /**
19
+ * 面板标题的显示宽度上限(半角单位,CJK/emoji 记 2)。
20
+ *
21
+ * 手机(~390px 内容宽)上折叠面板的标题只有**一行**可用:减去图标与内边距约
22
+ * 320px,按 ~7.5px/半角算 ≈ 42 个半角宽度(iPhone SE 更窄,超出时按 `·`/空格
23
+ * 折行仍可读,但不能整段换行)。超过就折行,断点常常落在 URL 或命令
24
+ * 中间(2026-10-19 截图反馈)。完整参数在展开后的正文里,标题只负责"哪个工具、
25
+ * 动了什么",并带上 `HH:mm` 时间戳(一起计入预算)。
26
+ */
27
+ export const HEADER_DISPLAY_MAX = 42;
14
28
  const BODY_FIELD_MAX = 600;
15
29
  const OUTPUT_MAX = 1200;
16
30
  /**
@@ -27,11 +41,11 @@ const BODY_TOTAL_MAX = 2500;
27
41
  * `✅ **Read** — /repo/a.ts`
28
42
  * `❌ **Grep** — pattern in path`
29
43
  */
30
- export function toolHeaderText(tool) {
44
+ export function toolHeaderText(tool, suffix = '') {
31
45
  const icon = tool.status === 'ok' ? '✅' : tool.status === 'error' ? '❌' : '⏳';
32
46
  // use the cached parsed record when available (avoids per-render parse).
33
47
  const input = tool.parsedInput !== undefined ? tool.parsedInput : tool.input;
34
- const summary = summarizeInput(tool.name, input);
48
+ const summary = summarizeToolInput(tool.name, input);
35
49
  // 信息保真 ACP kind 映射后 name 与原始 title 不同时,title 以
36
50
  // ` — {summary}` 追加在标题末尾(截断沿用 HEADER_SUMMARY_MAX)。
37
51
  // 用 truncate(suffix 为空)而非裸 slice:裸 slice 按 UTF-16 码元切,
@@ -44,9 +58,14 @@ export function toolHeaderText(tool) {
44
58
  // `✅ **Bash** — ls` instead of `✅ **Bash** — ls — ls`.
45
59
  const parts = [summary, blockSummary].filter(Boolean);
46
60
  const uniqueParts = parts.filter((part, i) => parts.indexOf(part) === i);
47
- return uniqueParts.length > 0
61
+ const title = uniqueParts.length > 0
48
62
  ? `${icon} **${tool.name}** — ${uniqueParts.join(' — ')}`
49
63
  : `${icon} **${tool.name}**`;
64
+ // 折叠面板的标题必须能在一行内放下(手机窄屏):超宽就按显示宽度截断,
65
+ // 完整值仍在展开后的正文里。`suffix`(时间戳)一起计入预算,否则"标题压到
66
+ // 60 宽 + 18 宽时间戳"在手机上照样折行。
67
+ // 中间省略:路径/URL 的结尾(文件名)比开头更有信息量。
68
+ return truncateMiddleToDisplayWidth(`${title}${suffix}`, HEADER_DISPLAY_MAX);
50
69
  }
51
70
  /**
52
71
  * Structured body markdown for a tool call. Renders input fields by tool name
@@ -56,13 +75,14 @@ export function toolHeaderText(tool) {
56
75
  * for unknown tools: `default` returns '' and unknown tools only surface their
57
76
  * args through the panel header's `summarizeInput` fallback.
58
77
  */
59
- export function toolBodyMd(tool) {
78
+ export function toolBodyMd(tool, opts = {}) {
60
79
  const parts = [];
61
80
  const inputMd = renderInput(tool);
62
81
  if (inputMd)
63
82
  parts.push(inputMd);
64
83
  if (tool.output) {
65
- const truncated = truncate(tool.output, OUTPUT_MAX);
84
+ const outputMax = Math.min(opts.outputMax ?? OUTPUT_MAX, OUTPUT_MAX);
85
+ const truncated = truncate(tool.output, outputMax);
66
86
  if (tool.status === 'error') {
67
87
  parts.push(`**Error**\n\`\`\`\n${truncated}\n\`\`\``);
68
88
  }
@@ -79,51 +99,6 @@ export function toolBodyMd(tool) {
79
99
  // 同上:走 truncate 保证切点不落在代理对中间(末尾 '…' 由 truncate 附加以对齐预算)。
80
100
  return `${truncate(body, BODY_TOTAL_MAX)}\n\n_(body 已截断,完整内容查日志)_`;
81
101
  }
82
- function summarizeInput(name, input) {
83
- const rec = asRecord(input);
84
- if (!rec)
85
- return '';
86
- const pick = (key, max = HEADER_SUMMARY_MAX) => {
87
- const v = rec[key];
88
- if (typeof v !== 'string')
89
- return '';
90
- const oneLine = v.replace(/\s+/g, ' ').trim();
91
- return oneLine.length > max ? `${oneLine.slice(0, max)}…` : oneLine;
92
- };
93
- switch (name) {
94
- case 'Bash':
95
- case 'bash':
96
- return pick('command');
97
- case 'Read':
98
- case 'Edit':
99
- case 'Write':
100
- case 'NotebookEdit':
101
- case 'read':
102
- case 'edit':
103
- case 'write':
104
- return pick('file_path') || pick('path');
105
- case 'Grep':
106
- case 'grep': {
107
- const pat = pick('pattern', 40);
108
- const p = pick('path', 30);
109
- return p ? `${pat} in ${p}` : pat;
110
- }
111
- case 'Glob':
112
- case 'find':
113
- return pick('pattern');
114
- case 'ls':
115
- return pick('path');
116
- case 'WebFetch':
117
- return pick('url');
118
- case 'WebSearch':
119
- return pick('query', 60);
120
- case 'Agent':
121
- case 'Task':
122
- return pick('description') || pick('subagent_type');
123
- default:
124
- return pick('command') || pick('file_path') || pick('path') || pick('query');
125
- }
126
- }
127
102
  // --- Named render bodies (shared by tool-name cases and toolHint branches) ---
128
103
  function renderBashInput(str) {
129
104
  const cmd = str('command');
@@ -155,6 +130,15 @@ function renderWebFetchInput(str) {
155
130
  function renderWebSearchInput(str) {
156
131
  return str('query') ? `**Query** \`${truncate(str('query'), BODY_FIELD_MAX)}\`` : '';
157
132
  }
133
+ /** DSH `web_search` 的批量查询参数(`queries: string[]`)。 */
134
+ function renderWebQueriesInput(queries) {
135
+ if (!Array.isArray(queries))
136
+ return '';
137
+ const items = queries.filter((q) => typeof q === 'string' && q.trim().length > 0);
138
+ if (items.length === 0)
139
+ return '';
140
+ return `**Query** \`${truncate(items.join('` / `'), BODY_FIELD_MAX)}\``;
141
+ }
158
142
  function renderInput(tool) {
159
143
  // use the cached parsed record when available (avoids per-render parse).
160
144
  const input = tool.parsedInput !== undefined ? tool.parsedInput : tool.input;
@@ -180,6 +164,7 @@ function renderInput(tool) {
180
164
  switch (tool.name) {
181
165
  case 'Bash':
182
166
  case 'bash':
167
+ case 'pwsh':
183
168
  return renderBashInput(str);
184
169
  case 'Read':
185
170
  case 'Edit':
@@ -188,37 +173,26 @@ function renderInput(tool) {
188
173
  case 'read':
189
174
  case 'edit':
190
175
  case 'write':
176
+ case 'read_image':
191
177
  return renderFileInput(str);
192
178
  case 'Grep':
193
179
  case 'grep':
194
180
  return renderGrepInput(str);
195
181
  case 'Glob':
182
+ case 'glob':
196
183
  case 'find':
197
184
  return renderGlobInput(str);
198
185
  case 'ls':
199
186
  return renderLsInput(str);
200
187
  case 'WebFetch':
188
+ case 'web_fetch':
201
189
  return renderWebFetchInput(str);
202
190
  case 'WebSearch':
203
191
  return renderWebSearchInput(str);
192
+ case 'web_search':
193
+ return renderWebQueriesInput(rec.queries) || renderWebSearchInput(str);
204
194
  default:
205
195
  return '';
206
196
  }
207
197
  }
208
- function asRecord(input) {
209
- if (!input)
210
- return null;
211
- if (typeof input === 'object')
212
- return input;
213
- if (typeof input === 'string') {
214
- try {
215
- const p = JSON.parse(input);
216
- return p && typeof p === 'object' ? p : null;
217
- }
218
- catch {
219
- return null;
220
- }
221
- }
222
- return null;
223
- }
224
198
  //# sourceMappingURL=tool-render.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 文本的**显示宽度**工具(半角 1 / 全角与 emoji 2)。
3
+ *
4
+ * 为什么单列一个模块:卡片标题、状态行、resume 卡片标题都要按"手机上能放几行"
5
+ * 来收敛,而卡片层(`src/card/`)与会话读取层(`src/session/`)都要用——放在
6
+ * 任何一侧都会让另一侧反向依赖。
7
+ */
8
+ /**
9
+ * 按**显示宽度**截断(半角 1 / 全角与 emoji 2)。
10
+ *
11
+ * markdown 的 `**` 与 emoji 都要按视觉宽度算:只按字符数截断时,一行 34 个
12
+ * CJK 就是 34 个字宽,手机上必然折行。
13
+ */
14
+ export declare function truncateToDisplayWidth(text: string, maxUnits: number): string;
15
+ /** 字符串的显示宽度(半角 1 / 全角与 emoji 2)。 */
16
+ export declare function displayWidth(text: string): number;
17
+ /**
18
+ * 中间省略截断:保留开头与结尾,只在中间打 `…`。
19
+ *
20
+ * 标题里的路径/URL 必须保留**结尾**才有信息量(`~/code/notes-app/.../notes/
21
+ * 2026-10-19-vision-remaining.md` 里真正有用的是文件名),尾部省略会把文件名
22
+ * 整个切掉;纯尾部截断只适合"开头即结论"的句子(命令)。
23
+ */
24
+ export declare function truncateMiddleToDisplayWidth(text: string, maxUnits: number): string;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * 文本的**显示宽度**工具(半角 1 / 全角与 emoji 2)。
3
+ *
4
+ * 为什么单列一个模块:卡片标题、状态行、resume 卡片标题都要按"手机上能放几行"
5
+ * 来收敛,而卡片层(`src/card/`)与会话读取层(`src/session/`)都要用——放在
6
+ * 任何一侧都会让另一侧反向依赖。
7
+ */
8
+ /**
9
+ * 按**显示宽度**截断(半角 1 / 全角与 emoji 2)。
10
+ *
11
+ * markdown 的 `**` 与 emoji 都要按视觉宽度算:只按字符数截断时,一行 34 个
12
+ * CJK 就是 34 个字宽,手机上必然折行。
13
+ */
14
+ export function truncateToDisplayWidth(text, maxUnits) {
15
+ if (displayWidth(text) <= maxUnits)
16
+ return text;
17
+ let used = 0;
18
+ let out = '';
19
+ for (const ch of text) {
20
+ const w = charWidth(ch);
21
+ // 预留 1 个单位给省略号
22
+ if (used + w > maxUnits - 1)
23
+ break;
24
+ out += ch;
25
+ used += w;
26
+ }
27
+ return `${out}…`;
28
+ }
29
+ /** 字符串的显示宽度(半角 1 / 全角与 emoji 2)。 */
30
+ export function displayWidth(text) {
31
+ let total = 0;
32
+ for (const ch of text)
33
+ total += charWidth(ch);
34
+ return total;
35
+ }
36
+ function charWidth(ch) {
37
+ const code = ch.codePointAt(0) ?? 0;
38
+ // CJK 统一表意文字、假名、全角标点、常见 emoji 都按两个半角算。
39
+ if (code >= 0x1100 && (code <= 0x115f || code === 0x2329 || code === 0x232a))
40
+ return 2;
41
+ if (code >= 0x2e80 && code <= 0xa4cf)
42
+ return 2;
43
+ if (code >= 0xac00 && code <= 0xd7a3)
44
+ return 2;
45
+ if (code >= 0xf900 && code <= 0xfaff)
46
+ return 2;
47
+ if (code >= 0xfe30 && code <= 0xfe6f)
48
+ return 2;
49
+ if (code >= 0xff00 && code <= 0xff60)
50
+ return 2;
51
+ if (code >= 0xffe0 && code <= 0xffe6)
52
+ return 2;
53
+ if (code >= 0x1f300 && code <= 0x1faff)
54
+ return 2;
55
+ if (code >= 0x2600 && code <= 0x27bf)
56
+ return 2;
57
+ return 1;
58
+ }
59
+ /**
60
+ * 中间省略截断:保留开头与结尾,只在中间打 `…`。
61
+ *
62
+ * 标题里的路径/URL 必须保留**结尾**才有信息量(`~/code/notes-app/.../notes/
63
+ * 2026-10-19-vision-remaining.md` 里真正有用的是文件名),尾部省略会把文件名
64
+ * 整个切掉;纯尾部截断只适合"开头即结论"的句子(命令)。
65
+ */
66
+ export function truncateMiddleToDisplayWidth(text, maxUnits) {
67
+ if (displayWidth(text) <= maxUnits)
68
+ return text;
69
+ const budget = Math.max(1, maxUnits - 1); // 给省略号留 1 个单位
70
+ const headBudget = Math.ceil(budget / 2);
71
+ const tailBudget = budget - headBudget;
72
+ let head = '';
73
+ let used = 0;
74
+ for (const ch of text) {
75
+ const w = charWidth(ch);
76
+ if (used + w > headBudget)
77
+ break;
78
+ head += ch;
79
+ used += w;
80
+ }
81
+ let tail = '';
82
+ used = 0;
83
+ const chars = [...text];
84
+ for (let i = chars.length - 1; i >= 0; i--) {
85
+ const w = charWidth(chars[i]);
86
+ if (used + w > tailBudget)
87
+ break;
88
+ tail = chars[i] + tail;
89
+ used += w;
90
+ }
91
+ return `${head}…${tail}`;
92
+ }
93
+ //# sourceMappingURL=text-width.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 工具调用参数 → 一行人话摘要(`bash — npm test`、`read — src/a.ts`)。
3
+ *
4
+ * 为什么要抽到 common:run 卡的面板标题(`src/card/tool-render.ts`)和
5
+ * resume 卡的「🔧 工具调用」标题(`src/session/<agent>/sessions.ts`)都要同一套口径;
6
+ * 各写一份必然漂移(同一个 `glob` 在一处显示 pattern、另一处显示目录)。
7
+ *
8
+ * 只做"取哪个字段",不做 agent 分支:工具名与参数名的对应表就是这张 switch。
9
+ */
10
+ export declare function asRecord(input: unknown): Record<string, unknown> | null;
11
+ export declare function summarizeToolInput(name: string, input: unknown): string;
@@ -0,0 +1,153 @@
1
+ /**
2
+ * 工具调用参数 → 一行人话摘要(`bash — npm test`、`read — src/a.ts`)。
3
+ *
4
+ * 为什么要抽到 common:run 卡的面板标题(`src/card/tool-render.ts`)和
5
+ * resume 卡的「🔧 工具调用」标题(`src/session/<agent>/sessions.ts`)都要同一套口径;
6
+ * 各写一份必然漂移(同一个 `glob` 在一处显示 pattern、另一处显示目录)。
7
+ *
8
+ * 只做"取哪个字段",不做 agent 分支:工具名与参数名的对应表就是这张 switch。
9
+ */
10
+ import { truncate } from './truncate.js';
11
+ const HEADER_SUMMARY_MAX = 80;
12
+ export function asRecord(input) {
13
+ if (!input)
14
+ return null;
15
+ if (typeof input === 'object')
16
+ return input;
17
+ if (typeof input === 'string') {
18
+ try {
19
+ const p = JSON.parse(input);
20
+ return p && typeof p === 'object' ? p : null;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ return null;
27
+ }
28
+ export function summarizeToolInput(name, input) {
29
+ const rec = asRecord(input);
30
+ if (!rec)
31
+ return '';
32
+ const pick = (key, max = HEADER_SUMMARY_MAX) => {
33
+ const v = rec[key];
34
+ if (typeof v !== 'string')
35
+ return '';
36
+ const oneLine = v.replace(/\s+/g, ' ').trim();
37
+ return oneLine.length > max ? `${oneLine.slice(0, max)}…` : oneLine;
38
+ };
39
+ /** First non-empty of several keys(DSH 同名工具跨版本改过参数名)。 */
40
+ const pickAny = (keys, max = HEADER_SUMMARY_MAX) => {
41
+ for (const key of keys) {
42
+ const v = pick(key, max);
43
+ if (v)
44
+ return v;
45
+ }
46
+ return '';
47
+ };
48
+ /** 数组参数的第一个字符串项(DSH web_search 的 queries、present 的 files)。 */
49
+ const pickFirst = (key, max = HEADER_SUMMARY_MAX) => {
50
+ const v = rec[key];
51
+ if (typeof v === 'string')
52
+ return pick(key, max);
53
+ if (!Array.isArray(v))
54
+ return '';
55
+ for (const item of v) {
56
+ if (typeof item === 'string' && item.trim()) {
57
+ const oneLine = item.replace(/\s+/g, ' ').trim();
58
+ return oneLine.length > max ? `${oneLine.slice(0, max)}…` : oneLine;
59
+ }
60
+ if (item && typeof item === 'object') {
61
+ const path = item.path;
62
+ if (typeof path === 'string' && path.trim())
63
+ return path;
64
+ }
65
+ }
66
+ return '';
67
+ };
68
+ switch (name) {
69
+ case 'Bash':
70
+ case 'bash':
71
+ case 'pwsh':
72
+ return pick('command');
73
+ case 'Read':
74
+ case 'Edit':
75
+ case 'Write':
76
+ case 'NotebookEdit':
77
+ case 'read':
78
+ case 'edit':
79
+ case 'write':
80
+ case 'read_image':
81
+ return pick('file_path') || pick('path');
82
+ case 'Grep':
83
+ case 'grep': {
84
+ const pat = pick('pattern', 40);
85
+ const p = pick('path', 30);
86
+ return p ? `${pat} in ${p}` : pat;
87
+ }
88
+ case 'Glob':
89
+ case 'glob':
90
+ case 'find':
91
+ return pick('pattern');
92
+ case 'ls':
93
+ return pick('path');
94
+ case 'WebFetch':
95
+ case 'web_fetch':
96
+ return pick('url');
97
+ case 'WebSearch':
98
+ case 'web_search':
99
+ return pickFirst('queries', 60) || pick('query', 60);
100
+ case 'Agent':
101
+ case 'Task':
102
+ case 'subagent':
103
+ case 'workflow':
104
+ return pick('description') || pick('subagent_type');
105
+ case 'todo_write':
106
+ return summarizeTodos(rec);
107
+ case 'job_output':
108
+ case 'job_kill':
109
+ return pick('job_id');
110
+ case 'job_list':
111
+ return '(列出任务)';
112
+ case 'present':
113
+ return pickFirst('files');
114
+ case 'skill':
115
+ return pick('name');
116
+ case 'run_code':
117
+ return pick('description') || pick('language');
118
+ case 'terminal_send':
119
+ return pick('text');
120
+ case 'ask_user_question': {
121
+ const questions = rec.questions;
122
+ return Array.isArray(questions) ? `${questions.length} 个问题` : '';
123
+ }
124
+ case 'send_message':
125
+ case 'interrupt_agent':
126
+ return pick('description') || pick('agent_id');
127
+ default:
128
+ return pickAny(['command', 'file_path', 'path', 'query', 'url']);
129
+ }
130
+ }
131
+ /** todo_write 摘要:完成进度 + 当前 in_progress 项主题。 */
132
+ function summarizeTodos(rec) {
133
+ const todos = rec.todos;
134
+ if (!Array.isArray(todos))
135
+ return '';
136
+ let done = 0;
137
+ let active = '';
138
+ for (const item of todos) {
139
+ if (!item || typeof item !== 'object')
140
+ continue;
141
+ const todo = item;
142
+ if (todo.status === 'completed')
143
+ done += 1;
144
+ if (!active && todo.status === 'in_progress' && typeof todo.content === 'string') {
145
+ active = todo.content.replace(/\s+/g, ' ').trim();
146
+ }
147
+ }
148
+ const progress = `${done}/${todos.length}`;
149
+ if (!active)
150
+ return `已完成 ${progress}`;
151
+ return truncate(`已完成 ${progress} · ${active}`, HEADER_SUMMARY_MAX, { suffix: '…' });
152
+ }
153
+ //# sourceMappingURL=tool-summary.js.map
@@ -28,7 +28,7 @@ export declare const DEFAULT_TURN_IDLE_TIMEOUT_MINUTES: 30;
28
28
  * 单文件上限默认 100MB:50MB 对视频偏小(2026-09-15 事故就是 mp4 被丢弃)。
29
29
  * 注意飞书侧 `type=file` 超过 100MB 必须用 Range 分片下载,而 SDK 的
30
30
  * `downloadResourceToFile` 不分片(必报 234037),所以 100 是实际可用上限
31
- * (inbound-message-matrix.md §4.4)。
31
+ * (inbound-message-matrix.md §4 第 4 条)。
32
32
  */
33
33
  export declare const DEFAULT_INBOUND_MEDIA_DIR_NAME = ".lark-remote-temp";
34
34
  export declare const DEFAULT_INBOUND_MEDIA_MAX_SIZE_MB = 100;
@@ -56,7 +56,7 @@ export const DEFAULT_TURN_IDLE_TIMEOUT_MINUTES = DEFAULTS.TURN_IDLE_TIMEOUT_MINU
56
56
  * 单文件上限默认 100MB:50MB 对视频偏小(2026-09-15 事故就是 mp4 被丢弃)。
57
57
  * 注意飞书侧 `type=file` 超过 100MB 必须用 Range 分片下载,而 SDK 的
58
58
  * `downloadResourceToFile` 不分片(必报 234037),所以 100 是实际可用上限
59
- * (inbound-message-matrix.md §4.4)。
59
+ * (inbound-message-matrix.md §4 第 4 条)。
60
60
  */
61
61
  export const DEFAULT_INBOUND_MEDIA_DIR_NAME = '.lark-remote-temp';
62
62
  export const DEFAULT_INBOUND_MEDIA_MAX_SIZE_MB = 100;
@@ -424,7 +424,7 @@ export class FeishuConnector {
424
424
  kind: res.kind,
425
425
  fileName: res.fileName,
426
426
  // 飞书侧 >100MB 必须 Range 分片(SDK 不分片),到上限就是下不动:
427
- // 提示写清楚,避免用户以为"再放大上限就能存"(matrix §4.4/§2 #11)。
427
+ // 提示写清楚,避免用户以为"再放大上限就能存"(inbound-message-matrix.md §4 第 4 条)。
428
428
  reason: `超过 ${limitMb}MB 大小限制` +
429
429
  (limitMb >= 100 ? '(飞书 >100MB 的文件需分片下载,暂不支持)' : ''),
430
430
  });