@zhushanwen/pi-subagent-workflow 9.0.1 → 9.1.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 (52) hide show
  1. package/package.json +10 -10
  2. package/skills/subagent-ext-config/SKILL.md +9 -11
  3. package/src/host/__tests__/pi-host.test.ts +0 -1
  4. package/src/index.ts +7 -7
  5. package/src/injectors/subagent-list-injector.ts +1 -1
  6. package/src/interface/command/__tests__/commands-open.test.ts +278 -0
  7. package/src/interface/{commands.ts → command/commands.ts} +15 -7
  8. package/src/interface/{subagents.ts → command/subagents.ts} +4 -4
  9. package/src/interface/format/__tests__/display-state.test.ts +146 -0
  10. package/src/interface/format/display-state.ts +112 -0
  11. package/src/interface/{format.ts → format/format.ts} +20 -14
  12. package/src/interface/{id-preview.ts → format/id-preview.ts} +1 -1
  13. package/src/interface/{bg-notify-render.ts → gui/bg-notify-render.ts} +60 -35
  14. package/src/interface/gui/gui-mappers.ts +74 -0
  15. package/src/interface/{subagent-actions.ts → gui/subagent-actions.ts} +3 -3
  16. package/src/interface/tool/__tests__/subagent-tool-actions.test.ts +238 -0
  17. package/src/interface/{__tests__ → tool/__tests__}/subagent-tool-path-guard.test.ts +6 -5
  18. package/src/interface/{__tests__ → tool/__tests__}/tool-subagents.test.ts +4 -4
  19. package/src/interface/tool/__tests__/tool-workflow-resume.test.ts +161 -0
  20. package/src/interface/{__tests__ → tool/__tests__}/tool-workflow.test.ts +33 -9
  21. package/src/interface/{subagent-tool-schema.ts → tool/subagent-tool-schema.ts} +1 -1
  22. package/src/interface/{subagent-tool.ts → tool/subagent-tool.ts} +4 -4
  23. package/src/interface/{tool-render.ts → tool/tool-render.ts} +3 -3
  24. package/src/interface/{tool-shared.ts → tool/tool-shared.ts} +1 -1
  25. package/src/interface/{tool-subagents.ts → tool/tool-subagents.ts} +12 -11
  26. package/src/interface/{tool-workflow-script.ts → tool/tool-workflow-script.ts} +1 -1
  27. package/src/interface/{tool-workflow.ts → tool/tool-workflow.ts} +20 -165
  28. package/src/interface/tui/__tests__/list-view.test.ts +188 -0
  29. package/src/interface/{list-component.ts → tui/list-component.ts} +2 -2
  30. package/src/interface/{list-shared.ts → tui/list-shared.ts} +1 -1
  31. package/src/interface/{list-view.ts → tui/list-view.ts} +2 -2
  32. package/src/interface/{tui-kit.ts → tui/tui-kit.ts} +1 -1
  33. package/src/interface/{views → tui/views}/WorkflowsView.ts +10 -6
  34. package/src/interface/tui/views/__tests__/WorkflowsView.driver.test.ts +595 -0
  35. package/src/interface/{views → tui/views}/__tests__/WorkflowsView.test.ts +10 -3
  36. package/src/interface/tui/views/__tests__/detail-content.test.ts +195 -0
  37. package/src/interface/{views → tui/views}/detail-content.ts +8 -3
  38. package/src/interface/{views → tui/views}/view-constants.ts +2 -2
  39. package/src/jsonl-run-store.ts +133 -81
  40. package/src/session-lifecycle.ts +2 -1
  41. package/src/workflow-events.ts +21 -3
  42. package/src/workflow-notify.ts +7 -6
  43. package/src/interface/__tests__/tool-workflow-resume.test.ts +0 -227
  44. package/src/interface/gui-mappers.ts +0 -81
  45. /package/src/interface/{__tests__ → command/__tests__}/commands-resume.test.ts +0 -0
  46. /package/src/interface/{command-actions.ts → command/command-actions.ts} +0 -0
  47. /package/src/interface/{__tests__ → tool/__tests__}/capture-tool.ts +0 -0
  48. /package/src/interface/{__tests__ → tool/__tests__}/tool-prompt-contract.test.ts +0 -0
  49. /package/src/interface/{__tests__ → tool/__tests__}/tool-render.test.ts +0 -0
  50. /package/src/interface/{__tests__ → tool/__tests__}/tool-workflow-script.test.ts +0 -0
  51. /package/src/interface/{reentry-guard.ts → tool/reentry-guard.ts} +0 -0
  52. /package/src/interface/{tool-result.ts → tool/tool-result.ts} +0 -0
