@zhushanwen/pi-subagent-workflow 8.14.7 → 9.0.1
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/README.md +6 -5
- package/package.json +14 -9
- package/relay/relay.mjs +45 -2
- package/skills/workflow-script-format/SKILL.md +2 -56
- package/src/host/__tests__/inflight-reporter.test.ts +26 -1
- package/src/host/__tests__/pi-host.test.ts +418 -4
- package/src/host/inflight-reporter.ts +13 -10
- package/src/host/pi-host.ts +205 -17
- package/src/index.ts +21 -72
- package/src/injectors/__tests__/engine-awareness.test.ts +236 -33
- package/src/injectors/__tests__/helpers/injector-test-mocks.ts +15 -7
- package/src/injectors/__tests__/workflow-list-injector.test.ts +447 -40
- package/src/injectors/resource-list-injector.ts +103 -32
- package/src/injectors/subagent-list-injector.ts +4 -28
- package/src/injectors/workflow-list-injector.ts +8 -4
- package/src/interface/__tests__/capture-tool.ts +56 -0
- package/src/interface/__tests__/commands-resume.test.ts +103 -0
- package/src/interface/__tests__/tool-prompt-contract.test.ts +361 -0
- package/src/interface/__tests__/tool-render.test.ts +105 -2
- package/src/interface/__tests__/tool-subagents.test.ts +24 -26
- package/src/interface/__tests__/tool-workflow-resume.test.ts +227 -0
- package/src/interface/__tests__/tool-workflow-script.test.ts +557 -0
- package/src/interface/__tests__/tool-workflow.test.ts +884 -0
- package/src/interface/bg-notify-render.ts +18 -9
- package/src/interface/command-actions.ts +22 -17
- package/src/interface/commands.ts +58 -12
- package/src/interface/format.ts +50 -16
- package/src/interface/id-preview.ts +4 -4
- package/src/interface/subagent-actions.ts +18 -50
- package/src/interface/subagent-tool.ts +26 -18
- package/src/interface/subagents.ts +5 -11
- package/src/interface/tool-render.ts +14 -30
- package/src/interface/tool-shared.ts +55 -12
- package/src/interface/tool-subagents.ts +21 -11
- package/src/interface/tool-workflow-script.ts +52 -72
- package/src/interface/tool-workflow.ts +345 -139
- package/src/interface/views/WorkflowsView.ts +10 -7
- package/src/interface/views/__tests__/{WorkflowsView-signature.test.ts → WorkflowsView.test.ts} +155 -11
- package/src/interface/views/detail-content.ts +3 -5
- package/src/jsonl-run-store.ts +656 -544
- package/src/model-events.ts +35 -0
- package/src/session-lifecycle.ts +349 -86
- package/src/subagents-events.ts +56 -0
- package/src/workflow-events.ts +285 -200
- package/src/workflow-notify.ts +456 -0
- package/src/injectors/__tests__/engine-section-stability.test.ts +0 -353
- package/src/injectors/__tests__/model-list-injector.test.ts +0 -136
- package/src/injectors/__tests__/subagent-list-injector.test.ts +0 -376
- package/src/interface/__tests__/detectors.test.ts +0 -161
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +0 -183
- package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +0 -219
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +0 -251
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +0 -215
- package/src/interface/__tests__/workflow-state-file-exposure.test.ts +0 -38
- package/src/interface/__tests__/workflow-tool-prompt.test.ts +0 -129
- package/src/interface/helpers.ts +0 -272
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +0 -73
- package/src/interface/views/__tests__/record-progress.test.ts +0 -235
|
@@ -48,6 +48,9 @@ import {
|
|
|
48
48
|
statusDotStr,
|
|
49
49
|
type ThemeLike,
|
|
50
50
|
} from "../format.ts";
|
|
51
|
+
// [W2/V1 D1 分流表第 9 行] CLI 视图显示换源:投影二值 status(混合判源收拢在
|
|
52
|
+
// displayStatusOf 单点——判源 = core runSummary 投影)。
|
|
53
|
+
import { displayStatusOf } from "../tool-workflow.ts";
|
|
51
54
|
import {
|
|
52
55
|
b,
|
|
53
56
|
dashes,
|
|
@@ -186,7 +189,7 @@ export function collectNodeLiveProgress(
|
|
|
186
189
|
*
|
|
187
190
|
* | 签名字段 | 消费点 |
|
|
188
191
|
* |---|---|
|
|
189
|
-
* | run
|
|
192
|
+
* | displayStatusOf(run) 投影二值([W2/V1 D1 第 6 行] 混合判源单点——v2 fold/注册表投影 ∨ v1 聚合读面)| WorkflowsView.ts renderHeader(statusDotStr+statusLabel 同串源)/ detail-content.ts statusLabel(statusDotStr 调用处)/ renderFooter |
|
|
190
193
|
* | 秒桶 Math.floor(now/1000) | renderHeader formatElapsed(现算 elapsed)+ detail-content.ts(now 参与未完成节点 elapsed)|
|
|
191
194
|
* | completed/total(节点 status 推导)| renderHeader |
|
|
192
195
|
* | budget 量化值(tokens round-k + cost toFixed(4)=BUDGET_COST_DECIMALS,与 renderHeader budgetStr 同精度——第 3-4 位小数变化是可见变化)| renderHeader / saveTraceToFile |
|
|
@@ -230,7 +233,7 @@ export function computeRenderSignature(
|
|
|
230
233
|
const l = liveProgress.get(n.stepIndex);
|
|
231
234
|
return `${n.stepIndex}:${n.status}:${n.sessionFile ?? "-"}:${l?.totalTokens ?? -1}:${l?.toolCallCount ?? -1}:${l?.elapsedSeconds ?? -1}:${l?.turns ?? -1}:${l?.eventLog.length ?? -1}:${l?.currentActivity ? `${l.currentActivity.type}:${l.currentActivity.label}` : "-"}:${l?.lastError ?? "-"}`;
|
|
232
235
|
});
|
|
233
|
-
return [run
|
|
236
|
+
return [displayStatusOf(run), Math.floor(now / SECOND_MS), `${completed}/${traceArr.length}`, budgetPart, errorLogsPart, ...nodeParts].join("|");
|
|
234
237
|
}
|
|
235
238
|
|
|
236
239
|
// ── View state ────────────────────────────────────────────────
|
|
@@ -281,7 +284,7 @@ function createInitialState(): ViewState {
|
|
|
281
284
|
/**
|
|
282
285
|
* 创建 workflow fullscreen view。
|
|
283
286
|
*
|
|
284
|
-
* @param run WorkflowRun 聚合根(读
|
|
287
|
+
* @param run WorkflowRun 聚合根(读 displayStatusOf 投影二值/spec/trace/meta)
|
|
285
288
|
* @param theme ThemeLike(避免直接 import Pi runtime)
|
|
286
289
|
* @param ctx ExtensionContext(调 ui.custom 渲染 + ui.notify 错误反馈)
|
|
287
290
|
* @param actions lifecycle 操作(abort),由调用方注入
|
|
@@ -496,7 +499,7 @@ export function createWorkflowsView(
|
|
|
496
499
|
function handleShortcutKeys(data: string): void {
|
|
497
500
|
// ── Lifecycle shortcuts (no restart per D-9; no pause/resume — one-shot) ──
|
|
498
501
|
if (data === "a") {
|
|
499
|
-
if (run
|
|
502
|
+
if (displayStatusOf(run) === "running") {
|
|
500
503
|
void actions.abort(run.runId)
|
|
501
504
|
.then(() => { cache.key = undefined; requestRender(); })
|
|
502
505
|
.catch((err: Error) => ctx.ui.notify(`Abort failed: ${err.message}`, "error"));
|
|
@@ -703,7 +706,7 @@ function renderHeader(
|
|
|
703
706
|
const completed = traceArr.filter((n) => n.status === "completed").length;
|
|
704
707
|
const total = traceArr.length;
|
|
705
708
|
const elapsed = formatElapsed(run.meta.startedAt);
|
|
706
|
-
const headerRight = `${formatStatusBadge(run
|
|
709
|
+
const headerRight = `${formatStatusBadge(displayStatusOf(run), theme)} · ${completed}/${total} agents · ${elapsed}`;
|
|
707
710
|
const budget = run.state.budget;
|
|
708
711
|
const budgetStr = `${Math.round(budget.usedTokens / BUDGET_TOKENS_DIVISOR)}k/${budget.maxTokens ? `${Math.round(budget.maxTokens / BUDGET_TOKENS_DIVISOR)}k` : "∞"} tok · $${budget.usedCost.toFixed(BUDGET_COST_DECIMALS)}`;
|
|
709
712
|
|
|
@@ -746,7 +749,7 @@ function renderFooter(
|
|
|
746
749
|
? "↑↓ agent · ⏎ detail"
|
|
747
750
|
: "↑↓ agent · ⏎ prompt · PgUp/PgDn scroll";
|
|
748
751
|
const actionParts: string[] = [];
|
|
749
|
-
if (run
|
|
752
|
+
if (displayStatusOf(run) === "running") {
|
|
750
753
|
actionParts.push("a abort");
|
|
751
754
|
}
|
|
752
755
|
actionParts.push("s save");
|
|
@@ -1055,7 +1058,7 @@ function saveTraceToFile(run: WorkflowRun, ctx: ExtensionContext): void {
|
|
|
1055
1058
|
function traceHeaderLines(run: WorkflowRun): string[] {
|
|
1056
1059
|
const lines: string[] = [];
|
|
1057
1060
|
lines.push(`# Workflow Trace: ${run.spec.scriptName} (${run.runId})`, "");
|
|
1058
|
-
lines.push(`Status: ${run
|
|
1061
|
+
lines.push(`Status: ${displayStatusOf(run)} | Started: ${run.meta.startedAt ?? "-"} | Duration: ${formatElapsed(run.meta.startedAt)}`);
|
|
1059
1062
|
const budget = run.state.budget;
|
|
1060
1063
|
lines.push(`Budget: ${budget.usedTokens}/${budget.maxTokens ?? "unlimited"} tokens, $${budget.usedCost.toFixed(BUDGET_COST_DECIMALS)}`, "");
|
|
1061
1064
|
return lines;
|
package/src/interface/views/__tests__/{WorkflowsView-signature.test.ts → WorkflowsView.test.ts}
RENAMED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* WorkflowsView 纯函数三块合一(同被测文件 views/WorkflowsView.ts 及其 companion
|
|
3
|
+
* detail-content.ts):渲染签名 / live 配对 / detail session 路径行,fixture 同族共享。
|
|
4
|
+
*
|
|
5
|
+
* ── computeRenderSignature(IF11/TC7/DM6)— 渲染签名单测(now 参数化) ──
|
|
3
6
|
*
|
|
4
7
|
* 契约:tick 条件失效的判据——签名字段集覆盖 header(renderHeader)/节点行
|
|
5
8
|
* (renderLevel1 agent list)/L2 detail(buildDetailContent)当前消费的全部动态
|
|
@@ -10,8 +13,7 @@
|
|
|
10
13
|
* (Map<stepIndex, LiveProgressView>,经 collectNodeLiveProgress 配对注入)——
|
|
11
14
|
* 字段口径不变(totalTokens/toolCallCount/elapsedSeconds/turns/eventLog.length/
|
|
12
15
|
* currentActivity/lastError),本文件用 makeLiveView 直接构造投影驱动「已入字段
|
|
13
|
-
* 变 →
|
|
14
|
-
* record-progress.test.ts(双路径对齐断言)。
|
|
16
|
+
* 变 → 签名变」。
|
|
15
17
|
*
|
|
16
18
|
* 确定性说明:签名非完全纯——live 投影的 elapsedSeconds 由 projectRecordProgress
|
|
17
19
|
* 内 computeElapsedSeconds 现算(record.endedAt ?? Date.now())。fake timers 控制
|
|
@@ -19,13 +21,16 @@
|
|
|
19
21
|
*/
|
|
20
22
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
21
23
|
|
|
22
|
-
import { computeRenderSignature } from "../WorkflowsView.ts";
|
|
23
24
|
import type { LiveProgressView } from "../detail-content.ts";
|
|
25
|
+
import { buildDetailContent } from "../detail-content.ts";
|
|
26
|
+
import type { ThemeLike } from "../../format.ts";
|
|
27
|
+
import { collectNodeLiveProgress, computeRenderSignature } from "../WorkflowsView.ts";
|
|
24
28
|
import type { AgentEventLogEntry } from "@zhushanwen/subagent-core";
|
|
25
29
|
import type { ExecutionTraceNode, WorkerLogEntry } from "@zhushanwen/subagent-core";
|
|
30
|
+
import type { SubagentRecord } from "@zhushanwen/subagent-core";
|
|
26
31
|
import type { WorkflowRun } from "@zhushanwen/subagent-core";
|
|
27
32
|
|
|
28
|
-
// ── Fixtures(duck typing
|
|
33
|
+
// ── Fixtures(duck typing)──
|
|
29
34
|
|
|
30
35
|
const T0 = 1_700_000_000_000; // 固定 epoch ms
|
|
31
36
|
|
|
@@ -57,7 +62,7 @@ function makeNode(overrides: Partial<ExecutionTraceNode> = {}): ExecutionTraceNo
|
|
|
57
62
|
};
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
type RunShape = {
|
|
61
66
|
status?: string;
|
|
62
67
|
budget?: { usedTokens: number; maxTokens?: number; usedCost: number };
|
|
63
68
|
nodes?: ExecutionTraceNode[];
|
|
@@ -65,7 +70,17 @@ interface RunShape {
|
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
function makeRun(shape: RunShape = {}): WorkflowRun {
|
|
73
|
+
return makeRunWithId(`wf-sig-${signatureRunSeq++}`, shape);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** [W2/V1] runId 唯一化 + spec 最小面(displayStatusOf → core runSummary 投影
|
|
77
|
+
* 读 runId/spec.scriptName——签名测试的 mock 需携带该最小字段集)。 */
|
|
78
|
+
let signatureRunSeq = 0;
|
|
79
|
+
function makeRunWithId(runId: string, shape: RunShape): WorkflowRun {
|
|
68
80
|
return {
|
|
81
|
+
runId,
|
|
82
|
+
spec: { scriptName: "sig-wf" },
|
|
83
|
+
meta: { startedAt: "2026-09-27T00:00:00.000Z" },
|
|
69
84
|
state: {
|
|
70
85
|
status: shape.status ?? "running",
|
|
71
86
|
budget: shape.budget ?? { usedTokens: 0, maxTokens: 200_000, usedCost: 0 },
|
|
@@ -84,6 +99,35 @@ function runWithLive(nodeOverrides: Partial<ExecutionTraceNode>, live: LiveProgr
|
|
|
84
99
|
};
|
|
85
100
|
}
|
|
86
101
|
|
|
102
|
+
/** plain theme: 着色方法直接返回原文,无 ANSI 色码——测试断言纯文本。
|
|
103
|
+
* bg/underline 为宽 ThemeLike 接口的 no-op 实现(workflow 渲染面只用 fg/bold)。 */
|
|
104
|
+
const plainTheme: ThemeLike = {
|
|
105
|
+
bg: (_color: string, text: string) => text,
|
|
106
|
+
fg: (_tag: string, text: string) => text,
|
|
107
|
+
bold: (text: string) => text,
|
|
108
|
+
underline: (text: string) => text,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** collectNodeLiveProgress 配对用 run:只含 trace 节点面(task/status/startedAt)。 */
|
|
112
|
+
function makeRunShape(nodes: { stepIndex: number; task: string; status: string; startedAt: string }[]): WorkflowRun {
|
|
113
|
+
return {
|
|
114
|
+
state: { trace: { toArray: () => nodes } },
|
|
115
|
+
} as unknown as WorkflowRun;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function makeSub(over: Partial<SubagentRecord> = {}): SubagentRecord {
|
|
119
|
+
return {
|
|
120
|
+
id: "sa-match",
|
|
121
|
+
task: "调研 A",
|
|
122
|
+
status: "running",
|
|
123
|
+
startedAt: T0,
|
|
124
|
+
turns: 0,
|
|
125
|
+
totalTokens: 0,
|
|
126
|
+
eventLog: [],
|
|
127
|
+
...over,
|
|
128
|
+
} as SubagentRecord;
|
|
129
|
+
}
|
|
130
|
+
|
|
87
131
|
beforeEach(() => {
|
|
88
132
|
vi.useFakeTimers();
|
|
89
133
|
vi.setSystemTime(T0);
|
|
@@ -220,17 +264,35 @@ describe("computeRenderSignature — 节点级字段(store record 投影)",
|
|
|
220
264
|
});
|
|
221
265
|
|
|
222
266
|
describe("computeRenderSignature — 多节点与无 live 终态", () => {
|
|
223
|
-
it("
|
|
267
|
+
it("多节点:live 投影按 stepIndex 配对生效(live 值出现 / 消失 / 挂到另一节点 → 签名变)", () => {
|
|
268
|
+
// 对比式断言:同一 run 换 live 投影驱动签名变化——词法格式串是内部表示非契约,
|
|
269
|
+
// 不作为主证明(结构快照见下一条)。
|
|
224
270
|
const run = makeRun({
|
|
225
271
|
nodes: [
|
|
226
272
|
makeNode({ stepIndex: 0, status: "completed" }),
|
|
227
273
|
makeNode({ stepIndex: 1, status: "running" }),
|
|
228
274
|
],
|
|
229
275
|
});
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
276
|
+
const noLive = new Map<number, LiveProgressView>();
|
|
277
|
+
const liveOnNode1 = new Map<number, LiveProgressView>([[1, makeLiveView({ totalTokens: 500 })]]);
|
|
278
|
+
const liveOnNode0 = new Map<number, LiveProgressView>([[0, makeLiveView({ totalTokens: 500 })]]);
|
|
279
|
+
|
|
280
|
+
const base = computeRenderSignature(run, noLive, T0);
|
|
281
|
+
// live 值出现(挂到节点 1)→ 签名变
|
|
282
|
+
expect(computeRenderSignature(run, liveOnNode1, T0)).not.toBe(base);
|
|
283
|
+
// 同值 live 挂到另一节点 → 签名也变(配对按 stepIndex 生效,非全局均摊)
|
|
284
|
+
expect(computeRenderSignature(run, liveOnNode0, T0)).not.toBe(computeRenderSignature(run, liveOnNode1, T0));
|
|
285
|
+
// live 值消失 → 签名回落基线
|
|
286
|
+
expect(computeRenderSignature(run, noLive, T0)).toBe(base);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// 结构快照(不承担主证明):签名段集 = run 级五段 + 每节点一段;
|
|
290
|
+
// 段内词法编码是内部表示,不锁定。
|
|
291
|
+
it("多节点结构快照:两节点 run 签名共 7 段(run 级 5 + 节点 2)", () => {
|
|
292
|
+
const run = makeRun({
|
|
293
|
+
nodes: [makeNode({ stepIndex: 0 }), makeNode({ stepIndex: 1 })],
|
|
294
|
+
});
|
|
295
|
+
expect(computeRenderSignature(run, new Map(), T0).split("|")).toHaveLength(7);
|
|
234
296
|
});
|
|
235
297
|
|
|
236
298
|
it("节点重排(trace 数组序对调,stepIndex 维度)→ 签名变", () => {
|
|
@@ -249,3 +311,85 @@ describe("computeRenderSignature — 多节点与无 live 终态", () => {
|
|
|
249
311
|
expect(sig.split("|")).toHaveLength(5);
|
|
250
312
|
});
|
|
251
313
|
});
|
|
314
|
+
|
|
315
|
+
// ── collectNodeLiveProgress 配对([H2 W3] store record live 进度配对) ──
|
|
316
|
+
//
|
|
317
|
+
// 配对规则:task 标签匹配 + startedAt 最近邻 + 贪心唯一;终态 node 不配;
|
|
318
|
+
// 无匹配 record(重试间隙)返回空。
|
|
319
|
+
// SubagentRecord 投影面(eventLog/turns/totalTokens 等)的投影恒等由 core 侧
|
|
320
|
+
// record-store 测试锁定,此处消费投影面。
|
|
321
|
+
|
|
322
|
+
describe("collectNodeLiveProgress 配对", () => {
|
|
323
|
+
it("task 全等匹配:running node ↔ running record,投影挂到 stepIndex", () => {
|
|
324
|
+
const run = makeRunShape([{ stepIndex: 0, task: "调研 A", status: "running", startedAt: new Date(T0).toISOString() }]);
|
|
325
|
+
const live = collectNodeLiveProgress(run, [makeSub({ totalTokens: 42 })]);
|
|
326
|
+
expect(live.get(0)?.totalTokens).toBe(42);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("多候选(parallel 同 prompt)取 startedAt 最近邻,贪心一一不重复消费", () => {
|
|
330
|
+
const run = makeRunShape([
|
|
331
|
+
{ stepIndex: 0, task: "调研 A", status: "running", startedAt: new Date(T0).toISOString() },
|
|
332
|
+
{ stepIndex: 1, task: "调研 A", status: "running", startedAt: new Date(T0 + 5000).toISOString() },
|
|
333
|
+
]);
|
|
334
|
+
const near0 = makeSub({ id: "sa-near0", startedAt: T0 + 10, totalTokens: 100 });
|
|
335
|
+
const near1 = makeSub({ id: "sa-near1", startedAt: T0 + 5010, totalTokens: 200 });
|
|
336
|
+
const live = collectNodeLiveProgress(run, [near1, near0]);
|
|
337
|
+
expect(live.get(0)?.totalTokens).toBe(100); // node0(startedAt=T0) 最近 = near0
|
|
338
|
+
expect(live.get(1)?.totalTokens).toBe(200); // node1(T0+5s) 最近 = near1
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("终态 node 不配对;task 不匹配(不同 prompt)不配对", () => {
|
|
342
|
+
const run = makeRunShape([
|
|
343
|
+
{ stepIndex: 0, task: "调研 A", status: "completed", startedAt: new Date(T0).toISOString() },
|
|
344
|
+
{ stepIndex: 1, task: "写总结", status: "running", startedAt: new Date(T0).toISOString() },
|
|
345
|
+
]);
|
|
346
|
+
const live = collectNodeLiveProgress(run, [makeSub({ task: "调研 A", totalTokens: 99 })]);
|
|
347
|
+
expect(live.size).toBe(0); // node0 终态跳过;node1 task 不匹配
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("重试间隙(无 running record)→ 空 map(views 走终态 fallback 渲染)", () => {
|
|
351
|
+
const run = makeRunShape([{ stepIndex: 0, task: "调研 A", status: "running", startedAt: new Date(T0).toISOString() }]);
|
|
352
|
+
const live = collectNodeLiveProgress(run, [makeSub({ status: "closed" })]);
|
|
353
|
+
expect(live.size).toBe(0);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ── buildDetailContent session 路径行渲染 ──
|
|
358
|
+
//
|
|
359
|
+
// TUI 渲染测试:buildDetailContent 在 agent 终态时渲染 session: 路径行。
|
|
360
|
+
//
|
|
361
|
+
// 防的 bug:node.sessionFile 有值但 TUI detail 不显示——用户在交互面板里
|
|
362
|
+
// 看不到 agent 的 session jsonl 路径,无法定位文件做后续查看。
|
|
363
|
+
|
|
364
|
+
describe("buildDetailContent session 路径行渲染", () => {
|
|
365
|
+
it("node.sessionFile 有值 → 详情末尾渲染 session: <路径>", () => {
|
|
366
|
+
const sessionPath = "/abs/.pi/agent/subagents/enc/sessions/2026-07-15T_session-abc.jsonl";
|
|
367
|
+
const node = makeNode({
|
|
368
|
+
status: "completed",
|
|
369
|
+
sessionFile: sessionPath,
|
|
370
|
+
result: { content: "done" },
|
|
371
|
+
});
|
|
372
|
+
const lines = buildDetailContent(node, { promptExpanded: false }, makeRun(), plainTheme, 120, Date.now());
|
|
373
|
+
const sessionLine = lines.find((l) => l.includes("session:"));
|
|
374
|
+
expect(sessionLine).toBeDefined();
|
|
375
|
+
expect(sessionLine).toContain(sessionPath);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("node.sessionFile undefined(窗口期)→ 不渲染 session: 行", () => {
|
|
379
|
+
const node = makeNode({ status: "completed", sessionFile: undefined, result: { content: "done" } });
|
|
380
|
+
const lines = buildDetailContent(node, { promptExpanded: false }, makeRun(), plainTheme, 120, Date.now());
|
|
381
|
+
const sessionLine = lines.find((l) => l.includes("session:"));
|
|
382
|
+
expect(sessionLine).toBeUndefined();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("长路径截断到 mainWidth(防溢出)", () => {
|
|
386
|
+
const longPath = "/very/long/path/" + "x".repeat(200) + "/session.jsonl";
|
|
387
|
+
const node = makeNode({ status: "completed", sessionFile: longPath, result: { content: "done" } });
|
|
388
|
+
const narrowWidth = 60;
|
|
389
|
+
const lines = buildDetailContent(node, { promptExpanded: false }, makeRun(), plainTheme, narrowWidth, Date.now());
|
|
390
|
+
const sessionLine = lines.find((l) => l.includes("session:"));
|
|
391
|
+
expect(sessionLine).toBeDefined();
|
|
392
|
+
// 截断后行宽不超过 mainWidth + 边框余量
|
|
393
|
+
expect(sessionLine!.length).toBeLessThanOrEqual(narrowWidth);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
@@ -45,9 +45,8 @@ const DETAIL_LEN_PROBE_WIDTH = 9999;
|
|
|
45
45
|
// ── [H2 W3] store record 的 live 进度投影(设计 D2:进度源从 node.live 切 store)──
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* running record
|
|
49
|
-
*
|
|
50
|
-
* currentActivity / lastError)。
|
|
48
|
+
* running record 的实时进度投影(totalTokens / 工具计数 / elapsedSeconds / turns /
|
|
49
|
+
* eventLog / currentActivity / lastError)。
|
|
51
50
|
*/
|
|
52
51
|
export interface LiveProgressView {
|
|
53
52
|
totalTokens: number;
|
|
@@ -60,8 +59,7 @@ export interface LiveProgressView {
|
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
/**
|
|
63
|
-
* SubagentRecord(store 查询投影)→ LiveProgressView
|
|
64
|
-
* projectLiveProgress(node.live) 的字段等价性(构造性论证,S1 等价表依据):
|
|
62
|
+
* SubagentRecord(store 查询投影)→ LiveProgressView。字段口径(S1 等价表):
|
|
65
63
|
* - totalTokens / turns:recordToSubagent 直读 record.totalTokens / turnCount,同源;
|
|
66
64
|
* - toolCallCount:eventLog 里 tool_start 计数——getEventLog 对每个 toolCall 恰产
|
|
67
65
|
* 一条 tool_start,与 getAllToolCalls(record).length 恒等;
|