@zhushanwen/pi-subagent-workflow 9.0.1 → 9.1.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/package.json +10 -10
- package/skills/subagent-ext-config/SKILL.md +9 -11
- package/src/host/__tests__/pi-host.test.ts +0 -1
- package/src/index.ts +7 -7
- package/src/injectors/subagent-list-injector.ts +1 -1
- package/src/interface/command/__tests__/commands-open.test.ts +278 -0
- package/src/interface/{commands.ts → command/commands.ts} +15 -7
- package/src/interface/{subagents.ts → command/subagents.ts} +4 -4
- package/src/interface/format/__tests__/display-state.test.ts +146 -0
- package/src/interface/format/display-state.ts +112 -0
- package/src/interface/{format.ts → format/format.ts} +20 -14
- package/src/interface/{id-preview.ts → format/id-preview.ts} +1 -1
- package/src/interface/{bg-notify-render.ts → gui/bg-notify-render.ts} +60 -35
- package/src/interface/gui/gui-mappers.ts +74 -0
- package/src/interface/{subagent-actions.ts → gui/subagent-actions.ts} +3 -3
- package/src/interface/tool/__tests__/subagent-tool-actions.test.ts +238 -0
- package/src/interface/{__tests__ → tool/__tests__}/subagent-tool-path-guard.test.ts +6 -5
- package/src/interface/{__tests__ → tool/__tests__}/tool-subagents.test.ts +4 -4
- package/src/interface/tool/__tests__/tool-workflow-resume.test.ts +161 -0
- package/src/interface/{__tests__ → tool/__tests__}/tool-workflow.test.ts +33 -9
- package/src/interface/{subagent-tool-schema.ts → tool/subagent-tool-schema.ts} +1 -1
- package/src/interface/{subagent-tool.ts → tool/subagent-tool.ts} +4 -4
- package/src/interface/{tool-render.ts → tool/tool-render.ts} +3 -3
- package/src/interface/{tool-shared.ts → tool/tool-shared.ts} +1 -1
- package/src/interface/{tool-subagents.ts → tool/tool-subagents.ts} +12 -11
- package/src/interface/{tool-workflow-script.ts → tool/tool-workflow-script.ts} +1 -1
- package/src/interface/{tool-workflow.ts → tool/tool-workflow.ts} +20 -165
- package/src/interface/tui/__tests__/list-view.test.ts +188 -0
- package/src/interface/{list-component.ts → tui/list-component.ts} +2 -2
- package/src/interface/{list-shared.ts → tui/list-shared.ts} +1 -1
- package/src/interface/{list-view.ts → tui/list-view.ts} +2 -2
- package/src/interface/{tui-kit.ts → tui/tui-kit.ts} +1 -1
- package/src/interface/{views → tui/views}/WorkflowsView.ts +10 -6
- package/src/interface/tui/views/__tests__/WorkflowsView.driver.test.ts +595 -0
- package/src/interface/{views → tui/views}/__tests__/WorkflowsView.test.ts +10 -3
- package/src/interface/tui/views/__tests__/detail-content.test.ts +195 -0
- package/src/interface/{views → tui/views}/detail-content.ts +8 -3
- package/src/interface/{views → tui/views}/view-constants.ts +2 -2
- package/src/jsonl-run-store.ts +133 -81
- package/src/session-lifecycle.ts +2 -1
- package/src/workflow-events.ts +21 -3
- package/src/workflow-notify.ts +7 -6
- package/src/interface/__tests__/tool-workflow-resume.test.ts +0 -227
- package/src/interface/gui-mappers.ts +0 -81
- /package/src/interface/{__tests__ → command/__tests__}/commands-resume.test.ts +0 -0
- /package/src/interface/{command-actions.ts → command/command-actions.ts} +0 -0
- /package/src/interface/{__tests__ → tool/__tests__}/capture-tool.ts +0 -0
- /package/src/interface/{__tests__ → tool/__tests__}/tool-prompt-contract.test.ts +0 -0
- /package/src/interface/{__tests__ → tool/__tests__}/tool-render.test.ts +0 -0
- /package/src/interface/{__tests__ → tool/__tests__}/tool-workflow-script.test.ts +0 -0
- /package/src/interface/{reentry-guard.ts → tool/reentry-guard.ts} +0 -0
- /package/src/interface/{tool-result.ts → tool/tool-result.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhushanwen/pi-subagent-workflow",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
|
|
@@ -58,22 +58,22 @@
|
|
|
58
58
|
]
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@zhushanwen/extension-protocol": "0.
|
|
62
|
-
"@zhushanwen/session-delivery": "0.
|
|
61
|
+
"@zhushanwen/extension-protocol": "0.17.0",
|
|
62
|
+
"@zhushanwen/session-delivery": "1.0.0",
|
|
63
63
|
"@zhushanwen/pi-ext-guards": "0.4.2",
|
|
64
64
|
"@zhushanwen/pi-extension-logger": "0.6.1",
|
|
65
|
-
"@zhushanwen/pi-notify-ledger-host": "0.2.
|
|
66
|
-
"@zhushanwen/subagent-core": "
|
|
67
|
-
"@zhushanwen/
|
|
68
|
-
"@zhushanwen/
|
|
65
|
+
"@zhushanwen/pi-notify-ledger-host": "0.2.2",
|
|
66
|
+
"@zhushanwen/subagent-core": "1.1.0",
|
|
67
|
+
"@zhushanwen/pi-subagent-cli": "0.5.0",
|
|
68
|
+
"@zhushanwen/zcode-subagent-cli": "0.4.4"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"@earendil-works/pi-ai": "^0.84.4",
|
|
72
72
|
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
73
73
|
"@earendil-works/pi-tui": "^0.84.4",
|
|
74
74
|
"typebox": "*",
|
|
75
|
-
"@zhushanwen/pi-pending-notifications": "0.8.
|
|
76
|
-
"@zhushanwen/pi-structured-output": "
|
|
75
|
+
"@zhushanwen/pi-pending-notifications": "0.8.1",
|
|
76
|
+
"@zhushanwen/pi-structured-output": "6.0.0"
|
|
77
77
|
},
|
|
78
78
|
"peerDependenciesMeta": {
|
|
79
79
|
"@earendil-works/pi-coding-agent": {
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"@types/node": "^24.0.0",
|
|
88
88
|
"@vitest/coverage-v8": "^4.1.9",
|
|
89
89
|
"vitest": "^4.1.8",
|
|
90
|
-
"@zhushanwen/subagent-engine-sdk": "0.
|
|
90
|
+
"@zhushanwen/subagent-engine-sdk": "0.8.0"
|
|
91
91
|
},
|
|
92
92
|
"scripts": {
|
|
93
93
|
"typecheck": "npx tsc --noEmit",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: subagent-ext-config
|
|
3
|
-
description: "使用或排查 @zhushanwen/pi-subagent-workflow 的引擎路由与同步收集配置时加载。说明 config.json 三环境路径与动态推导方法、字段表(defaultEngine /
|
|
3
|
+
description: "使用或排查 @zhushanwen/pi-subagent-workflow 的引擎路由与同步收集配置时加载。说明 config.json 三环境路径与动态推导方法、字段表(defaultEngine / maxConcurrent / collectSync 三键)、三层路由优先级、生效时机(新 session 生效)、probe 缓存语义、collectSync 预算(perItemChars/totalChars)热读时机、验证步骤与常见错误。触发词:subagent 引擎配置、切换 subagent 引擎、defaultEngine、zcode 派发、subagent 配置在哪、engine_not_found、engine_probe_failed、engine_config_unreadable、collectSync、sync collect、collect 配置、subagent-ext-config。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# subagent-workflow 引擎路由配置指南
|
|
@@ -21,14 +21,13 @@ config.json 位于 pi agent 目录下的 `subagents/config.json`,随环境不
|
|
|
21
21
|
|
|
22
22
|
**动态推导(推荐)**:agentDir 由 pi 核心 `getAgentDir()` 决定(读 `PI_CODING_AGENT_DIR`,默认 `~/.pi/agent`);taiji 通过 `TAIJI_AGENT_DATA_DIR` 隔离数据目录。排查时先查这两个 env 变量组合出实际路径(`<agentDir>/subagents/config.json`),不要假设单一环境——写错环境的配置文件改了也不生效。
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
文件不存在(ENOENT,合法缺省)/ 字段缺失 → 回默认配置;**文件存在但读不出来(坏 JSON / 权限)** → 派发前显式拒绝 `engine_config_unreadable`(缺省引擎是未知量,不按内置缺省 pi 执行),修复或删除该文件后重试。旧版 `categories` / `fallback` / `yoloByDefault` 等字段读取时忽略(模型解析已退化为「主 agent model 优先」)。
|
|
25
25
|
|
|
26
26
|
## 字段表(sanitize 语义)
|
|
27
27
|
|
|
28
28
|
| 字段 | 类型 | 默认 | 说明 |
|
|
29
29
|
|------|------|------|------|
|
|
30
|
-
| `defaultEngine` | string | 缺省由路由层落 `'pi'` | 全局默认引擎。合法值 `'pi'` / `'zcode'`。坏值(非字符串/空串)**静默忽略**回缺省 pi,不报错——排查「改了 defaultEngine 却还在用 pi」时先检查 JSON
|
|
31
|
-
| `engineRouting.strict` | boolean | `false` | `true` = 一切 probe 失败直接报错、不做兜底回退。仅认 strict 布尔键,其余键忽略 |
|
|
30
|
+
| `defaultEngine` | string | 缺省由路由层落 `'pi'` | 全局默认引擎。合法值 `'pi'` / `'zcode'`。坏值(非字符串/空串)**静默忽略**回缺省 pi,不报错——排查「改了 defaultEngine 却还在用 pi」时先检查 JSON 值合法性。指向未安装引擎时不回落:派发期报 `engine_not_found` |
|
|
32
31
|
| `maxConcurrent` | number | `6` | subagent 并发池大小。正整数,非正整数/非整数回默认 |
|
|
33
32
|
|
|
34
33
|
示例(最小可用):
|
|
@@ -37,14 +36,13 @@ config.json 位于 pi agent 目录下的 `subagents/config.json`,随环境不
|
|
|
37
36
|
{
|
|
38
37
|
"version": 1,
|
|
39
38
|
"defaultEngine": "zcode",
|
|
40
|
-
"engineRouting": { "strict": false },
|
|
41
39
|
"maxConcurrent": 6
|
|
42
40
|
}
|
|
43
41
|
```
|
|
44
42
|
|
|
45
43
|
## collectSync 节(同步收集)
|
|
46
44
|
|
|
47
|
-
`collectSync` 控制 subagent 同步收集(`collect:"sync"` 批通知)的默认模式与结果预算。整节缺失 = 不落键,消费方兜底默认(与 defaultEngine
|
|
45
|
+
`collectSync` 控制 subagent 同步收集(`collect:"sync"` 批通知)的默认模式与结果预算。整节缺失 = 不落键,消费方兜底默认(与 defaultEngine 同风格);逐字段 sanitize——坏字段回该字段默认、好字段透传(部分覆盖合法);非对象值(字符串/null 等)整节回默认。坏值不炸启动(与 maxConcurrent 同判)。
|
|
48
46
|
|
|
49
47
|
| 字段 | 类型 | 默认 | 说明 |
|
|
50
48
|
|------|------|------|------|
|
|
@@ -72,13 +70,12 @@ config.json 位于 pi agent 目录下的 `subagents/config.json`,随环境不
|
|
|
72
70
|
| 2 | agent `.md` frontmatter 的 `engine` 字段 |
|
|
73
71
|
| 3 | config.json 的 `defaultEngine` |
|
|
74
72
|
|
|
75
|
-
|
|
73
|
+
三层(调用参数 / agent frontmatter / 全局缺省)任一指定了引擎,运行期就按该引擎执行——**不可用即结构化失败,不换目标**:probe 失败一律报 `engine_probe_failed`(含逐项 check 摘要与恢复指引),id 未注册报 `engine_not_found`;要换引擎只能由调用方显式改传 `engine` 参数。三层全缺时落内置缺省 `pi`(免探)。
|
|
76
74
|
|
|
77
75
|
## 生效时机与 probe 缓存
|
|
78
76
|
|
|
79
77
|
- **配置读取**:pi 子进程启动 + 每次 `session_start` 各读一次,session 内不重读。改配置 → 新建 session 生效。
|
|
80
|
-
- **probe 缓存**:引擎探针(zcode CLI
|
|
81
|
-
- **engineFallback 留痕条件**:兜底回 pi(record 带 `engineFallback` 标记)只在探针**未缓存**时触发。同一 session 内先 probe 成功后 CLI 损坏,不会再触发兜底。要复现/验证 fallback 场景,必须新建 session 重置缓存。
|
|
78
|
+
- **probe 缓存**:引擎探针(zcode CLI 存在性/版本检查)成功或失败均缓存直返,**进程存活期内不重探**——CLI 刚装好 / 刚损坏都要新建 session 重置缓存才会重新探测。
|
|
82
79
|
|
|
83
80
|
## 验证步骤
|
|
84
81
|
|
|
@@ -93,8 +90,9 @@ config.json 位于 pi agent 目录下的 `subagents/config.json`,随环境不
|
|
|
93
90
|
|
|
94
91
|
| 症状 | 原因与处置 |
|
|
95
92
|
|------|------|
|
|
96
|
-
| `engine_not_found` | engine id
|
|
93
|
+
| `engine_not_found` | engine id 未注册(含 `defaultEngine` 指向已卸载引擎)。检查拼写,合法值仅 `pi` / `zcode`;不回落其它引擎 |
|
|
94
|
+
| `engine_probe_failed` | 引擎探针失败(CLI 缺失/版本不符)。引擎路由不换目标——修好引擎后重试,或由调用方显式改传 `engine` 参数 |
|
|
95
|
+
| `engine_config_unreadable` | 全局 config.json 存在但读不出来(坏 JSON / 权限)——修复或删除该文件后重试(不按内置缺省 pi 执行) |
|
|
97
96
|
| zcode 任务传 `conversation` / `fork` / `worktree` 被预检拒绝 | 这些是 pi 专属能力,zcode 不支持。改用 `engine: pi` 或不传该参数重试(预检在 record 创建前同步拒绝,可立即换引擎) |
|
|
98
97
|
| 改了 `defaultEngine` 没生效 | 两种可能:① 当前 session 不重读配置——新建 session;② 值非法被静默忽略回 pi——核对 JSON 值 |
|
|
99
|
-
| 期望 fallback 回 pi 却报错 | 显式指定引擎(工具参数/frontmatter)属守卫命中,probe 失败不兜底直接报错;只有走 `defaultEngine` 的任务才兜底 |
|
|
100
98
|
| probe 结果与 CLI 实际状态不符 | 探针进程存活期内缓存——CLI 刚装好/刚损坏,需新建 session 重置缓存 |
|
|
@@ -558,7 +558,6 @@ describe("createPiNotifyDomainPorts.createDelivery(透传 session-delivery)"
|
|
|
558
558
|
const fakePort = {
|
|
559
559
|
supportedPayloads: ["text"] as const,
|
|
560
560
|
isIdle: () => true,
|
|
561
|
-
hasPendingMessages: () => false,
|
|
562
561
|
send: () => undefined,
|
|
563
562
|
};
|
|
564
563
|
const fakeConfig = { intent: "after-run" as const };
|
package/src/index.ts
CHANGED
|
@@ -47,14 +47,14 @@ import { setupEngineAwarenessInjector } from "./injectors/engine-awareness.ts";
|
|
|
47
47
|
import { setupModelListInjector } from "./injectors/model-list-injector.ts";
|
|
48
48
|
import { setupSubagentListInjector } from "./injectors/subagent-list-injector.ts";
|
|
49
49
|
import { setupWorkflowListInjector } from "./injectors/workflow-list-injector.ts";
|
|
50
|
-
import { renderBgNotifyMessage } from "./interface/bg-notify-render.ts";
|
|
51
|
-
import { registerWorkflowsCommand } from "./interface/commands.ts";
|
|
52
|
-
import { registerSubagentTool } from "./interface/subagent-tool.ts";
|
|
50
|
+
import { renderBgNotifyMessage } from "./interface/gui/bg-notify-render.ts";
|
|
51
|
+
import { registerWorkflowsCommand } from "./interface/command/commands.ts";
|
|
52
|
+
import { registerSubagentTool } from "./interface/tool/subagent-tool.ts";
|
|
53
53
|
// ═══ interface/ 层(tools/commands/tui 合并) ═══
|
|
54
|
-
import { registerSubagentsCommand } from "./interface/subagents.ts";
|
|
55
|
-
import { registerSubagentsTool } from "./interface/tool-subagents.ts";
|
|
56
|
-
import { registerWorkflowTool } from "./interface/tool-workflow.ts";
|
|
57
|
-
import { registerWorkflowScriptTool } from "./interface/tool-workflow-script.ts";
|
|
54
|
+
import { registerSubagentsCommand } from "./interface/command/subagents.ts";
|
|
55
|
+
import { registerSubagentsTool } from "./interface/tool/tool-subagents.ts";
|
|
56
|
+
import { registerWorkflowTool } from "./interface/tool/tool-workflow.ts";
|
|
57
|
+
import { registerWorkflowScriptTool } from "./interface/tool/tool-workflow-script.ts";
|
|
58
58
|
// ═══ workflow 域事件族装配 seam(7 个 pi.on handler + makeDeps/lazyDeps;
|
|
59
59
|
// 与 session-lifecycle.ts 同构,D2 原样搬移 + lazyDeps 样板收敛) ═══
|
|
60
60
|
import { setupWorkflowDomain } from "./workflow-events.ts";
|
|
@@ -36,7 +36,7 @@ import { createResourceListInjector } from "./resource-list-injector.ts";
|
|
|
36
36
|
/**
|
|
37
37
|
* pi 版注入引导文案(C5① guide 参数化——core 渲染函数不内嵌平台文案,宿主注入)。
|
|
38
38
|
*
|
|
39
|
-
* 2026-08 C5 重写依据(pi 现参数面,src/interface/subagent-tool-schema.ts):
|
|
39
|
+
* 2026-08 C5 重写依据(pi 现参数面,src/interface/tool/subagent-tool-schema.ts):
|
|
40
40
|
* 旧句「pass systemPrompt alongside the agent name to create a dynamic agent」
|
|
41
41
|
* 指向的 systemPrompt 参数已不在 subagent tool schema——现 agent 参数 = .md 绝对
|
|
42
42
|
* 路径(<location>),缺省落 general-purpose(继承主 agent 模型与项目上下文),
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /workflows 命令的打开通道契约(与 commands-resume.test.ts 的 resume 通道互补)。
|
|
3
|
+
*
|
|
4
|
+
* 覆盖面:
|
|
5
|
+
* - getArgumentCompletions:一级 lifecycle 动词(前缀过滤)/ 二级 runId 补全
|
|
6
|
+
* (数据源空 → null、getRuns 抛错 → null + warn 留痕)/ 非动词前缀 → null
|
|
7
|
+
* - handler:print/json 模式降级提示;runId 精确匹配 / 前缀唯一匹配 / 前缀歧义 /
|
|
8
|
+
* 未命中;无参 0 runs 提示 / 单 run 直开 / 多 run select(排序 + 取消不打开)
|
|
9
|
+
* - openView:ViewActions.abort 接线 + liveRecords 查询域接线(service 在场时)
|
|
10
|
+
*
|
|
11
|
+
* mock 策略:WorkflowsView 模块 stub(createWorkflowsView 为 vi.fn——openView 的
|
|
12
|
+
* 断言面是传参而非真实 TUI);resume-run 深路径 stub(对齐 commands-resume.test.ts);
|
|
13
|
+
* pi-extension-logger stub(catch 冷路径 warn 不落真实日志盘)。
|
|
14
|
+
*/
|
|
15
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
16
|
+
|
|
17
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { setSubagentService, GLOBAL_SLOT_KEYS } from "@zhushanwen/subagent-core";
|
|
19
|
+
import { noteRebuiltSettlement } from "@zhushanwen/subagent-core";
|
|
20
|
+
import type { WorkflowRun } from "@zhushanwen/subagent-core";
|
|
21
|
+
|
|
22
|
+
vi.mock("@zhushanwen/pi-extension-logger", () => ({
|
|
23
|
+
getLogger: () => ({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }),
|
|
24
|
+
}));
|
|
25
|
+
vi.mock("@zhushanwen/subagent-core/orchestration/resume-run.ts", () => ({
|
|
26
|
+
resumeRun: vi.fn(),
|
|
27
|
+
}));
|
|
28
|
+
vi.mock("../../tui/views/WorkflowsView.ts", () => ({
|
|
29
|
+
createWorkflowsView: vi.fn(() => Promise.resolve()),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
import { createWorkflowsView } from "../../tui/views/WorkflowsView.ts";
|
|
33
|
+
import { registerWorkflowsCommand } from "../commands.ts";
|
|
34
|
+
import { LIST_LIMIT } from "../../tui/list-shared.ts";
|
|
35
|
+
|
|
36
|
+
// ── fixture ──────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
const T0 = 1_700_000_000_000;
|
|
39
|
+
let runSeq = 0;
|
|
40
|
+
|
|
41
|
+
function makeRun(opts: { status?: "running" | "interrupted" | "done"; startedAt?: number; name?: string; runId?: string } = {}): WorkflowRun {
|
|
42
|
+
const runId = opts.runId ?? `wf-cmd-${runSeq++}`;
|
|
43
|
+
const status = opts.status ?? "running";
|
|
44
|
+
if (status === "done") {
|
|
45
|
+
noteRebuiltSettlement(runId, { outcome: "done", settledAt: Date.parse("2026-09-27T00:00:00.000Z") });
|
|
46
|
+
}
|
|
47
|
+
const meta: Record<string, unknown> = { startedAt: new Date(opts.startedAt ?? T0).toISOString() };
|
|
48
|
+
if (status === "interrupted") meta.interruptedAt = new Date(T0 + 5000).toISOString();
|
|
49
|
+
return {
|
|
50
|
+
runId,
|
|
51
|
+
spec: { scriptName: opts.name ?? `wf-${runId.slice(-1)}` },
|
|
52
|
+
meta,
|
|
53
|
+
state: { status: "running", budget: { usedTokens: 0, usedCost: 0 }, trace: { toArray: () => [] }, errorLogs: [] },
|
|
54
|
+
} as unknown as WorkflowRun;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface CompletionsView { // oe-exempt:20260930:test:test double shape for command capture (test infra)
|
|
58
|
+
getArgumentCompletions(prefix: string): Array<{ label: string; value: string }> | null;
|
|
59
|
+
handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface CtxHarness { // oe-exempt:20260930:test:test fixture harness shape (test infra)
|
|
63
|
+
ctx: ExtensionCommandContext;
|
|
64
|
+
notifies: Array<{ msg: string; level: string }>;
|
|
65
|
+
select: ReturnType<typeof vi.fn>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function captureCommand(getRuns: () => Map<string, WorkflowRun>, deps: Record<string, unknown> = {}): CompletionsView {
|
|
69
|
+
let captured!: CompletionsView;
|
|
70
|
+
const api = {
|
|
71
|
+
registerCommand: (_name: string, def: unknown) => {
|
|
72
|
+
captured = def as CompletionsView;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
registerWorkflowsCommand(api as never, getRuns, { store: { stateFilePath: (id: string) => `/state/${id}` }, ...deps } as never);
|
|
76
|
+
return captured;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function makeTuiCtx(): CtxHarness {
|
|
80
|
+
const notifies: Array<{ msg: string; level: string }> = [];
|
|
81
|
+
const select = vi.fn(() => Promise.resolve(undefined));
|
|
82
|
+
const ctx = {
|
|
83
|
+
mode: "tui",
|
|
84
|
+
ui: {
|
|
85
|
+
theme: { fg: (_t: string, text: string) => text, bold: (t: string) => t, bg: (_c: string, t: string) => t, underline: (t: string) => t },
|
|
86
|
+
notify: (msg: string, level: string) => notifies.push({ msg, level }),
|
|
87
|
+
select,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
return { ctx: ctx as unknown as ExtensionCommandContext, notifies, select };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 重置进程级 SubagentService 单例槽(测试清理,key 与生产 getServiceSlot 一致)。 */
|
|
94
|
+
function resetServiceSlot(): void {
|
|
95
|
+
const slot = Reflect.get(globalThis, Symbol.for(GLOBAL_SLOT_KEYS.service)) as
|
|
96
|
+
| { current: unknown }
|
|
97
|
+
| undefined;
|
|
98
|
+
if (slot) slot.current = null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
beforeEach(() => {
|
|
102
|
+
runSeq = 0;
|
|
103
|
+
vi.mocked(createWorkflowsView).mockClear();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
resetServiceSlot();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ── 补全面 ───────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
describe("/workflows getArgumentCompletions", () => {
|
|
113
|
+
it("一级:空前缀给 abort/resume 两动词(带尾随空格),前缀过滤生效", () => {
|
|
114
|
+
const cmd = captureCommand(() => new Map());
|
|
115
|
+
const all = cmd.getArgumentCompletions("");
|
|
116
|
+
expect(all).toHaveLength(2);
|
|
117
|
+
expect(all!.map((o) => o.value)).toEqual(["abort ", "resume "]);
|
|
118
|
+
expect(cmd.getArgumentCompletions("ab")).toEqual([
|
|
119
|
+
{ label: "abort", value: "abort ", description: "Abort a workflow run" },
|
|
120
|
+
]);
|
|
121
|
+
expect(cmd.getArgumentCompletions("wor")).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("二级:动词后补全 session runs(runId + 名字/状态描述),按排序输出", () => {
|
|
125
|
+
const older = makeRun({ startedAt: T0, name: "older" });
|
|
126
|
+
const newer = makeRun({ startedAt: T0 + 60_000, name: "newer" });
|
|
127
|
+
const runs = new Map<string, WorkflowRun>([
|
|
128
|
+
[older.runId, older],
|
|
129
|
+
[newer.runId, newer],
|
|
130
|
+
]);
|
|
131
|
+
const cmd = captureCommand(() => runs);
|
|
132
|
+
// 二级触发条件 = parts.length ≥ 2("abort x"——尾随单空格会被 trim 进一级动词匹配)
|
|
133
|
+
const items = cmd.getArgumentCompletions("abort x");
|
|
134
|
+
expect(items).toHaveLength(2);
|
|
135
|
+
expect(items![0]!.label).toBe(newer.runId); // 新的在前
|
|
136
|
+
expect(items![0]!.description).toContain("newer");
|
|
137
|
+
expect(items![0]!.description).toContain("[running]");
|
|
138
|
+
expect(cmd.getArgumentCompletions("resume x")).toHaveLength(2);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("二级:0 runs → null;getRuns 抛错 → null(补全缺失不致命)", () => {
|
|
142
|
+
const cmd = captureCommand(() => new Map());
|
|
143
|
+
expect(cmd.getArgumentCompletions("abort x")).toBeNull();
|
|
144
|
+
const boom = captureCommand(() => {
|
|
145
|
+
throw new Error("store unavailable");
|
|
146
|
+
});
|
|
147
|
+
expect(boom.getArgumentCompletions("resume x")).toBeNull();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("非动词首词 → null", () => {
|
|
151
|
+
const cmd = captureCommand(() => new Map());
|
|
152
|
+
expect(cmd.getArgumentCompletions("view x")).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ── handler:模式分流 + 打开通道 ─────────────────────────────
|
|
157
|
+
|
|
158
|
+
describe("/workflows handler 打开通道(TUI)", () => {
|
|
159
|
+
it("print 模式 → 交互模式降级提示,不打开面板", async () => {
|
|
160
|
+
const cmd = captureCommand(() => new Map());
|
|
161
|
+
const { ctx, notifies } = makeTuiCtx();
|
|
162
|
+
(ctx as { mode: string }).mode = "print";
|
|
163
|
+
await cmd.handler("", ctx);
|
|
164
|
+
expect(notifies).toEqual([{ msg: "/workflows requires interactive mode", level: "error" }]);
|
|
165
|
+
expect(vi.mocked(createWorkflowsView)).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("runId 精确匹配 → 打开该 run(theme/stateFilePath 原样透传;service 缺席 → liveRecords undefined)", async () => {
|
|
169
|
+
const run = makeRun();
|
|
170
|
+
const cmd = captureCommand(() => new Map([[run.runId, run]]));
|
|
171
|
+
const { ctx } = makeTuiCtx();
|
|
172
|
+
await cmd.handler(run.runId, ctx);
|
|
173
|
+
expect(vi.mocked(createWorkflowsView)).toHaveBeenCalledTimes(1);
|
|
174
|
+
const args = vi.mocked(createWorkflowsView).mock.calls[0] as unknown as [
|
|
175
|
+
WorkflowRun, unknown, ExtensionCommandContext, { abort: (id: string) => Promise<void> }, string, undefined,
|
|
176
|
+
];
|
|
177
|
+
expect(args[0]).toBe(run);
|
|
178
|
+
expect(args[2]).toBe(ctx);
|
|
179
|
+
expect(args[4]).toBe(`/state/${run.runId}`);
|
|
180
|
+
expect(args[5]).toBeUndefined();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("runId 前缀唯一匹配 → 打开;前缀歧义/未命中 → error notify", async () => {
|
|
184
|
+
const a = makeRun({ runId: "wf-alpha-1" });
|
|
185
|
+
const b = makeRun({ runId: "wf-beta-2" });
|
|
186
|
+
const cmd = captureCommand(() => new Map([[a.runId, a], [b.runId, b]]));
|
|
187
|
+
const { ctx, notifies } = makeTuiCtx();
|
|
188
|
+
await cmd.handler("wf-alp", ctx);
|
|
189
|
+
expect(vi.mocked(createWorkflowsView)).toHaveBeenCalledTimes(1);
|
|
190
|
+
expect((vi.mocked(createWorkflowsView).mock.calls[0] as unknown as [WorkflowRun])[0]).toBe(a);
|
|
191
|
+
|
|
192
|
+
await cmd.handler("wf-", ctx); // 两 run 同前缀 → 歧义不打开
|
|
193
|
+
expect(notifies.at(-1)).toMatchObject({ msg: "Workflow 'wf-' not found", level: "error" });
|
|
194
|
+
|
|
195
|
+
await cmd.handler("nope", ctx);
|
|
196
|
+
expect(notifies.at(-1)).toMatchObject({ msg: "Workflow 'nope' not found", level: "error" });
|
|
197
|
+
expect(vi.mocked(createWorkflowsView)).toHaveBeenCalledTimes(1);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("无参 0 runs → No workflows 提示;单 run 直开", async () => {
|
|
201
|
+
const cmd = captureCommand(() => new Map());
|
|
202
|
+
const { ctx, notifies } = makeTuiCtx();
|
|
203
|
+
await cmd.handler("", ctx);
|
|
204
|
+
expect(notifies).toEqual([{ msg: "No workflows in current session.", level: "info" }]);
|
|
205
|
+
|
|
206
|
+
const run = makeRun();
|
|
207
|
+
const cmd1 = captureCommand(() => new Map([[run.runId, run]]));
|
|
208
|
+
await cmd1.handler("", ctx);
|
|
209
|
+
expect(vi.mocked(createWorkflowsView)).toHaveBeenCalledTimes(1);
|
|
210
|
+
expect((vi.mocked(createWorkflowsView).mock.calls[0] as unknown as [WorkflowRun])[0]).toBe(run);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("无参多 runs → select 展示排序条目(running 优先、新在前),选中后打开", async () => {
|
|
214
|
+
const older = makeRun({ startedAt: T0, name: "older" });
|
|
215
|
+
const newer = makeRun({ startedAt: T0 + 60_000, name: "newer" });
|
|
216
|
+
const done = makeRun({ status: "done", name: "finished" });
|
|
217
|
+
const interrupted = makeRun({ status: "interrupted", name: "paused" });
|
|
218
|
+
const runs = new Map<string, WorkflowRun>([
|
|
219
|
+
[done.runId, done],
|
|
220
|
+
[interrupted.runId, interrupted],
|
|
221
|
+
[older.runId, older],
|
|
222
|
+
[newer.runId, newer],
|
|
223
|
+
]);
|
|
224
|
+
const cmd = captureCommand(() => runs);
|
|
225
|
+
const { ctx, notifies, select } = makeTuiCtx();
|
|
226
|
+
select.mockReturnValue(Promise.resolve(undefined)); // 取消 → 不打开
|
|
227
|
+
await cmd.handler("", ctx);
|
|
228
|
+
expect(vi.mocked(createWorkflowsView)).not.toHaveBeenCalled();
|
|
229
|
+
|
|
230
|
+
// 排序:running(新) → running(旧) → interrupted → done
|
|
231
|
+
const entries = select.mock.calls[0][1] as string[];
|
|
232
|
+
expect(entries).toHaveLength(4);
|
|
233
|
+
expect(entries[0]).toContain("newer");
|
|
234
|
+
expect(entries[1]).toContain("older");
|
|
235
|
+
expect(entries[2]).toContain("paused");
|
|
236
|
+
expect(entries[2]).toContain("[interrupted]");
|
|
237
|
+
expect(entries[3]).toContain("finished");
|
|
238
|
+
expect(entries[3]).toContain("[done]");
|
|
239
|
+
|
|
240
|
+
select.mockReturnValue(Promise.resolve(entries[1])); // 选中 older
|
|
241
|
+
await cmd.handler("", ctx);
|
|
242
|
+
expect(vi.mocked(createWorkflowsView)).toHaveBeenCalledTimes(1);
|
|
243
|
+
expect((vi.mocked(createWorkflowsView).mock.calls[0] as unknown as [WorkflowRun])[0]).toBe(older);
|
|
244
|
+
expect(notifies).toHaveLength(0);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("select 条目不在列表(索引 -1)→ 不打开", async () => {
|
|
248
|
+
const a = makeRun();
|
|
249
|
+
const b = makeRun();
|
|
250
|
+
const cmd = captureCommand(() => new Map([[a.runId, a], [b.runId, b]]));
|
|
251
|
+
const { ctx, select } = makeTuiCtx();
|
|
252
|
+
select.mockReturnValue(Promise.resolve("ghost-entry"));
|
|
253
|
+
await cmd.handler("", ctx);
|
|
254
|
+
expect(vi.mocked(createWorkflowsView)).not.toHaveBeenCalled();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ── openView 接线 ────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
describe("/workflows openView 接线", () => {
|
|
261
|
+
it("service 在场:liveRecords 绑定 collectRecordsByParentRunId(runId, LIST_LIMIT)", async () => {
|
|
262
|
+
const collectRecordsByParentRunId = vi.fn(() => []);
|
|
263
|
+
setSubagentService({
|
|
264
|
+
queries: { collectRecordsByParentRunId },
|
|
265
|
+
} as never);
|
|
266
|
+
const run = makeRun();
|
|
267
|
+
const cmd = captureCommand(() => new Map([[run.runId, run]]));
|
|
268
|
+
const { ctx } = makeTuiCtx();
|
|
269
|
+
await cmd.handler(run.runId, ctx);
|
|
270
|
+
|
|
271
|
+
const args = vi.mocked(createWorkflowsView).mock.calls[0] as unknown as [
|
|
272
|
+
WorkflowRun, unknown, ExtensionCommandContext, { abort: (id: string) => Promise<void> }, string, () => unknown[],
|
|
273
|
+
];
|
|
274
|
+
expect(args[5]).toBeTypeOf("function");
|
|
275
|
+
expect(args[5]()).toEqual([]);
|
|
276
|
+
expect(collectRecordsByParentRunId).toHaveBeenCalledWith(run.runId, LIST_LIMIT);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
@@ -21,16 +21,17 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
24
25
|
|
|
25
26
|
import type { LauncherDeps } from "@zhushanwen/subagent-core";
|
|
26
27
|
import { abortRun, getSubagentService, resumeRun } from "@zhushanwen/subagent-core";
|
|
27
28
|
import type { WorkflowRun } from "@zhushanwen/subagent-core";
|
|
28
29
|
import { parseWorkflowRpcCommand, type WorkflowRpcAction } from "./command-actions.ts";
|
|
29
|
-
import { createWorkflowsView, type ViewActions } from "
|
|
30
|
+
import { createWorkflowsView, type ViewActions } from "../tui/views/WorkflowsView.ts";
|
|
30
31
|
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
31
|
-
import { LIST_LIMIT } from "
|
|
32
|
-
import { ID_PREVIEW_LENGTH } from "
|
|
33
|
-
import { displayStatusOf } from "
|
|
32
|
+
import { LIST_LIMIT } from "../tui/list-shared.ts";
|
|
33
|
+
import { ID_PREVIEW_LENGTH } from "../format/id-preview.ts";
|
|
34
|
+
import { displayStatusOf } from "../tool/tool-workflow.ts";
|
|
34
35
|
|
|
35
36
|
/** status 显示顺序:running 优先(活跃态在前),interrupted 次之([D2] 暂停态
|
|
36
37
|
* ——无活体但可续跑,排活体后、终局前),再 startedAt 倒序。
|
|
@@ -94,8 +95,15 @@ export function registerWorkflowsCommand(
|
|
|
94
95
|
value: r.runId,
|
|
95
96
|
description: `${r.spec.scriptName} [${displayStatusOf(r)}]`,
|
|
96
97
|
}));
|
|
97
|
-
} catch {
|
|
98
|
-
//
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// runs 补全数据源不可用 → 补全缺失(返回 null,不改变 command 行为),warn 留痕
|
|
100
|
+
// ——补全静默消失与「当前 session 没有 run」不可区分。
|
|
101
|
+
// getLogger 惰性调用(catch 是冷路径——测试环境对 pi-extension-logger 的
|
|
102
|
+
// module-level mock 可能返回 undefined,模块级持有会在 import 期踩 undefined)
|
|
103
|
+
getLogger("subagent-workflow").warn(
|
|
104
|
+
"[commands] /workflows runId completion unavailable — runs data source failed, completion omitted",
|
|
105
|
+
{ reason: toErrorMessage(err) },
|
|
106
|
+
);
|
|
99
107
|
return null;
|
|
100
108
|
}
|
|
101
109
|
}
|
|
@@ -292,7 +300,7 @@ function sortedRuns(runs: Map<string, WorkflowRun>): WorkflowRun[] {
|
|
|
292
300
|
* 打开 WorkflowsView(三级导航 TUI),注入 lifecycle ViewActions。
|
|
293
301
|
*
|
|
294
302
|
* ViewActions 通过 deps 调 lifecycle(abort),与 view 解耦——
|
|
295
|
-
* view 单测可注入 mock actions(见 views/__tests__/WorkflowsView.test.ts)。
|
|
303
|
+
* view 单测可注入 mock actions(见 tui/views/__tests__/WorkflowsView.test.ts)。
|
|
296
304
|
*
|
|
297
305
|
* [H2 W3] live 进度数据源(设计 D2 进度源切换):view 经 store 查询
|
|
298
306
|
* collectRecordsByParentRunId(run.runId)(内存 ∪ 磁盘重建 ∪ manifest,LIST_LIMIT
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// src/interface/subagents.ts
|
|
1
|
+
// src/interface/command/subagents.ts
|
|
2
2
|
//
|
|
3
3
|
// /subagents 命令。薄壳——打开 list overlay(等同原 /subagents list [<id>])。
|
|
4
|
-
// 同名 tool(批量派发入口,模型调用面)见 interface/tool-subagents.ts——两者无共享状态、无调用关系。
|
|
4
|
+
// 同名 tool(批量派发入口,模型调用面)见 interface/tool/tool-subagents.ts——两者无共享状态、无调用关系。
|
|
5
5
|
//
|
|
6
6
|
// 解析:args[0] 直接作可选 <id>(聚焦该 record)。
|
|
7
7
|
// RPC 模式(taiji GUI):解析 cancel/message/start action 直接执行,不打开 TUI。
|
|
@@ -17,8 +17,8 @@ import { messageHandler, startHandler } from "@zhushanwen/subagent-core";
|
|
|
17
17
|
import { SUBAGENT_DIRECTIVE_CUSTOM_TYPE } from "@zhushanwen/extension-protocol";
|
|
18
18
|
import { parseSubagentRpcCommand } from "./command-actions.ts";
|
|
19
19
|
import type { SubagentRpcAction } from "./command-actions.ts";
|
|
20
|
-
import { LIST_LIMIT } from "
|
|
21
|
-
import { createSubagentsView } from "
|
|
20
|
+
import { LIST_LIMIT } from "../tui/list-shared.ts";
|
|
21
|
+
import { createSubagentsView } from "../tui/list-view.ts";
|
|
22
22
|
import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
|
|
23
23
|
|
|
24
24
|
// subagent-directive customType 经 extension-protocol 单源(与 shared/runtime 消费侧
|