lark-remote 0.5.6 → 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.
- package/CHANGELOG.md +30 -6
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/dist/bridge/index.d.ts +48 -1
- package/dist/bridge/index.js +86 -14
- package/dist/card/card-budget.d.ts +37 -0
- package/dist/card/card-budget.js +128 -5
- package/dist/card/run-renderer.d.ts +24 -0
- package/dist/card/run-renderer.js +256 -60
- package/dist/card/run-state.d.ts +26 -0
- package/dist/card/run-state.js +45 -0
- package/dist/card/time.d.ts +14 -0
- package/dist/card/time.js +30 -0
- package/dist/card/tool-render.d.ts +15 -2
- package/dist/card/tool-render.js +40 -66
- package/dist/common/text-width.d.ts +24 -0
- package/dist/common/text-width.js +93 -0
- package/dist/common/tool-summary.d.ts +11 -0
- package/dist/common/tool-summary.js +153 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/index.js +61 -6
- package/dist/platform/command.js +1 -1
- package/dist/platform/shell.d.ts +2 -2
- package/dist/platform/spawn.d.ts +1 -1
- package/dist/platform/spawn.js +2 -2
- package/dist/platform/types.d.ts +1 -1
- package/dist/router/card-helpers.d.ts +18 -16
- package/dist/router/card-helpers.js +65 -4
- package/dist/router/index.d.ts +9 -0
- package/dist/router/index.js +57 -7
- package/dist/runner/claude/session.d.ts +1 -1
- package/dist/runner/claude/session.js +3 -3
- package/dist/runner/common/jsonrpc/transport.d.ts +1 -1
- package/dist/runner/common/jsonrpc/transport.js +3 -3
- package/dist/runner/common/spawning-runner.d.ts +1 -1
- package/dist/runner/common/spawning-runner.js +2 -2
- package/dist/runner/dsh/auth.js +2 -1
- package/dist/runner/dsh/client.d.ts +100 -3
- package/dist/runner/dsh/client.js +289 -4
- package/dist/runner/dsh/exit-cancel.d.ts +32 -0
- package/dist/runner/dsh/exit-cancel.js +72 -0
- package/dist/runner/dsh/inflight.d.ts +62 -0
- package/dist/runner/dsh/inflight.js +140 -0
- package/dist/runner/dsh/runner.d.ts +90 -4
- package/dist/runner/dsh/runner.js +436 -18
- package/dist/runner/dsh/translator.d.ts +1 -1
- package/dist/runner/dsh/translator.js +103 -1
- package/dist/runner/dsh/types.d.ts +60 -0
- package/dist/runner/types.d.ts +76 -2
- package/dist/session/claude/sessions.js +2 -2
- package/dist/session/dsh/sessions.js +100 -15
- package/dist/update/install.js +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { terminalToColor, terminalToLabel, newSessionButton, stopButton, compactButton, agentDisplayName, } from './card-shared.js';
|
|
1
|
+
import { terminalToColor, terminalToLabel, newSessionButton, stopButton, compactButton, cardButton, 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, CARD_BUDGET_BYTES } from './text-truncate.js';
|
|
5
|
-
import {
|
|
5
|
+
import { countCardElements, MAX_CARD_ELEMENTS } from './card-budget.js';
|
|
6
|
+
import { truncate } from '../common/truncate.js';
|
|
7
|
+
import { formatClock, formatTimestamp } from './time.js';
|
|
6
8
|
import { formatUsageStats } from '../router/usage-format.js';
|
|
7
9
|
import { renderApprovalArea } from './approval-render.js';
|
|
8
10
|
const REASONING_BYTES = 4_500;
|
|
@@ -10,6 +12,15 @@ const TEXT_BYTES = 10_000;
|
|
|
10
12
|
const DEGRADED_THINKING_KEEP = 2;
|
|
11
13
|
const DEGRADED_TEXT_BYTES = 5_000;
|
|
12
14
|
const DEGRADED_THINKING_BYTES = 1_000;
|
|
15
|
+
/**
|
|
16
|
+
* 带「详情」按钮的工具行数上限(从最新往回数)。
|
|
17
|
+
*
|
|
18
|
+
* 按钮行是节点大户:`column_set` + 2 个 `column` + 标题 div/text + button/text/
|
|
19
|
+
* behaviors/value ≈ 10 个 JSON 节点;纯标题行只有 2 个。24 行全带按钮 ≈ 240 节点,
|
|
20
|
+
* 直接撞飞书的元素总数上限(实测 203 通过 / 204 失败,见 card-budget.ts)。
|
|
21
|
+
* 最近 8 行保留按钮(用户最可能展开的就是刚跑完的),更早的行退化成一行标题。
|
|
22
|
+
*/
|
|
23
|
+
export const MAX_TOOL_ROWS_WITH_BUTTON = 8;
|
|
13
24
|
const DEGRADED_TOOL_KEEP = 3; // Degraded: keep last 3 tool panels
|
|
14
25
|
const EXTREME_TOOL_KEEP = 1; // Extreme: keep last 1 tool panel
|
|
15
26
|
// --- Budget estimate (先估后建) ---
|
|
@@ -228,25 +239,97 @@ function groupBlocks(blocks) {
|
|
|
228
239
|
return groups;
|
|
229
240
|
}
|
|
230
241
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
242
|
+
* 工具块 = **原生折叠面板**(与 thinking/plan 同一交互)。
|
|
243
|
+
*
|
|
244
|
+
* 早先试过"一行 + 服务端「详情」按钮":折叠不省字节所以正文不入 payload,展开
|
|
245
|
+
* 只能回服务端重出卡。代价是手机上要多点一次、按钮还占掉标题宽度(长 URL 被挤
|
|
246
|
+
* 成两行,2026-10-19 反馈)。现在改成折叠面板 + **按工具数量缩放的正文截断**:
|
|
247
|
+
* 正文始终随卡下发(面板折叠时不显示),点标题即展开,无往返、无按钮。
|
|
248
|
+
*
|
|
249
|
+
* 展开态:运行中/失败的默认展开(用户在等结果、错误要立刻可见),其余折叠。
|
|
234
250
|
*/
|
|
235
|
-
function renderTool(tool,
|
|
236
|
-
const
|
|
237
|
-
const body = toolBodyMd(tool);
|
|
251
|
+
function renderTool(tool, outputMax) {
|
|
252
|
+
const body = toolBodyMd(tool, { outputMax });
|
|
238
253
|
return collapsibleMarkdownPanel({
|
|
239
254
|
title: toolTitle(tool),
|
|
240
|
-
expanded:
|
|
241
|
-
border,
|
|
255
|
+
expanded: tool.status === 'running' || tool.status === 'error',
|
|
256
|
+
border: tool.status === 'error' ? 'red' : 'grey',
|
|
242
257
|
content: body || '_无输出_',
|
|
243
258
|
textSize: 'notation',
|
|
244
259
|
});
|
|
245
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* 每个工具的正文输出预算:工具越多,单个工具留的字符越少。
|
|
263
|
+
*
|
|
264
|
+
* 24 个工具(`MAX_BLOCKS`,也是上限)× 1200 字符的正文会把卡片顶到 28KB 以上
|
|
265
|
+
* (实测 13 个 1200 字符的面板就开始走 skeleton 降级)。这里按总量反推:正文
|
|
266
|
+
* 合计约 8KB,单个工具 160–1200 字符——够看清前几行输出,又不会让整卡超预算。
|
|
267
|
+
*/
|
|
268
|
+
function toolOutputBudget(state) {
|
|
269
|
+
let count = 0;
|
|
270
|
+
for (const block of state.blocks)
|
|
271
|
+
if (block.kind === 'tool')
|
|
272
|
+
count += 1;
|
|
273
|
+
return Math.max(160, Math.min(1200, Math.floor(8000 / Math.max(1, count))));
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* 工具面板标题:`✅ **bash** — npm test (21:23)`。
|
|
277
|
+
*
|
|
278
|
+
* 标题连同时间戳一起收进 `HEADER_DISPLAY_MAX`(见 tool-render)——手机上折叠
|
|
279
|
+
* 面板的标题只有一行可用,超了就会断在 URL/命令中间。时间戳用 `HH:mm` 而不是
|
|
280
|
+
* 完整日期:同一张卡里的时间戳只需要区分先后。
|
|
281
|
+
*/
|
|
246
282
|
function toolTitle(tool) {
|
|
247
|
-
const
|
|
248
|
-
const title = toolHeaderText(tool);
|
|
249
|
-
return
|
|
283
|
+
const clock = formatClock(tool.completedAt ?? tool.startedAt);
|
|
284
|
+
const title = toolHeaderText(tool, clock ? ` (${clock})` : '');
|
|
285
|
+
return title;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* 内容组的元素成本(口径 = 递归 `elements` 数组成员,与飞书实测判据一致)。
|
|
289
|
+
*
|
|
290
|
+
* 工具是折叠面板:面板 1 + 内部 div 1 = 2 个元素,与展开态无关(折叠跟展开的
|
|
291
|
+
* payload 一样,这正是"折叠不省字节"的含义——所以靠截断控制大小,而不是靠
|
|
292
|
+
* 不发送正文)。
|
|
293
|
+
*/
|
|
294
|
+
const TOOL_PANEL_ELEMENTS = 2;
|
|
295
|
+
const PANEL_ELEMENTS = 2;
|
|
296
|
+
const TEXT_BLOCK_ELEMENTS = 1;
|
|
297
|
+
/**
|
|
298
|
+
* 内容区(blocks)可用的元素预算 = 总预算 − 固定开销预留。
|
|
299
|
+
*
|
|
300
|
+
* 固定开销:状态行 1 + summary 1 + 操作行 4 + 省略提示/通知若干,实测约 10;
|
|
301
|
+
* 预留 20 留出通知与提示的波动空间。
|
|
302
|
+
*/
|
|
303
|
+
const CONTENT_ELEMENT_BUDGET = MAX_CARD_ELEMENTS - 20;
|
|
304
|
+
/** 单个内容组的元素成本(保守:宁多不少)。 */
|
|
305
|
+
function groupElementCost(group) {
|
|
306
|
+
if (group.kind === 'tool')
|
|
307
|
+
return TOOL_PANEL_ELEMENTS;
|
|
308
|
+
if (group.kind === 'text')
|
|
309
|
+
return TEXT_BLOCK_ELEMENTS;
|
|
310
|
+
return PANEL_ELEMENTS;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* 元素预算内从最新往回选择可见的内容组。
|
|
314
|
+
*
|
|
315
|
+
* 为什么在正常路径里先裁剪而不是整卡降级:降级档(degraded)只保留最后 3 个
|
|
316
|
+
* 工具 + 2 个思考块,对一次跑了几十步的 run 是灾难性的信息损失;元素超限的
|
|
317
|
+
* 成因只是"元素多",砍掉最旧的几步就能同时保住元素预算和最新内容的完整渲染。
|
|
318
|
+
* 被省略的数量由调用方渲染成 `_已省略 N 个早期步骤_`(与 omittedBlocks 同款提示)。
|
|
319
|
+
*/
|
|
320
|
+
function selectVisibleGroups(groups) {
|
|
321
|
+
if (groups.length === 0)
|
|
322
|
+
return { visible: groups, omitted: 0 };
|
|
323
|
+
let used = 0;
|
|
324
|
+
let start = groups.length;
|
|
325
|
+
for (let i = groups.length - 1; i >= 0; i--) {
|
|
326
|
+
const cost = groupElementCost(groups[i]);
|
|
327
|
+
if (start < groups.length && used + cost > CONTENT_ELEMENT_BUDGET)
|
|
328
|
+
break;
|
|
329
|
+
used += cost;
|
|
330
|
+
start = i;
|
|
331
|
+
}
|
|
332
|
+
return { visible: groups.slice(start), omitted: start };
|
|
250
333
|
}
|
|
251
334
|
const DEGRADED_TIER = {
|
|
252
335
|
thinkingKeep: DEGRADED_THINKING_KEEP,
|
|
@@ -331,6 +414,7 @@ function assembleRunCard(state, options, elements) {
|
|
|
331
414
|
*/
|
|
332
415
|
function buildFallbackElements(state, options, tier) {
|
|
333
416
|
const elements = [];
|
|
417
|
+
const outputMax = toolOutputBudget(state);
|
|
334
418
|
elements.push(statusRow(state));
|
|
335
419
|
elements.push(...omittedBlocksHint(state));
|
|
336
420
|
// 单遍分桶:thinking / tool(保持原相对顺序)
|
|
@@ -403,7 +487,7 @@ function buildFallbackElements(state, options, tier) {
|
|
|
403
487
|
}
|
|
404
488
|
else if (group.kind === 'tool') {
|
|
405
489
|
// Show tool individually (each tool is independent now)
|
|
406
|
-
elements.push(renderTool(group.tool,
|
|
490
|
+
elements.push(renderTool(group.tool, outputMax));
|
|
407
491
|
}
|
|
408
492
|
}
|
|
409
493
|
if (elements.length === 0 && tier === EXTREME_TIER) {
|
|
@@ -429,25 +513,87 @@ function buildDegradedElements(state, options) {
|
|
|
429
513
|
function buildExtremeFallbackElements(state, options) {
|
|
430
514
|
return buildFallbackElements(state, options, EXTREME_TIER);
|
|
431
515
|
}
|
|
432
|
-
/**
|
|
516
|
+
/**
|
|
517
|
+
* 状态行内容:**一行、一个 div、窄屏不换行**。
|
|
518
|
+
*
|
|
519
|
+
* 设计约束(2026-10-19 手机端实测):旧版把「状态标签」和「耗时/指标」放进
|
|
520
|
+
* `column_set` 的 auto + grow 两列,窄屏上 auto 列被挤到只剩两个字宽,
|
|
521
|
+
* "运行中" 被拆成 "运行 / 中"。列式布局的可用宽度取决于对端渲染,服务端无法
|
|
522
|
+
* 保证;所以这里改成单 div,段间用 " · " 分隔——换行只会发生在分隔符处,
|
|
523
|
+
* 而且整行控制在 ~26 个半角宽度内,320px 宽的手机也能放下。
|
|
524
|
+
*
|
|
525
|
+
* 状态标签(运行中/已完成…)不再重复:header 已经写了(`🔧 dsh · 调用工具`
|
|
526
|
+
* / `✅ dsh · 已完成`),终态 summary 还有一次。
|
|
527
|
+
*
|
|
528
|
+
* 导出供窄屏守卫测试断言分段宽度(与 `estimateCardBytes` 同一惯例)。
|
|
529
|
+
*/
|
|
530
|
+
export function statusLine(state) {
|
|
531
|
+
const parts = [];
|
|
532
|
+
// 等待人工决策是「现在卡在哪儿」的唯一线索,值得在状态行重复一次 header
|
|
533
|
+
// (窄屏上用户往往只扫这一行)。其余活动词(思考中/输出中)不重复,header 有。
|
|
534
|
+
if (hasPendingApproval(state) && state.terminal === 'running')
|
|
535
|
+
parts.push('✋ 等待审批');
|
|
536
|
+
const elapsed = elapsedLabel(state);
|
|
537
|
+
if (elapsed)
|
|
538
|
+
parts.push(`⏱ ${elapsed}`);
|
|
539
|
+
// 运行期投影只在 running/finalizing 展示:终态 summary 有权威统计,同一张卡
|
|
540
|
+
// 上出现两次只会互相矛盾。
|
|
541
|
+
const live = state.liveStatus;
|
|
542
|
+
if (live && (state.terminal === 'running' || state.terminal === 'finalizing')) {
|
|
543
|
+
if (live.steps !== undefined)
|
|
544
|
+
parts.push(`🧰 ${live.steps} 步`);
|
|
545
|
+
if (live.contextUsed !== undefined &&
|
|
546
|
+
live.contextLimit !== undefined &&
|
|
547
|
+
live.contextLimit > 0) {
|
|
548
|
+
// 官方口径:projectedTokens ?? pressureTokens,取整封顶 100;`~` 表示投影值。
|
|
549
|
+
const pct = Math.min(100, Math.round((live.contextUsed / live.contextLimit) * 100));
|
|
550
|
+
parts.push(`📊 ~${pct}%`);
|
|
551
|
+
}
|
|
552
|
+
else if (live.contextUsed !== undefined) {
|
|
553
|
+
parts.push(`📊 ${formatTokens(live.contextUsed)}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (parts.length === 0) {
|
|
557
|
+
// 没有任何可用指标时退回状态词(骨架卡/测试构造的状态没有时间戳)。
|
|
558
|
+
return statusTagLabel(state.terminal);
|
|
559
|
+
}
|
|
560
|
+
return parts.join(' · ');
|
|
561
|
+
}
|
|
562
|
+
/** 耗时文案:`12s` / `2m13s` / `1h04m` / `2d3h`。空 = 无时间戳。 */
|
|
563
|
+
function elapsedLabel(state) {
|
|
564
|
+
if (state.startedAt === undefined)
|
|
565
|
+
return '';
|
|
566
|
+
const end = state.finishedAt ?? Date.now();
|
|
567
|
+
const ms = end - state.startedAt;
|
|
568
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
569
|
+
return '';
|
|
570
|
+
const totalSeconds = Math.floor(ms / 1000);
|
|
571
|
+
if (totalSeconds < 60)
|
|
572
|
+
return `${totalSeconds}s`;
|
|
573
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
574
|
+
if (minutes < 60)
|
|
575
|
+
return `${minutes}m${String(totalSeconds % 60).padStart(2, '0')}s`;
|
|
576
|
+
const hours = Math.floor(minutes / 60);
|
|
577
|
+
if (hours < 24)
|
|
578
|
+
return `${hours}h${String(minutes % 60).padStart(2, '0')}m`;
|
|
579
|
+
return `${Math.floor(hours / 24)}d${String(hours % 24).padStart(2, '0')}h`;
|
|
580
|
+
}
|
|
581
|
+
/** token 数量的紧凑写法:`950` / `12K` / `1.2M`。 */
|
|
582
|
+
function formatTokens(tokens) {
|
|
583
|
+
if (tokens < 1000)
|
|
584
|
+
return String(tokens);
|
|
585
|
+
if (tokens < 1_000_000)
|
|
586
|
+
return `${Math.round(tokens / 1000)}K`;
|
|
587
|
+
return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* 状态行元素(full/degraded/extreme/skeleton 共用)。
|
|
591
|
+
*
|
|
592
|
+
* 单 div + `notation` 字号:列式布局在窄屏会把短标签挤成竖排(手机端实测),
|
|
593
|
+
* 单行文本的换行点只在 " · " 处,可预期。
|
|
594
|
+
*/
|
|
433
595
|
function statusRow(state) {
|
|
434
|
-
|
|
435
|
-
const durationInfo = buildDurationInfo(state);
|
|
436
|
-
return {
|
|
437
|
-
tag: 'column_set',
|
|
438
|
-
columns: [
|
|
439
|
-
{
|
|
440
|
-
tag: 'column',
|
|
441
|
-
width: 'auto',
|
|
442
|
-
elements: [{ tag: 'div', text: { content: statusLabel, tag: 'lark_md' } }],
|
|
443
|
-
},
|
|
444
|
-
{
|
|
445
|
-
tag: 'column',
|
|
446
|
-
width: 'grow',
|
|
447
|
-
elements: [{ tag: 'div', text: { content: durationInfo, tag: 'lark_md' } }],
|
|
448
|
-
},
|
|
449
|
-
],
|
|
450
|
-
};
|
|
596
|
+
return markdownDiv(statusLine(state), 'notation');
|
|
451
597
|
}
|
|
452
598
|
/**
|
|
453
599
|
* Minimal skeleton card — the final safety net when even the extreme fallback
|
|
@@ -551,11 +697,14 @@ function describePanelGroup(group, prepared) {
|
|
|
551
697
|
}
|
|
552
698
|
/** 导出供测试断言估算精度(估算 ≈ 实际渲染字节,见 tests/anchor/run-card/run-card.test.ts)。 */
|
|
553
699
|
export function estimateCardBytes(state, options = {}, shared) {
|
|
554
|
-
const finalized = state.terminal !== 'running';
|
|
555
700
|
// renderRunCard 传入共享 groups/prepared 时直接复用(截断只做一次);
|
|
556
701
|
// 独立调用(测试/预算探针)时自建,行为与原先一致。
|
|
557
702
|
const groups = shared?.groups ?? groupBlocks(state.blocks);
|
|
558
703
|
const prepared = shared?.prepared ?? prepareGroupContent(groups);
|
|
704
|
+
// 与 buildChronologicalContent 同源:估算必须测量"将要渲染的内容",而正常
|
|
705
|
+
// 路径会按节点预算省略最旧的组(不模拟这一步会系统性高估)。
|
|
706
|
+
const { visible, omitted } = selectVisibleGroups(groups);
|
|
707
|
+
const outputMax = toolOutputBudget(state);
|
|
559
708
|
// 卡片外壳直接实测 assembleRunCard 空元素产物(schema/config/header/body 结构
|
|
560
709
|
// 与正式渲染完全一致);status/summary/操作行同样实测渲染结构。
|
|
561
710
|
// 注:审批区(approvalArea)不参与估算——待审批通常在途且体积小,超预算由
|
|
@@ -566,11 +715,15 @@ export function estimateCardBytes(state, options = {}, shared) {
|
|
|
566
715
|
measureJson(actionRow(state, options));
|
|
567
716
|
// 块内容:与 buildChronologicalContent 同源(describePanelGroup 单源)
|
|
568
717
|
let renderedAny = false;
|
|
569
|
-
|
|
718
|
+
if (omitted > 0) {
|
|
719
|
+
renderedAny = true;
|
|
720
|
+
total += measureJson(markdownDiv(`_💡 已省略 ${omitted} 个早期步骤_`));
|
|
721
|
+
}
|
|
722
|
+
for (const group of visible) {
|
|
570
723
|
if (group.kind === 'tool') {
|
|
571
724
|
renderedAny = true;
|
|
572
725
|
// tool 直接实测真实渲染元素(复用 renderTool,output ≤1200 字符,物化廉价)
|
|
573
|
-
total += measureJson(renderTool(group.tool,
|
|
726
|
+
total += measureJson(renderTool(group.tool, outputMax));
|
|
574
727
|
continue;
|
|
575
728
|
}
|
|
576
729
|
if (group.kind === 'text') {
|
|
@@ -636,14 +789,15 @@ export function renderRunCard(state, options = {}) {
|
|
|
636
789
|
...approvalArea(state),
|
|
637
790
|
];
|
|
638
791
|
const card = assembleRunCard(state, options, elements);
|
|
639
|
-
// Check budget — stringify
|
|
640
|
-
|
|
792
|
+
// Check budget — stringify 兜底(估算可能低估,仍保留安全网)。
|
|
793
|
+
// 两个维度都必须过:字节(28KB)与元素总数(飞书 11310,实测 203 节点上限)。
|
|
794
|
+
if (withinRunCardBudget(card))
|
|
641
795
|
return card;
|
|
642
796
|
// 超预算 → 继续走 degraded(不 return,fallthrough)
|
|
643
797
|
}
|
|
644
|
-
// 估算 ≥ 阈值 或
|
|
798
|
+
// 估算 ≥ 阈值 或 完整卡超预算 — 走 degraded 渲染
|
|
645
799
|
const degradedCard = assembleRunCard(state, options, buildDegradedElements(state, options));
|
|
646
|
-
if (
|
|
800
|
+
if (withinRunCardBudget(degradedCard)) {
|
|
647
801
|
return degradedCard;
|
|
648
802
|
}
|
|
649
803
|
// Extreme fallback: even degraded exceeds budget
|
|
@@ -651,7 +805,7 @@ export function renderRunCard(state, options = {}) {
|
|
|
651
805
|
// Final safety net: extreme fallback 是最后一层,但高转义内容(反斜杠 4× 膨胀)
|
|
652
806
|
// 可能让 extreme 产物本身 >28KB。补上 stringify 兜底(与 normal/degraded 对称):
|
|
653
807
|
// 超预算则降级到结构性保证 ≤28KB 的 skeleton 卡,避免飞书 11310 整卡不可用。
|
|
654
|
-
if (
|
|
808
|
+
if (withinRunCardBudget(extremeCard)) {
|
|
655
809
|
return extremeCard;
|
|
656
810
|
}
|
|
657
811
|
const skeletonCard = assembleRunCard(state, options, buildSkeletonElements(state, options));
|
|
@@ -675,16 +829,34 @@ export function renderRunCard(state, options = {}) {
|
|
|
675
829
|
};
|
|
676
830
|
return truncatedSkeleton;
|
|
677
831
|
}
|
|
832
|
+
/**
|
|
833
|
+
* Run 卡片的双预算检查:UTF-8 字节 ≤ 28KB **且** 元素数 ≤ `MAX_CARD_ELEMENTS`。
|
|
834
|
+
*
|
|
835
|
+
* 为什么两个维度都要查:字节超限来自内容长(截断即可),元素超限来自元素多
|
|
836
|
+
* (必须删元素)。一张只有几十个短文本的卡片可以远低于 28KB 却把飞书的元素
|
|
837
|
+
* 总数撑爆(实测 24 行工具 + 24 个展开正文 = 103 个元素 → 11310),只查字节
|
|
838
|
+
* 的安全网抓不到它。
|
|
839
|
+
*/
|
|
840
|
+
function withinRunCardBudget(card) {
|
|
841
|
+
if (countCardElements(card) > MAX_CARD_ELEMENTS)
|
|
842
|
+
return false;
|
|
843
|
+
return Buffer.byteLength(JSON.stringify(card), 'utf8') <= CARD_BUDGET_BYTES;
|
|
844
|
+
}
|
|
678
845
|
/** Build content in chronological order using groupBlocks to interleave thinking/text/tools. */
|
|
679
846
|
function buildChronologicalContent(state, prepared, groups) {
|
|
680
847
|
const elements = [];
|
|
681
848
|
// finalized = 非 running(含 finalizing:主结果已出,thinking 折叠)
|
|
682
849
|
const finalized = state.terminal !== 'running';
|
|
850
|
+
// 节点预算内从最新往回选择可见组:超限时只省略最旧的几步,不整卡降级。
|
|
851
|
+
const { visible, omitted } = selectVisibleGroups(groups);
|
|
852
|
+
const outputMax = toolOutputBudget(state);
|
|
853
|
+
if (omitted > 0)
|
|
854
|
+
elements.push(markdownDiv(`_💡 已省略 ${omitted} 个早期步骤_`));
|
|
683
855
|
// renderRunCard 传入同一 groups 数组时直接复用(prepared 的键是组对象
|
|
684
856
|
// 引用,重建数组会让 prepared.get 永远 miss,退化为二次截断)。
|
|
685
857
|
// 面板组描述经 describePanelGroup 与估算共享同一份模板。
|
|
686
858
|
// notices 作为普通消息按到达时间插入内容流(有内容时不再沉到 summary)。
|
|
687
|
-
const contentItems = mergeNoticesIntoContent(
|
|
859
|
+
const contentItems = mergeNoticesIntoContent(visible, state.notices);
|
|
688
860
|
for (const item of contentItems) {
|
|
689
861
|
if (item.kind === 'notice') {
|
|
690
862
|
elements.push(renderNoticeRow(item.notice));
|
|
@@ -692,7 +864,7 @@ function buildChronologicalContent(state, prepared, groups) {
|
|
|
692
864
|
}
|
|
693
865
|
const group = item;
|
|
694
866
|
if (group.kind === 'tool') {
|
|
695
|
-
elements.push(renderTool(group.tool,
|
|
867
|
+
elements.push(renderTool(group.tool, outputMax));
|
|
696
868
|
continue;
|
|
697
869
|
}
|
|
698
870
|
if (group.kind === 'text') {
|
|
@@ -788,28 +960,52 @@ function buildSummaryContent(state, options = {}) {
|
|
|
788
960
|
reasoningTokens: state.reasoningTokens,
|
|
789
961
|
}, { showResult: true, result });
|
|
790
962
|
elements.push(markdownDiv(usageStatsStr + empty));
|
|
963
|
+
// 后台任务:跨 turn 生命周期,必须显式告知并可逐个停止。
|
|
964
|
+
elements.push(...buildBackgroundJobsSection(state));
|
|
791
965
|
}
|
|
792
966
|
return elements;
|
|
793
967
|
}
|
|
794
|
-
/**
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
968
|
+
/** 终态卡最多列几个后台任务(多出来的只报数)。 */
|
|
969
|
+
const MAX_BACKGROUND_JOB_LINES = 3;
|
|
970
|
+
/**
|
|
971
|
+
* 后台任务小节:一行标题 + 每个任务一行(标签 + 停止按钮)。
|
|
972
|
+
*
|
|
973
|
+
* 按钮 payload 只带 runId/jobId:sessionId 与 cwd 由 bridge 的快照解析,卡片
|
|
974
|
+
* 上少放一个可变字段就少一处不一致。
|
|
975
|
+
*/
|
|
976
|
+
function buildBackgroundJobsSection(state) {
|
|
977
|
+
const jobs = state.backgroundJobs ?? [];
|
|
978
|
+
if (jobs.length === 0)
|
|
979
|
+
return [];
|
|
980
|
+
const elements = [
|
|
981
|
+
markdownDiv(`🧵 **后台任务** ${jobs.length} 个仍在运行(本轮结束后继续)`),
|
|
982
|
+
];
|
|
983
|
+
for (const job of jobs.slice(0, MAX_BACKGROUND_JOB_LINES)) {
|
|
984
|
+
// label 是任务自报的一行描述,可能含反引号/regex 反斜杠:按 markdown 安全
|
|
985
|
+
// 形态收敛(markdownDiv 只转义反斜杠,反引号会截断行内代码)。
|
|
986
|
+
const label = truncate(job.label.replace(/[\r\n]+/g, ' ').replace(/`/g, "'"), 60);
|
|
987
|
+
elements.push({
|
|
988
|
+
tag: 'column_set',
|
|
989
|
+
columns: [
|
|
990
|
+
{
|
|
991
|
+
tag: 'column',
|
|
992
|
+
width: 'grow',
|
|
993
|
+
elements: [markdownDiv(`· \`${label}\``, 'notation')],
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
tag: 'column',
|
|
997
|
+
width: 'auto',
|
|
998
|
+
elements: [
|
|
999
|
+
cardButton('⏹ 停止', { cmd: 'job.kill', runId: state.runId, jobId: job.id }, { size: 'small', type: 'danger' }),
|
|
1000
|
+
],
|
|
1001
|
+
},
|
|
1002
|
+
],
|
|
1003
|
+
});
|
|
801
1004
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
return
|
|
806
|
-
}
|
|
807
|
-
function footerText(footer) {
|
|
808
|
-
if (footer === 'tool_running')
|
|
809
|
-
return '🧰 工具调用中';
|
|
810
|
-
if (footer === 'streaming')
|
|
811
|
-
return '✍️ 输出中';
|
|
812
|
-
return '🧠 思考中';
|
|
1005
|
+
if (jobs.length > MAX_BACKGROUND_JOB_LINES) {
|
|
1006
|
+
elements.push(markdownDiv(`_…另有 ${jobs.length - MAX_BACKGROUND_JOB_LINES} 个任务_`));
|
|
1007
|
+
}
|
|
1008
|
+
return elements;
|
|
813
1009
|
}
|
|
814
1010
|
/** Status tag label */
|
|
815
1011
|
function statusTagLabel(terminal) {
|
package/dist/card/run-state.d.ts
CHANGED
|
@@ -72,6 +72,10 @@ export interface RunState {
|
|
|
72
72
|
runId: string;
|
|
73
73
|
terminal: RunTerminal;
|
|
74
74
|
footer: RunFooter;
|
|
75
|
+
/** 本 run 开始时间(epoch ms):状态行的「⏱ 12s」由它算出。 */
|
|
76
|
+
startedAt?: number;
|
|
77
|
+
/** 本 run 结束时间(epoch ms):终态后耗时定格,不再随渲染时刻增长。 */
|
|
78
|
+
finishedAt?: number;
|
|
75
79
|
blocks: RunBlock[];
|
|
76
80
|
sessionId?: string;
|
|
77
81
|
resultSubtype?: 'success' | 'error' | 'interrupted';
|
|
@@ -135,6 +139,28 @@ export interface RunState {
|
|
|
135
139
|
* 终态渲染(2026-08-15 事故:标记被消费后卡片误报「已被用户终止」)。
|
|
136
140
|
*/
|
|
137
141
|
approvalExpired?: boolean;
|
|
142
|
+
/**
|
|
143
|
+
* 运行期状态投影(DSH `session/control` 的 contextPressure / sessionStats)。
|
|
144
|
+
*
|
|
145
|
+
* 只在 running/finalizing 渲染成状态行的一条(终态卡有权威 summary,不再
|
|
146
|
+
* 重复)。字段缺省 = 该维度暂无数据。
|
|
147
|
+
*/
|
|
148
|
+
liveStatus?: {
|
|
149
|
+
contextUsed?: number;
|
|
150
|
+
contextLimit?: number;
|
|
151
|
+
steps?: number;
|
|
152
|
+
toolMs?: number;
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* run 结束时仍在后台运行的任务。跨 turn 生命周期:卡片必须说清楚"这一轮
|
|
156
|
+
* 结束了,但还有东西在跑",并给出逐个停止的入口。
|
|
157
|
+
*/
|
|
158
|
+
backgroundJobs?: Array<{
|
|
159
|
+
id: string;
|
|
160
|
+
label: string;
|
|
161
|
+
status?: string;
|
|
162
|
+
progress?: string;
|
|
163
|
+
}>;
|
|
138
164
|
}
|
|
139
165
|
export interface FinishMeta {
|
|
140
166
|
resultSubtype?: 'success' | 'error' | 'interrupted';
|
package/dist/card/run-state.js
CHANGED
|
@@ -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 } : {}),
|
package/dist/card/time.d.ts
CHANGED
|
@@ -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
|
|
39
|
+
export declare function toolBodyMd(tool: ToolEntry, opts?: {
|
|
40
|
+
outputMax?: number;
|
|
41
|
+
}): string;
|