@@ -66,7 +66,7 @@ import { WorkflowScriptRegistryImpl } from "@zhushanwen/subagent-core";
66
66
  // 管道),session_start / session_shutdown 驱动 attach/detach;测试可注入 fake。
67
67
  import { createInFlightReporter, type InFlightReporter } from "./host/inflight-reporter.ts";
68
68
  // [W2/V1 D1 第 8 行] 判活类消费面(isScriptRunning)经 core 投影单点
69
- //(runSummary.status 二值——内部收拢注册表 ∨ 聚合混合判源)。
69
+ //(runSummary.status 三态投影——终局判定源 = 终局记录注册表)。
70
70
  import { type RunSettlementRecord } from "./jsonl-run-store.ts";
71
71
  import { notifyDone, trackNotifiedRunId } from "./workflow-notify.ts";
72
72
  // ═══ 跨域事件注册(handler 体住各自域模块,本 seam 原位调用保注册顺序) ═══
@@ -74,6 +74,7 @@ import { setupNotifyLedgerCompactionGuard } from "./workflow-notify.ts";
74
74
  import { setupModelEvents } from "./model-events.ts";
75
75
  import { setupSubagentsCascadeEvents } from "./subagents-events.ts";
76
76
  // ═══ session 生命周期装配 seam(bootstrap seam,设计 §3.1/D1) ═══
77
+ import { GLOBAL_SLOT_KEYS } from "@zhushanwen/subagent-core";
77
78
  import {
78
79
  getOrCreateDialogQueue,
79
80
  setupSessionLifecycle,
@@ -125,7 +126,7 @@ function createWorkflowDomainState(): WorkflowDomainState {
125
126
  // 此槽拿回同一 domain state,session_shutdown(reload) 跳过清理(D1)保住的
126
127
  // 在飞 run / store 由 post-reload session_start(reason=reload) 的 adoption
127
128
  // 接管(D4,session-lifecycle.ts)。
128
- const WORKFLOW_DOMAIN_SLOT_KEY = Symbol.for("@zhushanwen/pi-subagents.workflow-domain-state");
129
+ const WORKFLOW_DOMAIN_SLOT_KEY = Symbol.for(GLOBAL_SLOT_KEYS.workflowDomainState);
129
130
 
130
131
  function getOrCreateWorkflowDomainState(): WorkflowDomainState {
131
132
  let state = Reflect.get(globalThis, WORKFLOW_DOMAIN_SLOT_KEY) as WorkflowDomainState | undefined;
@@ -148,7 +149,7 @@ function resolveCurrentPi(): ExtensionAPI {
148
149
  if (!pi) {
149
150
  throw new Error(
150
151
  "workflow deps current pi binding unset: setupWorkflowDomain has not run " +
151
- "(slot @zhushanwen/pi-subagents.workflow-domain-state); re-run extension factory to re-register",
152
+ `(slot ${GLOBAL_SLOT_KEYS.workflowDomainState}); re-run extension factory to re-register`,
152
153
  );
153
154
  }
154
155
  return pi;
@@ -482,6 +483,15 @@ export function setupWorkflowDomain(
482
483
  const sessionId = ctx.sessionManager.getSessionId();
483
484
  lsRef.lastSessionId = sessionId;
484
485
 
486
+ // [PS-68] tree 导航 = 同进程分支导航,非会话替换:pi 原地换叶子
487
+ //(agent.state.messages = sessionContext.messages)后 emit,不 teardown、不失效
488
+ // runner——pi 绑定保持有效,此处不作废。替换形态 = new/fork/resume/quit +
489
+ // switchSession(借 reason="resume",无专用枚举成员),全部发 session_shutdown;
490
+ // switchSession 的替换语义由 session_shutdown 非 reload 分支承接,与本 handler
491
+ // 无交集。reload 另有本文件 :546 显式作废。禁止在此 invalidatePiBinding:作废后
492
+ // 唯一重臂点 initSession 只由 session_start 触发,tree 导航不触发 session_start,
493
+ // 本 session 余生 record/notify 的 pi 写入将静默 no-op。
494
+ // [HISTORICAL] 曾按「switchSession 不发 session_shutdown、其内部路径即 tree 导航域」的假前提在此作废绑定,撤销依据与触发词表登记见 docs/pi-semantics.json PS-61。
485
495
  const state = sessionState.get(sessionId);
486
496
  if (state) {
487
497
  // 一次性生命周期(D-2):running run 转 done,failed 落盘(helper 内部自过滤
@@ -526,6 +536,14 @@ export function setupWorkflowDomain(
526
536
  // ════════════════════════════════════════════════════════════
527
537
  pi.on("session_shutdown", async (event: SessionShutdownEvent, _ctx: ExtensionContext) => {
528
538
  if (event.reason === "reload") {
539
+ // [§1.4 (a) reload 分支显式作废句柄] reload 有意跳过全部破坏性清理(下方 D1 分支
540
+ // 说明——在途 run 交给 reload 后 adoption 接管),但 pi 会话替换会使旧 runner
541
+ // 失效(PS-30:assertActive 抛 stale after session replacement),core SubagentService
542
+ // 的 late-bound pi 读面在旧失效句柄上会一路命中 assertActive 抛错(在途 run 的
543
+ // 轮终收尾正落该窗口)。此处只作废「句柄可用性判定」,不动在途 run 纳管:
544
+ // 作废后读面降级 null(轮终收尾经有界等待挂到新 initSession 注入),新 session_start
545
+ // 注入新代际句柄后自然恢复。方法本身不抛(字段写 + 唤醒等待者)。
546
+ getSubagentService()?.invalidatePiBinding("session replacement (reload)");
529
547
  // b 动作保留(理由见上方 D1 分支说明)。
530
548
  inflightReporter.detachSession();
531
549
 
@@ -35,7 +35,7 @@ import {
35
35
  boundedPrettySerialize,
36
36
  getBoundNotifyLedger,
37
37
  isTerminalDoneReason,
38
- RUN_EVENT_JOURNAL_SUFFIX,
38
+ RUN_EVENTS_SUFFIX,
39
39
  type RunOutcome,
40
40
  } from "@zhushanwen/subagent-core";
41
41
  import type { WorkflowRun } from "@zhushanwen/subagent-core";
@@ -58,12 +58,13 @@ const MAX_RESULT_SUMMARY_LENGTH = 500;
58
58
  const RECORD_FAIL_CONTENT_PREVIEW_LENGTH = 200;
59
59
 
60
60
  /**
61
- * [D7] 事件 journal 文件名(run store 旁,artifactsDir 内):`<runId>.events.jsonl`
62
- * (runId 即 generateRunId 的 wf- 前缀产物,渲染名 = 设计 D5 的 wf-<id>.events.jsonl;
63
- * 后缀经 core barrel 单源 RUN_EVENT_JOURNAL_SUFFIX,core 命名变更时编译期同步)。
61
+ * [D7] 事件 journal 文件名(run store 旁,artifactsDir 内):`<runId>.record.jsonl`
62
+ * (runId 即 generateRunId 的 wf- 前缀产物;后缀随 core RUN_EVENTS_SUFFIX 改名,
63
+ * 历史 .events.jsonl = 旧写入方实体,设计 D5 的渲染名 wf-<id>.events.jsonl 即该
64
+ * 旧形态;实值经 core barrel 单源 RUN_EVENTS_SUFFIX,core 命名变更时编译期同步)。
64
65
  */
65
66
  function runEventsJournalPath(artifactsDir: string, runId: string): string {
66
- return join(artifactsDir, `${runId}${RUN_EVENT_JOURNAL_SUFFIX}`);
67
+ return join(artifactsDir, `${runId}${RUN_EVENTS_SUFFIX}`);
67
68
  }
68
69
 
69
70
  /**
@@ -138,7 +139,7 @@ interface WorkflowNotifyDetails { // oe-exempt:20260929:framework:workflow/recor
138
139
  */
139
140
  artifactsDir?: string;
140
141
  /**
141
- * [D7] 事件 journal 指针(`<artifactsDir>/wf-<runId>.events.jsonl`)——终局
142
+ * [D7] 事件 journal 指针(`<artifactsDir>/wf-<runId>.record.jsonl`)——终局
142
143
  * 证据的入口载荷(D5-3 诊断引用落账的消费面)。
143
144
  */
144
145
  eventsJournalPath?: string;
@@ -1,227 +0,0 @@
1
- /**
2
- * workflow tool resume action 契约(workflow-run-resume-revision U3 / D14)。
3
- *
4
- * 覆盖面:
5
- * - D14 args 校验(场景 24 单元层):传入 args 与 run-created 帧 argsSummary 的
6
- * 逐字段深度比对(排除 _runId)——不一致明确拒绝且差异字段可见;一致幂等通过;
7
- * 不传默认沿用历史(D14 层不拦)。场景级(真链路)用例由 u4b 落地。
8
- * - argsSummary 截断形态的保守拒绝(全文 args 不进 record,截断摘要不可比对)。
9
- * - record 流无 run-created 帧 → D14 层透传(资格判据归 core resumeRun 权威文案)。
10
- * - resumeRun 接线:budgetTimeMs(time 形参透传)/ host / runId;成功文案与 details。
11
- * - diffResumeArgs 纯逻辑(差异三形态 + 嵌套 + 数组 + _runId 排除)。
12
- *
13
- * mock 策略:resume-run 深路径 stub(resumeRun 为 vi.fn——不起锁/IO,只测入口
14
- * 面与校验层);record 流用 mkdtemp 真文件(D14 读取面读真实路径)。范式对齐
15
- * tool-workflow.test.ts(lifecycle 深路径 mock + captureTool 注册层黑盒)。
16
- */
17
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
18
- import { tmpdir } from "node:os";
19
- import { join } from "node:path";
20
-
21
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
22
-
23
- // 被 mock 的模块——import 路径与被测源文件的等值实例(vi.mock hoist 后 barrel
24
- // re-export 与深路径指向同一 mock 实例,对齐 tool-workflow.test.ts 先例)
25
- import { resumeRun } from "@zhushanwen/subagent-core/orchestration/resume-run.ts";
26
-
27
- import { actionResume, diffResumeArgs } from "../tool-workflow.ts";
28
-
29
- /** 桩化 resume-run——resumeRun 为 vi.fn(不起锁/record IO,只测入口校验与接线面)。 */
30
- vi.mock("@zhushanwen/subagent-core/orchestration/resume-run.ts", () => ({
31
- resumeRun: vi.fn(),
32
- }));
33
-
34
- // ── fixture ──────────────────────────────────────────────────
35
-
36
- let tmpDir: string;
37
-
38
- beforeEach(() => {
39
- vi.mocked(resumeRun).mockReset();
40
- vi.mocked(resumeRun).mockResolvedValue("wf-test");
41
- tmpDir = mkdtempSync(join(tmpdir(), "u3-resume-"));
42
- });
43
-
44
- afterEach(() => {
45
- rmSync(tmpDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
46
- vi.restoreAllMocks();
47
- });
48
-
49
- /** 写 record 流(每行一个 JSON 对象)并返回路径——D14 读取面的真实文件。 */
50
- function writeRecordStream(lines: unknown[]): string {
51
- const path = join(tmpDir, "wf-test.record.jsonl");
52
- writeFileSync(path, lines.map((l) => JSON.stringify(l)).join("\n") + "\n", "utf8");
53
- return path;
54
- }
55
-
56
- /** run-created 帧(args 含 _runId——对齐 lifecycle runWorkflow 注入后落账形态)。 */
57
- function runCreatedFrame(argsSummary: string) {
58
- return {
59
- type: "run-created",
60
- seq: 1,
61
- ts: 1_700_000_000_000,
62
- runId: "wf-test",
63
- workflowName: "demo",
64
- argsSummary,
65
- scriptSource: "// demo",
66
- };
67
- }
68
-
69
- /** 最小 deps stub(actionResume 只触 store.stateFilePath 与 resumeRun(deps))。 */
70
- function makeDeps(stateFilePath: string): unknown {
71
- return {
72
- runs: new Map(),
73
- store: { stateFilePath: () => stateFilePath },
74
- registry: { get: vi.fn(), getPath: vi.fn(), loadAll: vi.fn() },
75
- };
76
- }
77
-
78
- // ── diffResumeArgs 纯逻辑 ────────────────────────────────────
79
-
80
- describe("diffResumeArgs(D14 深度比对)", () => {
81
- it("值不等 → 差异字段可见(场景 24 前半:{a:1} 历史以 {a:2} 发)", () => {
82
- expect(diffResumeArgs({ a: 2 }, { a: 1, _runId: "wf-test" })).toEqual([
83
- "args.a: resume 2 vs original 1",
84
- ]);
85
- });
86
-
87
- it("一致 → 空(场景 24 后半:{a:1} 幂等通过)", () => {
88
- expect(diffResumeArgs({ a: 1 }, { a: 1, _runId: "wf-test" })).toEqual([]);
89
- });
90
-
91
- it("_runId 双侧排除(incoming 侧携带同样不比)", () => {
92
- expect(diffResumeArgs({ a: 1, _runId: "other" }, { a: 1, _runId: "wf-test" })).toEqual([]);
93
- });
94
-
95
- it("仅 resume 侧有 → unexpected 形态", () => {
96
- expect(diffResumeArgs({ a: 1, b: 2 }, { a: 1 })).toEqual([
97
- "args.b: resume args only (not in original run)",
98
- ]);
99
- });
100
-
101
- it("仅原 run 侧有 → missing 形态", () => {
102
- expect(diffResumeArgs({ a: 1 }, { a: 1, b: "x" })).toEqual([
103
- "args.b: original run only (missing from resume args)",
104
- ]);
105
- });
106
-
107
- it("嵌套 plain object 递归(路径 a.b 形态)", () => {
108
- expect(
109
- diffResumeArgs({ cfg: { deep: 1, same: "s" } }, { cfg: { deep: 2, same: "s" } }),
110
- ).toEqual(["args.cfg.deep: resume 1 vs original 2"]);
111
- });
112
-
113
- it("数组按值整体比(不递归元素)", () => {
114
- expect(diffResumeArgs({ items: [1, 2] }, { items: [1, 3] })).toEqual([
115
- 'args.items: resume [1,2] vs original [1,3]',
116
- ]);
117
- expect(diffResumeArgs({ items: [1, 2] }, { items: [1, 2] })).toEqual([]);
118
- });
119
- });
120
-
121
- // ── actionResume:入口校验与接线 ─────────────────────────────
122
-
123
- describe("actionResume", () => {
124
- it("runId 缺失 → throw 用法指引(不触 resumeRun)", async () => {
125
- await expect(
126
- actionResume({ action: "resume" } as never, makeDeps("/nonexistent") as never),
127
- ).rejects.toThrow(/resume requires 'runId'/);
128
- expect(vi.mocked(resumeRun)).not.toHaveBeenCalled();
129
- });
130
-
131
- it("args 与历史不一致 → 拒绝且差异字段可见,不触 resumeRun(场景 24 前半)", async () => {
132
- const recordPath = writeRecordStream([runCreatedFrame('{"a":1,"_runId":"wf-test"}')]);
133
- const err = await actionResume(
134
- { action: "resume", runId: "wf-test", args: { a: 2 } } as never,
135
- makeDeps(recordPath) as never,
136
- ).catch((e: unknown) => e as Error);
137
- expect(err).toBeInstanceOf(Error);
138
- expect(err.message).toContain("differ from the original run");
139
- expect(err.message).toContain("args.a: resume 2 vs original 1");
140
- expect(err.message).toContain("start a new run");
141
- expect(vi.mocked(resumeRun)).not.toHaveBeenCalled();
142
- });
143
-
144
- it("args 与历史一致 → 幂等通过(场景 24 后半),resumeRun 接线生效", async () => {
145
- const recordPath = writeRecordStream([runCreatedFrame('{"a":1,"_runId":"wf-test"}')]);
146
- const result = (await actionResume(
147
- { action: "resume", runId: "wf-test", args: { a: 1 } } as never,
148
- makeDeps(recordPath) as never,
149
- )) as { content: Array<{ text: string }>; details: Record<string, unknown> };
150
- expect(vi.mocked(resumeRun)).toHaveBeenCalledTimes(1);
151
- const [calledRunId, calledDeps, options] = vi.mocked(resumeRun).mock.calls[0]!;
152
- expect(calledRunId).toBe("wf-test");
153
- expect(calledDeps).toBeDefined();
154
- expect((options as { budgetTimeMs?: number }).budgetTimeMs).toBeUndefined();
155
- expect(typeof (options as { host?: string }).host).toBe("string");
156
- expect(result.content[0]!.text).toContain("Resuming workflow run wf-test");
157
- expect(result.details).toMatchObject({ action: "resume", runId: "wf-test", status: "running" });
158
- });
159
-
160
- it("args 缺省 → 默认沿用历史(D14 层不拦,不读 record)", async () => {
161
- await actionResume(
162
- { action: "resume", runId: "wf-test" } as never,
163
- makeDeps(join(tmpDir, "missing.record.jsonl")) as never,
164
- );
165
- expect(vi.mocked(resumeRun)).toHaveBeenCalledTimes(1);
166
- });
167
-
168
- it("record 流无 run-created 帧 → D14 层透传(资格判据归 resumeRun 权威文案)", async () => {
169
- const recordPath = writeRecordStream([
170
- { type: "phase-started", seq: 1, ts: 1, phase: "p1" },
171
- ]);
172
- await actionResume(
173
- { action: "resume", runId: "wf-test", args: { a: 1 } } as never,
174
- makeDeps(recordPath) as never,
175
- );
176
- expect(vi.mocked(resumeRun)).toHaveBeenCalledTimes(1);
177
- });
178
-
179
- it("argsSummary 截断 → 保守拒绝(无法逐字段比对;引导免 args resume 或开新 run)", async () => {
180
- const long = "x".repeat(300);
181
- const recordPath = writeRecordStream([runCreatedFrame(`{"payload":"${long}","_runId":"wf-test"}…`)]);
182
- const err = await actionResume(
183
- { action: "resume", runId: "wf-test", args: { payload: long } } as never,
184
- makeDeps(recordPath) as never,
185
- ).catch((e: unknown) => e as Error);
186
- expect(err).toBeInstanceOf(Error);
187
- expect(err.message).toContain("exceed the record's args summary limit");
188
- expect(vi.mocked(resumeRun)).not.toHaveBeenCalled();
189
- });
190
-
191
- it("argsSummary 被篡改为非法 JSON → 拒绝含恢复指引(不含裸 SyntaxError;U21 与 malformed 分支同构)", async () => {
192
- // 字段篡改形态:未截断摘要字符串是非法 JSON(JSON.stringify 只把它当字符串值
193
- // 序列化,读取面 parse 必炸)——catch 后抛结构化拒绝文案,原生 SyntaxError
194
- // 不透出。
195
- const recordPath = writeRecordStream([runCreatedFrame("{broken json")]);
196
- const err = await actionResume(
197
- { action: "resume", runId: "wf-test", args: { a: 1 } } as never,
198
- makeDeps(recordPath) as never,
199
- ).catch((e: unknown) => e as Error);
200
- expect(err).toBeInstanceOf(Error);
201
- expect(err.message).toContain("Resume rejected: original args of run wf-test are malformed");
202
- expect(err.message).toContain("Recovery: inspect the record file");
203
- expect(err.message).toContain("start a new run");
204
- expect(err.message).not.toContain("SyntaxError");
205
- expect(vi.mocked(resumeRun)).not.toHaveBeenCalled();
206
- });
207
-
208
- it("time 形参透传 budgetTimeMs(u2 偏差②:预算经 D14 同款通道传入)", async () => {
209
- const recordPath = writeRecordStream([runCreatedFrame('{"a":1}')]);
210
- await actionResume(
211
- { action: "resume", runId: "wf-test", args: { a: 1 }, time: 120_000 } as never,
212
- makeDeps(recordPath) as never,
213
- );
214
- const options = vi.mocked(resumeRun).mock.calls[0]![2] as { budgetTimeMs?: number };
215
- expect(options.budgetTimeMs).toBe(120_000);
216
- });
217
-
218
- it("time 负值 → 入口护栏拒绝(与 run action 同源 assertEntryTimeBudget)", async () => {
219
- await expect(
220
- actionResume(
221
- { action: "resume", runId: "wf-test", time: -1 } as never,
222
- makeDeps("/nonexistent") as never,
223
- ),
224
- ).rejects.toThrow(/time/);
225
- expect(vi.mocked(resumeRun)).not.toHaveBeenCalled();
226
- });
227
- });
@@ -1,81 +0,0 @@
1
- /**
2
- * GUI 协议映射辅助函数 —— run/subagent 状态字符串 → 协议 TreeItem 状态 + 图标。
3
- *
4
- * 协议包 @zhushanwen/extension-protocol 的 list-tree 组件用 TreeItem.status(三态)
5
- * + TreeItem.icon 表达运行态。本模块把 workflow/subagent 领域的丰富状态字符串收口
6
- * 到这两个枚举,供 helpers.ts / tool-workflow.ts / subagent-actions.ts 复用。
7
- *
8
- * 参考:@zhushanwen/extension-protocol GuiComponentProps['list-tree']。
9
- */
10
-
11
- import type { GuiContext, TreeItem, TreeItemIcon } from "@zhushanwen/extension-protocol";
12
-
13
- /**
14
- * 从 Pi ExtensionContext 构造协议 GuiContext 的最小子集。
15
- *
16
- * Pi SDK 的 ExtensionContext 在结构上满足协议 GuiContext(有 mode/hasUI/ui),
17
- * 但 ui.custom 的泛型签名与协议 GuiContext.ui.custom 不兼容(前者复杂泛型,后者
18
- * 简化签名),直接 `as GuiContext` 会触发 TS 结构兼容错误(ui.custom 参数逆变)。
19
- * 此 helper 显式提取 mode/hasUI,构造最小 GuiContext,规避 ui.custom 签名冲突。
20
- *
21
- * 与 ask-user extension 的 runRpcInteraction 同构(见 ask-user/src/index.ts)。
22
- */
23
- export function toGuiCtx(ctx: { mode: GuiContext["mode"]; hasUI: boolean } | undefined): GuiContext | undefined {
24
- if (!ctx) return undefined;
25
- return { mode: ctx.mode, hasUI: ctx.hasUI };
26
- }
27
-
28
- /** TreeItem.status 枚举(协议三态)。 */
29
- type TreeStatus = NonNullable<TreeItem["status"]>;
30
-
31
- /**
32
- * 状态字符串是否为失败态(mapRunStatus/mapRunIcon 共享谓词——两映射仅返回值形态不同,
33
- * 关键词表必须同步演化,抽单点防双写漂移)。入参须已 toLowerCase。
34
- */
35
- function isFailedStatus(s: string): boolean {
36
- return (
37
- s.includes("failed") ||
38
- s.includes("abort") ||
39
- s.includes("cancel") ||
40
- s.includes("crash") ||
41
- s.includes("error") ||
42
- s.includes("budget") ||
43
- s.includes("time_limited")
44
- );
45
- }
46
-
47
- /**
48
- * 把 workflow/subagent 状态字符串映射到 list-tree 的三态 status。
49
- *
50
- * 输入可能是纯 RunStatus(running/done)、RunStatus+reason 组合
51
- * (如 "done (failed)"),或 subagent status(running/idle/legacy done/failed/
52
- * cancelled/crashed)。
53
- *
54
- * 映射规则:
55
- * - running → running
56
- * - failed / aborted / error / crashed / cancelled / budget_limited / time_limited → failed
57
- * - 其他(done / completed / success / pending / idle)→ done
58
- *
59
- * [U8 两态] subagent 的 idle(无任务在飞可续聊)在协议三态里落 done(空闲 = 无
60
- * 进行中工作,check 图标);「为什么停」不进树形状态(协议无该维度)。
61
- */
62
- export function mapRunStatus(status: string): TreeStatus {
63
- const s = status.toLowerCase();
64
- if (s.includes("running")) return "running";
65
- if (isFailedStatus(s)) return "failed";
66
- return "done";
67
- }
68
-
69
- /**
70
- * 把状态字符串映射到 TreeItem.icon。
71
- *
72
- * running → circle(进行中)
73
- * failed/abort/cancel/crash → cross
74
- * 其他(done) → check
75
- */
76
- export function mapRunIcon(status: string): TreeItemIcon {
77
- const s = status.toLowerCase();
78
- if (s.includes("running")) return "circle";
79
- if (isFailedStatus(s)) return "cross";
80
- return "check";
81
- }