@zhushanwen/pi-subagent-workflow 0.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.
- package/agents/context-builder.md +17 -0
- package/agents/general-purpose.md +16 -0
- package/agents/oracle.md +17 -0
- package/agents/planner.md +17 -0
- package/agents/researcher.md +17 -0
- package/agents/reviewer.md +17 -0
- package/agents/scout.md +17 -0
- package/agents/worker.md +16 -0
- package/examples/README.md +43 -0
- package/examples/chain.example.js +92 -0
- package/examples/map-reduce.example.js +99 -0
- package/examples/parallel.example.js +82 -0
- package/examples/scatter-gather.example.js +106 -0
- package/index.ts +1 -0
- package/package.json +66 -0
- package/skills/workflow-script-format/SKILL.md +328 -0
- package/src/execution/__tests__/agent-registry.test.ts +164 -0
- package/src/execution/__tests__/agent-result-mapper.test.ts +128 -0
- package/src/execution/__tests__/alive-store.test.ts +147 -0
- package/src/execution/__tests__/bg-notify-render.test.ts +256 -0
- package/src/execution/__tests__/concurrency-pool.test.ts +217 -0
- package/src/execution/__tests__/config.test.ts +110 -0
- package/src/execution/__tests__/crash-recovery.test.ts +311 -0
- package/src/execution/__tests__/execute-nesting.test.ts +359 -0
- package/src/execution/__tests__/execute-options-mapper.test.ts +138 -0
- package/src/execution/__tests__/execution-record.test.ts +959 -0
- package/src/execution/__tests__/finalized-marker.test.ts +82 -0
- package/src/execution/__tests__/format-schema-instruction.test.ts +135 -0
- package/src/execution/__tests__/format.test.ts +320 -0
- package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
- package/src/execution/__tests__/list-component.test.ts +347 -0
- package/src/execution/__tests__/model-resolver.test.ts +356 -0
- package/src/execution/__tests__/output-collector.test.ts +61 -0
- package/src/execution/__tests__/path-encoding.test.ts +75 -0
- package/src/execution/__tests__/pi-invocation.test.ts +73 -0
- package/src/execution/__tests__/record-store.test.ts +545 -0
- package/src/execution/__tests__/run-spawn-edges.test.ts +439 -0
- package/src/execution/__tests__/run-spawn-integration.test.ts +897 -0
- package/src/execution/__tests__/sdk-contract.test.ts +272 -0
- package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
- package/src/execution/__tests__/session-file-gc.test.ts +247 -0
- package/src/execution/__tests__/session-reconstructor.test.ts +359 -0
- package/src/execution/__tests__/session-runner-schema-env.test.ts +314 -0
- package/src/execution/__tests__/session-start-reaper.test.ts +227 -0
- package/src/execution/__tests__/spawn-args.test.ts +244 -0
- package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
- package/src/execution/__tests__/subagent-service.test.ts +678 -0
- package/src/execution/__tests__/subprocess-agent-runner.test.ts +389 -0
- package/src/execution/__tests__/temp-prompt.test.ts +53 -0
- package/src/execution/__tests__/timeout-integration.test.ts +381 -0
- package/src/execution/__tests__/tombstone-store.test.ts +73 -0
- package/src/execution/__tests__/tool-action.test.ts +330 -0
- package/src/execution/__tests__/turn-limiter.test.ts +65 -0
- package/src/execution/__tests__/worktree-manager.test.ts +423 -0
- package/src/execution/__tests__/worktree-registry.test.ts +161 -0
- package/src/execution/agent-registry.ts +252 -0
- package/src/execution/agent-result-mapper.ts +84 -0
- package/src/execution/alive-store.ts +92 -0
- package/src/execution/best-effort.ts +30 -0
- package/src/execution/concurrency-pool.ts +84 -0
- package/src/execution/config.ts +73 -0
- package/src/execution/execute-options-mapper.ts +86 -0
- package/src/execution/execution-record.ts +778 -0
- package/src/execution/finalized-marker.ts +51 -0
- package/src/execution/model-config-service.ts +225 -0
- package/src/execution/model-resolver.ts +247 -0
- package/src/execution/notifier.ts +168 -0
- package/src/execution/output-collector.ts +88 -0
- package/src/execution/path-encoding.ts +34 -0
- package/src/execution/pi-invocation.ts +70 -0
- package/src/execution/record-store.ts +350 -0
- package/src/execution/session-context-resolver.ts +64 -0
- package/src/execution/session-file-gc.ts +98 -0
- package/src/execution/session-reconstructor.ts +450 -0
- package/src/execution/session-runner.ts +725 -0
- package/src/execution/spawn-event-adapter.ts +150 -0
- package/src/execution/subagent-service.ts +973 -0
- package/src/execution/subprocess-agent-runner.ts +108 -0
- package/src/execution/temp-prompt.ts +57 -0
- package/src/execution/tombstone-store.ts +72 -0
- package/src/execution/turn-limiter.ts +88 -0
- package/src/execution/types.ts +634 -0
- package/src/execution/worktree-manager.ts +285 -0
- package/src/execution/worktree-registry.ts +144 -0
- package/src/index.ts +454 -0
- package/src/interface/bg-notify-render.ts +286 -0
- package/src/interface/commands.ts +157 -0
- package/src/interface/format.ts +501 -0
- package/src/interface/gui-adapter.ts +136 -0
- package/src/interface/helpers.ts +110 -0
- package/src/interface/list-component.ts +643 -0
- package/src/interface/list-shared.ts +84 -0
- package/src/interface/list-view.ts +373 -0
- package/src/interface/reentry-guard.ts +30 -0
- package/src/interface/subagent-actions.ts +294 -0
- package/src/interface/subagent-tool.ts +294 -0
- package/src/interface/subagents.ts +30 -0
- package/src/interface/tool-render.ts +333 -0
- package/src/interface/tool-workflow-script.ts +351 -0
- package/src/interface/tool-workflow.ts +485 -0
- package/src/interface/views/WorkflowsView.ts +944 -0
- package/src/interface/views/detail-content.ts +298 -0
- package/src/interface/views/format.ts +320 -0
- package/src/orchestration/__tests__/concurrency-gate.test.ts +125 -0
- package/src/orchestration/__tests__/config-loader.test.ts +381 -0
- package/src/orchestration/__tests__/error-recovery-handlers.test.ts +332 -0
- package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
- package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +248 -0
- package/src/orchestration/__tests__/lifecycle.test.ts +385 -0
- package/src/orchestration/__tests__/script-lint.test.ts +347 -0
- package/src/orchestration/__tests__/worker-script-builder.test.ts +42 -0
- package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +319 -0
- package/src/orchestration/agent-opts-resolver.ts +128 -0
- package/src/orchestration/concurrency-gate.ts +69 -0
- package/src/orchestration/config-loader.ts +313 -0
- package/src/orchestration/error-recovery.ts +578 -0
- package/src/orchestration/execute-agent-call.ts +174 -0
- package/src/orchestration/jsonl-run-store.ts +292 -0
- package/src/orchestration/launcher.ts +368 -0
- package/src/orchestration/lifecycle.ts +373 -0
- package/src/orchestration/models/__tests__/budget.test.ts +367 -0
- package/src/orchestration/models/agent-call.ts +76 -0
- package/src/orchestration/models/budget.ts +148 -0
- package/src/orchestration/models/ports.ts +165 -0
- package/src/orchestration/models/run-runtime.ts +91 -0
- package/src/orchestration/models/run-spec.ts +54 -0
- package/src/orchestration/models/run-state.ts +44 -0
- package/src/orchestration/models/trace.ts +102 -0
- package/src/orchestration/models/types.ts +242 -0
- package/src/orchestration/models/workflow-run.ts +275 -0
- package/src/orchestration/models/workflow-script-registry.ts +32 -0
- package/src/orchestration/models/workflow-script.ts +90 -0
- package/src/orchestration/node-ops.ts +192 -0
- package/src/orchestration/script-lint.ts +387 -0
- package/src/orchestration/skill-discovery.ts +60 -0
- package/src/orchestration/worker-handle.ts +115 -0
- package/src/orchestration/worker-host.ts +93 -0
- package/src/orchestration/worker-script-builder.ts +281 -0
- package/src/orchestration/workflow-files.ts +85 -0
- package/src/orchestration/workflow-script-registry-impl.ts +128 -0
- package/src/shared/__tests__/resource-discovery.test.ts +226 -0
- package/src/shared/agent-event.ts +13 -0
- package/src/shared/resource-discovery.ts +535 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/tui/list-shared.ts
|
|
2
|
+
//
|
|
3
|
+
// list-view 与 list-component 共享的类型/常量/纯函数,避免两者循环依赖。
|
|
4
|
+
// 原循环:list-view import 组件类 SubagentsListComponent;组件 import key 处理 + 状态类型。
|
|
5
|
+
// 本文件下沉「无跨文件依赖」的共享契约(list-view → list-component 单向)。
|
|
6
|
+
// 按键处理 processKey 仍留 list-view,由 list-view factory 注入给组件(KeyHandler 类型在此),
|
|
7
|
+
// 故 list-component 无需 import list-view,循环消除。
|
|
8
|
+
|
|
9
|
+
import type { SubagentService } from "../execution/subagent-service.ts";
|
|
10
|
+
import type { SubagentRecord } from "../execution/types.ts";
|
|
11
|
+
|
|
12
|
+
// ============================================================
|
|
13
|
+
// 常量
|
|
14
|
+
// ============================================================
|
|
15
|
+
|
|
16
|
+
/** list 收集的 record 上限(足够覆盖一个活跃 session)。factory/key/组件共用。 */
|
|
17
|
+
export const LIST_LIMIT = 100;
|
|
18
|
+
|
|
19
|
+
// ============================================================
|
|
20
|
+
// 类型
|
|
21
|
+
// ============================================================
|
|
22
|
+
|
|
23
|
+
/** list 视图内部状态。 */
|
|
24
|
+
export interface ViewState {
|
|
25
|
+
selectedIdx: number;
|
|
26
|
+
scrollOffset: number;
|
|
27
|
+
filterText: string;
|
|
28
|
+
detailMode: boolean;
|
|
29
|
+
disposed: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 详情翻屏上下文(processKey 算步长用)。 */
|
|
33
|
+
export interface DetailKeyContext {
|
|
34
|
+
viewportHeight: number;
|
|
35
|
+
contentLines?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** TUI 接口(duck-type:requestRender + terminal.rows)。
|
|
39
|
+
* terminal.rows 用于全屏框填满 + 详情翻屏步长(同 WorkflowsView.ts:104 cast)。 */
|
|
40
|
+
export interface TuiLike {
|
|
41
|
+
requestRender(): void;
|
|
42
|
+
terminal: { rows: number };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 触发外部 notify 的回调(避免 list-view 直接依赖 ctx.ui)。 */
|
|
46
|
+
export type NotifyFn = (message: string, type?: "info" | "warning" | "error") => void;
|
|
47
|
+
|
|
48
|
+
/** 按键处理结果:changed 表示状态变更需重绘;exit 表示调用方应关闭 overlay。二者正交。 */
|
|
49
|
+
export interface KeyResult {
|
|
50
|
+
/** 状态变更,需 invalidate + requestRender。 */
|
|
51
|
+
changed: boolean;
|
|
52
|
+
/** 调用方应调用 closeFn 关闭 overlay。 */
|
|
53
|
+
exit: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 按键处理函数签名(list-view 的 processKey 类型)。
|
|
57
|
+
* 依赖注入给 list-component,避免组件 import list-view 的 processKey 造成循环依赖。 */
|
|
58
|
+
export type KeyHandler = (
|
|
59
|
+
data: string,
|
|
60
|
+
records: SubagentRecord[],
|
|
61
|
+
state: ViewState,
|
|
62
|
+
selected: SubagentRecord | null,
|
|
63
|
+
service: SubagentService | null,
|
|
64
|
+
detailCtx: DetailKeyContext | undefined,
|
|
65
|
+
notify: NotifyFn | undefined,
|
|
66
|
+
) => KeyResult;
|
|
67
|
+
|
|
68
|
+
// ============================================================
|
|
69
|
+
// 纯函数
|
|
70
|
+
// ============================================================
|
|
71
|
+
|
|
72
|
+
/** filter 过滤 + 排序(纯函数,可单测)。 */
|
|
73
|
+
export function applyFilter(records: SubagentRecord[], filterText: string): SubagentRecord[] {
|
|
74
|
+
const q = filterText.trim().toLowerCase();
|
|
75
|
+
if (!q) return records;
|
|
76
|
+
return records.filter((r) => {
|
|
77
|
+
return (
|
|
78
|
+
r.agent.toLowerCase().includes(q) ||
|
|
79
|
+
r.status.toLowerCase().includes(q) ||
|
|
80
|
+
r.mode.toLowerCase().includes(q) ||
|
|
81
|
+
r.id.toLowerCase().includes(q)
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// src/tui/list-view.ts
|
|
2
|
+
//
|
|
3
|
+
// /subagents list 全屏带框左右分屏 overlay。
|
|
4
|
+
// 左列:record 列表(状态图标 + agent + mode + 绝对时长)
|
|
5
|
+
// 右列:选中 record 详情(eventLog + result/error,可翻屏)
|
|
6
|
+
//
|
|
7
|
+
// 布局(margin:0 全屏覆盖,自画视觉边距盖住底下对话流):
|
|
8
|
+
// overlay 覆盖整个终端;框外留 1 行/1 列空白(applyPadding 画),框在内侧。
|
|
9
|
+
// ┌────────────────────────────┐ ← overlay 顶空白行(盖底下)
|
|
10
|
+
// │ ╭─ Subagents ───────────╮ │ ← 左 1 空格 + 框 + 右 1 空格
|
|
11
|
+
// │ │ filter: _ │ │
|
|
12
|
+
// │ ├─ Records ─┬─ Detail ───┤ │
|
|
13
|
+
// │ │ body ... │ body ... │ │
|
|
14
|
+
// │ ├────────────┴────────────┤ │
|
|
15
|
+
// │ │ ↑↓ 导航 ... │ │
|
|
16
|
+
// │ ╰─────────────────────────╯ │
|
|
17
|
+
// └────────────────────────────┘ ← overlay 底空白行
|
|
18
|
+
// (外层框线仅为示意,实际是空白行/空格列)
|
|
19
|
+
//
|
|
20
|
+
// 契约(ctx.ui.custom overlay,对照 pi-tui-development-guide.md §3.2):
|
|
21
|
+
// custom<void>((tui, theme, kb, done) => Component, {overlay:true, overlayOptions})
|
|
22
|
+
// Component: render(width):string[] + invalidate() + handleInput?(data)
|
|
23
|
+
//
|
|
24
|
+
// 关键避坑:
|
|
25
|
+
// 1. G-017 防叠加:模块级 activeView 单例,进入前 close(),factory 内 setActiveView
|
|
26
|
+
// 2. 导航只用方向键 matchesKey("up"|"down"),禁 j/k(避 filter 冲突)
|
|
27
|
+
// 3. overlay 退出 wrappedDone:幂等→标记→unsubscribe→clearAnimTimer→clearActiveView→done
|
|
28
|
+
// 4. 不调 theme.bg(背景由 Pi overlay 容器施加),只 fg/bold
|
|
29
|
+
// 6. 所有行经 truncLine(ANSI 安全)
|
|
30
|
+
// 7. 边框不调 renderShell:"self"(守 default-shell / 无残影契约)
|
|
31
|
+
// 8. 不用 Pi 的 overlay margin(那是物理留白会透出底内容)——改 margin:0 全屏覆盖
|
|
32
|
+
// + applyPadding 自画视觉边距(顶底空白行 + 左右空格列),盖住底下对话流
|
|
33
|
+
// 9. 动画 setInterval(250ms) 安全:行数恒定(pad 到满屏),diff 只重画 spinner/elapsed
|
|
34
|
+
|
|
35
|
+
import { matchesKey } from "@earendil-works/pi-tui";
|
|
36
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
37
|
+
|
|
38
|
+
import type { SubagentService } from "../execution/subagent-service.ts";
|
|
39
|
+
import type { SubagentRecord } from "../execution/types.ts";
|
|
40
|
+
import { type ThemeLike } from "./format.ts";
|
|
41
|
+
import { SubagentsListComponent } from "./list-component.ts";
|
|
42
|
+
import {
|
|
43
|
+
type DetailKeyContext,
|
|
44
|
+
type KeyHandler,
|
|
45
|
+
type KeyResult,
|
|
46
|
+
LIST_LIMIT,
|
|
47
|
+
type NotifyFn,
|
|
48
|
+
type TuiLike,
|
|
49
|
+
type ViewState,
|
|
50
|
+
} from "./list-shared.ts";
|
|
51
|
+
|
|
52
|
+
// ============================================================
|
|
53
|
+
// 常量
|
|
54
|
+
// ============================================================
|
|
55
|
+
|
|
56
|
+
// 布局/边框常量(LEFT_COL_RATIO、COL_*、BORDER_WIDTH、PAD_*、MIN_*、TITLE_*、
|
|
57
|
+
// SPLIT_FIXED_LINES、TERM_ROWS_FALLBACK、DETAIL_LEN_PROBE_WIDTH、VERT_CENTER_DIVISOR、
|
|
58
|
+
// SPINNER_FRAME_MS、PREVIEW_RECENT_LINES)已随 SubagentsListComponent 移至 list-component.ts。
|
|
59
|
+
// 共享类型/常量/纯函数(LIST_LIMIT/ViewState/DetailKeyContext/TuiLike/NotifyFn/applyFilter/
|
|
60
|
+
// KeyResult/KeyHandler)已移至 list-shared.ts(消除 list-view ↔ list-component 循环依赖)。
|
|
61
|
+
|
|
62
|
+
/** 详情区 eventLog 翻屏步长(方向键单步)。 */
|
|
63
|
+
const DETAIL_SCROLL_STEP = 1;
|
|
64
|
+
/** 详情区 PgUp/PgDn 默认步长(无 viewport 信息时)。 */
|
|
65
|
+
const PAGE_SCROLL_DEFAULT = 10;
|
|
66
|
+
|
|
67
|
+
/** overlay 动画刷新间隔(spinner 换帧 + elapsed 跳动)。同 tool-render.ts SPINNER_INTERVAL_MS。 */
|
|
68
|
+
const OVERLAY_REFRESH_MS = 250;
|
|
69
|
+
/** onChange 重绘节流间隔。streaming 期间 text_delta 每个 token 都触发 store 变化,
|
|
70
|
+
* 若每次全屏 invalidate + requestRender,重绘频率极高,pi diff 引擎在高频重绘下
|
|
71
|
+
* 易产生 cell 残留(视觉重影)。节流到 120ms,animTimer(250ms)兜底保证刷新。 */
|
|
72
|
+
const ONCHANGE_DEBOUNCE_MS = 120;
|
|
73
|
+
|
|
74
|
+
// ============================================================
|
|
75
|
+
// G-017:模块级 overlay 单例(防叠加)
|
|
76
|
+
// ============================================================
|
|
77
|
+
|
|
78
|
+
/** 当前活动的 list overlay 句柄(null 表示无)。连按两次快捷键时先 close 前一个。 */
|
|
79
|
+
let activeView: { close: () => void } | null = null;
|
|
80
|
+
|
|
81
|
+
// ============================================================
|
|
82
|
+
// overlay 工厂
|
|
83
|
+
// ============================================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 创建全屏左右分屏 overlay。
|
|
87
|
+
*
|
|
88
|
+
* ╔══════════════════════════════════════════════════════════════════╗
|
|
89
|
+
* ║ 1. G-017 防叠加:activeView?.close() ║
|
|
90
|
+
* ║ 2. ctx.ui.custom((tui, theme, kb, done) => { ║
|
|
91
|
+
* ║ unsubscribe = service.onChange(() => tui.requestRender()) ║
|
|
92
|
+
* ║ activeView = { close: wrappedDone } ║
|
|
93
|
+
* ║ return new SubagentsListComponent(...) ║
|
|
94
|
+
* ║ }, { overlay:true, overlayOptions:{margin:0, width:"100%"}}) ║
|
|
95
|
+
* ║ ║
|
|
96
|
+
* ║ directId 不在 records 中 → notify 警告,仍打开列表 ║
|
|
97
|
+
* ╚══════════════════════════════════════════════════════════════════╝
|
|
98
|
+
*/
|
|
99
|
+
export async function createSubagentsView(
|
|
100
|
+
service: SubagentService,
|
|
101
|
+
theme: ThemeLike,
|
|
102
|
+
ctx: ExtensionContext,
|
|
103
|
+
directId?: string,
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
// G-017:先关前一个 overlay
|
|
106
|
+
if (activeView) {
|
|
107
|
+
activeView.close();
|
|
108
|
+
activeView = null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const notify: NotifyFn = (msg, type) => ctx.ui.notify(msg, type);
|
|
112
|
+
|
|
113
|
+
// directId 提示
|
|
114
|
+
if (directId) {
|
|
115
|
+
const all = service.collectRecords(LIST_LIMIT);
|
|
116
|
+
if (!all.some((r) => r.id === directId)) {
|
|
117
|
+
notify(`No record found for id "${directId}", showing all`, "warning");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await ctx.ui.custom<void>(
|
|
122
|
+
(tui, _theme, _kb, done) => {
|
|
123
|
+
// duck-type cast:读 terminal.rows 做满屏填高 + 详情翻屏步长
|
|
124
|
+
const tuiLike = tui as TuiLike;
|
|
125
|
+
const state: ViewState = {
|
|
126
|
+
selectedIdx: 0,
|
|
127
|
+
scrollOffset: 0,
|
|
128
|
+
filterText: "",
|
|
129
|
+
detailMode: false,
|
|
130
|
+
disposed: false,
|
|
131
|
+
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// 订阅 store 变化 → invalidate + requestRender(store 驱动重渲)。
|
|
135
|
+
// 必须 invalidate:render 命中 width×rows 缓存会返回旧行——最后一个 running record
|
|
136
|
+
// 完成、且无其他 running 时 animTimer 不再 invalidate(hasRunning=false 提前返回),
|
|
137
|
+
// 导致 running→done 状态翻转永不重绘。invalidateFn 在 component 创建后绑定。
|
|
138
|
+
// holder:onChange 注册时 component 尚未创建,用 ref 延后绑定 invalidate
|
|
139
|
+
// (避免 let 重赋值触发 prefer-const)
|
|
140
|
+
const invalidateRef = { fn: undefined as (() => void) | undefined };
|
|
141
|
+
// debounce:streaming 期间 text_delta 每个 token 都触发 store 变化,若每次都
|
|
142
|
+
// 全屏 invalidate + requestRender,重绘频率极高,pi diff 引擎在高频重绘下
|
|
143
|
+
// 易产生 cell 残留(视觉重影)。节流到 ONCHANGE_DEBOUNCE_MS,animTimer(250ms)
|
|
144
|
+
// 兜底刷新。终态变化(record 完成)延迟可接受。
|
|
145
|
+
let renderDebounce: ReturnType<typeof setTimeout> | undefined;
|
|
146
|
+
const unsubscribe = service.onChange(() => {
|
|
147
|
+
if (state.disposed) return;
|
|
148
|
+
if (renderDebounce) clearTimeout(renderDebounce);
|
|
149
|
+
renderDebounce = setTimeout(() => {
|
|
150
|
+
renderDebounce = undefined;
|
|
151
|
+
if (state.disposed) return;
|
|
152
|
+
invalidateRef.fn?.();
|
|
153
|
+
tuiLike.requestRender();
|
|
154
|
+
}, ONCHANGE_DEBOUNCE_MS);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// directId 命中 → 进详情模式(右侧就地展开,底部对齐)
|
|
158
|
+
if (directId) {
|
|
159
|
+
const records = service.collectRecords(LIST_LIMIT);
|
|
160
|
+
const idx = records.findIndex((r) => r.id === directId);
|
|
161
|
+
if (idx >= 0) {
|
|
162
|
+
state.selectedIdx = idx;
|
|
163
|
+
state.detailMode = true;
|
|
164
|
+
state.scrollOffset = 0; // 顶部对齐:task 置顶可见(与 Enter 进详情一致)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const component = new SubagentsListComponent(
|
|
169
|
+
service,
|
|
170
|
+
theme,
|
|
171
|
+
tuiLike,
|
|
172
|
+
state,
|
|
173
|
+
unsubscribe,
|
|
174
|
+
notify,
|
|
175
|
+
processKey, // 依赖注入:组件不 import list-view(消除循环依赖)
|
|
176
|
+
);
|
|
177
|
+
invalidateRef.fn = () => component.invalidate();
|
|
178
|
+
|
|
179
|
+
// 动画 timer:有 running record 时定期 invalidate + requestRender,
|
|
180
|
+
// 让 spinner 丝滑换帧、elapsed 实时跳动(行数恒定,安全——对照
|
|
181
|
+
// tool-render.ts 的 setInterval 模式 + dev guide §8160a5d13 安全分析)。
|
|
182
|
+
const animTimer = setInterval(() => {
|
|
183
|
+
if (state.disposed) return;
|
|
184
|
+
if (!component.hasRunning()) return; // 无 running 不浪费刷新
|
|
185
|
+
component.invalidate();
|
|
186
|
+
tuiLike.requestRender();
|
|
187
|
+
}, OVERLAY_REFRESH_MS);
|
|
188
|
+
component.setAnimTimer(animTimer);
|
|
189
|
+
|
|
190
|
+
// wrappedDone(dev guide §4 顺序:幂等→标记→unsubscribe→clearAnimTimer→clearActiveView→done)
|
|
191
|
+
const wrappedDone = () => {
|
|
192
|
+
if (state.disposed) return; // 幂等
|
|
193
|
+
state.disposed = true; // ① 标记
|
|
194
|
+
unsubscribe(); // ② 解订 store 事件
|
|
195
|
+
clearInterval(animTimer); // ③ 清动画 timer
|
|
196
|
+
if (renderDebounce) clearTimeout(renderDebounce); // ④ 清 debounce timer
|
|
197
|
+
activeView = null; // ⑤ 清 G-017 句柄
|
|
198
|
+
done(undefined); // ⑥ 框架 done(触发 overlay 销毁)
|
|
199
|
+
};
|
|
200
|
+
component.setCloseFn(wrappedDone);
|
|
201
|
+
activeView = { close: wrappedDone };
|
|
202
|
+
|
|
203
|
+
return component;
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
overlay: true,
|
|
207
|
+
overlayOptions: {
|
|
208
|
+
anchor: "center" as const,
|
|
209
|
+
width: "100%",
|
|
210
|
+
maxHeight: "100%",
|
|
211
|
+
// margin:0 → overlay 覆盖整个终端(不留物理空白)。
|
|
212
|
+
// 视觉边距由 buildLines 自画(顶底空白行 + 左右空格列),盖住底下对话流。
|
|
213
|
+
// Pi 的 margin 是「物理留白透出底内容」,这里不能用。
|
|
214
|
+
margin: 0,
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ============================================================
|
|
221
|
+
// 按键处理(纯函数,可单测)
|
|
222
|
+
// ============================================================
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 按键处理。两阶段焦点(detailMode 控制):
|
|
226
|
+
*
|
|
227
|
+
* ╔══════════════════════════════════════════════════════════════════╗
|
|
228
|
+
// ║ 阶段 1(list 焦点,detailMode=false): ║
|
|
229
|
+
// ║ Esc 退出(有 filter 先清)/ ↑↓ 导航左列 / Enter 进阶段 2 ║
|
|
230
|
+
// ║ Backspace 删 filter / 可打印字符直接 filter ║
|
|
231
|
+
// ║ ║
|
|
232
|
+
// ║ 阶段 2(detail 焦点,detailMode=true):左侧锚定,滚右侧详情 ║
|
|
233
|
+
// ║ Esc 返回阶段 1 / ↑↓ PgUp/PgDn Home End 滚右侧 eventLog ║
|
|
234
|
+
// ║ x 停止:background → service.cancel(id)(真正 abort) ║
|
|
235
|
+
// ╚══════════════════════════════════════════════════════════════════╝
|
|
236
|
+
*
|
|
237
|
+
* 返回 KeyResult:changed 表示状态变更需重绘;exit 表示调用方应关闭 overlay。
|
|
238
|
+
* 二者正交——Esc 在阶段 1 无 filter 时 changed=false + exit=true。
|
|
239
|
+
*
|
|
240
|
+
* 注:KeyResult/KeyHandler 类型定义在 list-shared.ts(list-component 也用 KeyHandler 做构造
|
|
241
|
+
* 参数类型,避免组件 import list-view)。本函数由 list-view factory 注入组件(依赖注入)。
|
|
242
|
+
*/
|
|
243
|
+
export const processKey: KeyHandler = (
|
|
244
|
+
data,
|
|
245
|
+
records,
|
|
246
|
+
state,
|
|
247
|
+
selected,
|
|
248
|
+
service,
|
|
249
|
+
detailCtx,
|
|
250
|
+
notify,
|
|
251
|
+
): KeyResult => {
|
|
252
|
+
// ── 阶段 2(detail 焦点,detailMode=true):左侧锚定,滚右侧详情 ──
|
|
253
|
+
if (state.detailMode) {
|
|
254
|
+
if (matchesKey(data, "escape")) {
|
|
255
|
+
state.detailMode = false;
|
|
256
|
+
state.scrollOffset = 0;
|
|
257
|
+
return { changed: true, exit: false };
|
|
258
|
+
}
|
|
259
|
+
if (matchesKey(data, "up")) {
|
|
260
|
+
state.scrollOffset = Math.max(0, state.scrollOffset - DETAIL_SCROLL_STEP);
|
|
261
|
+
return { changed: true, exit: false };
|
|
262
|
+
}
|
|
263
|
+
if (matchesKey(data, "down")) {
|
|
264
|
+
const max = detailScrollMax(detailCtx);
|
|
265
|
+
state.scrollOffset = Math.min(max, state.scrollOffset + DETAIL_SCROLL_STEP);
|
|
266
|
+
return { changed: true, exit: false };
|
|
267
|
+
}
|
|
268
|
+
if (matchesKey(data, "pageUp")) {
|
|
269
|
+
const step = detailCtx?.viewportHeight ?? PAGE_SCROLL_DEFAULT;
|
|
270
|
+
state.scrollOffset = Math.max(0, state.scrollOffset - step);
|
|
271
|
+
return { changed: true, exit: false };
|
|
272
|
+
}
|
|
273
|
+
if (matchesKey(data, "pageDown")) {
|
|
274
|
+
const step = detailCtx?.viewportHeight ?? PAGE_SCROLL_DEFAULT;
|
|
275
|
+
const max = detailScrollMax(detailCtx);
|
|
276
|
+
state.scrollOffset = Math.min(max, state.scrollOffset + step);
|
|
277
|
+
return { changed: true, exit: false };
|
|
278
|
+
}
|
|
279
|
+
if (matchesKey(data, "home")) {
|
|
280
|
+
state.scrollOffset = 0;
|
|
281
|
+
return { changed: true, exit: false };
|
|
282
|
+
}
|
|
283
|
+
if (matchesKey(data, "end")) {
|
|
284
|
+
state.scrollOffset = detailScrollMax(detailCtx);
|
|
285
|
+
return { changed: true, exit: false };
|
|
286
|
+
}
|
|
287
|
+
// x:停止当前 record
|
|
288
|
+
if (data === "x" && selected) {
|
|
289
|
+
const changed = handleCancel(selected, service, notify);
|
|
290
|
+
return { changed, exit: false };
|
|
291
|
+
}
|
|
292
|
+
return { changed: false, exit: false };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── 阶段 1(list 焦点,detailMode=false):↑↓ 导航左列 ──
|
|
296
|
+
if (matchesKey(data, "escape")) {
|
|
297
|
+
// 有 filter 先清(changed);无 filter → 退出 overlay(exit)
|
|
298
|
+
if (state.filterText.length > 0) {
|
|
299
|
+
state.filterText = "";
|
|
300
|
+
state.selectedIdx = 0;
|
|
301
|
+
return { changed: true, exit: false };
|
|
302
|
+
}
|
|
303
|
+
return { changed: false, exit: true };
|
|
304
|
+
}
|
|
305
|
+
if (matchesKey(data, "up")) {
|
|
306
|
+
state.selectedIdx = Math.max(0, state.selectedIdx - 1);
|
|
307
|
+
return { changed: true, exit: false };
|
|
308
|
+
}
|
|
309
|
+
if (matchesKey(data, "down")) {
|
|
310
|
+
state.selectedIdx = Math.min(Math.max(0, records.length - 1), state.selectedIdx + 1);
|
|
311
|
+
return { changed: true, exit: false };
|
|
312
|
+
}
|
|
313
|
+
if (matchesKey(data, "enter") || matchesKey(data, "return")) {
|
|
314
|
+
if (selected) {
|
|
315
|
+
state.detailMode = true;
|
|
316
|
+
// 顶部对齐:task 提示词置顶(buildDetailContent 首行),进详情第一眼就看到。
|
|
317
|
+
// 此前用 MAX_SAFE_INTEGER 底部对齐是为「event log 最新在底」,但 content > viewH 时
|
|
318
|
+
// 会把置顶的 task 滚出视口顶——task 是「它在干嘛」的唯一线索(streaming 尤甚),
|
|
319
|
+
// 必须可见。event log 在下方,向下滚可看历史。
|
|
320
|
+
state.scrollOffset = 0;
|
|
321
|
+
return { changed: true, exit: false };
|
|
322
|
+
}
|
|
323
|
+
return { changed: false, exit: false };
|
|
324
|
+
}
|
|
325
|
+
if (matchesKey(data, "backspace")) {
|
|
326
|
+
if (state.filterText.length > 0) {
|
|
327
|
+
state.filterText = state.filterText.slice(0, -1);
|
|
328
|
+
state.selectedIdx = 0;
|
|
329
|
+
return { changed: true, exit: false };
|
|
330
|
+
}
|
|
331
|
+
return { changed: false, exit: false };
|
|
332
|
+
}
|
|
333
|
+
// 可打印字符 → filter(单字符 ASCII 可见区)
|
|
334
|
+
if (data.length === 1 && data >= " " && data <= "~") {
|
|
335
|
+
state.filterText += data;
|
|
336
|
+
state.selectedIdx = 0;
|
|
337
|
+
return { changed: true, exit: false };
|
|
338
|
+
}
|
|
339
|
+
return { changed: false, exit: false };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ============================================================
|
|
343
|
+
// 内部辅助
|
|
344
|
+
// ============================================================
|
|
345
|
+
|
|
346
|
+
/** 详情翻屏最大 offset(contentLines - viewportHeight,兜底 0)。
|
|
347
|
+
* 与 renderRightDetail 的 max 计算保持一致(content.length - viewH)。 */
|
|
348
|
+
function detailScrollMax(detailCtx: DetailKeyContext | undefined): number {
|
|
349
|
+
const content = detailCtx?.contentLines ?? 0;
|
|
350
|
+
const viewH = detailCtx?.viewportHeight ?? 1;
|
|
351
|
+
return Math.max(0, content - viewH);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** 处理取消按键(x)。background 真正 abort。返回是否变化。 */
|
|
355
|
+
function handleCancel(
|
|
356
|
+
record: SubagentRecord,
|
|
357
|
+
service: SubagentService | null,
|
|
358
|
+
notify: NotifyFn | undefined,
|
|
359
|
+
): boolean {
|
|
360
|
+
if (record.status !== "running") {
|
|
361
|
+
notify?.(`Cannot stop: record is ${record.status}`, "warning");
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
if (!service) {
|
|
365
|
+
notify?.("Runtime not ready, cannot stop", "error");
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
const ok = service.cancel(record.id);
|
|
369
|
+
notify?.(ok ? `Requested stop for ${record.id}` : `Stop failed (record may have ended)`, ok ? "info" : "warning");
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// firstLine 已上移到 ./format.ts 共享。
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reentry-guard.ts — Shared reentry guard helpers for workflow tools.
|
|
3
|
+
*
|
|
4
|
+
* Keeps the "check → set → try/finally release" pattern in one place
|
|
5
|
+
* without wrapping execute in a higher-order function (which would
|
|
6
|
+
* complicate inferred union return types).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ReentryGuardRef {
|
|
10
|
+
isProcessing: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** reentry 拒绝时的统一提示文案。 */
|
|
14
|
+
export const REENTRY_BUSY_MESSAGE =
|
|
15
|
+
"Another workflow operation is in progress; please wait for it to complete before issuing another command.";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 尝试获取 reentry guard。返回 true 表示获取成功(调用方必须在 finally 中调用 releaseReentryGuard)。
|
|
19
|
+
* 已被占用时返回 false,调用方应返回 isError:true 的 busy 响应。
|
|
20
|
+
*/
|
|
21
|
+
export function acquireReentryGuard(guard: ReentryGuardRef): boolean {
|
|
22
|
+
if (guard.isProcessing) return false;
|
|
23
|
+
guard.isProcessing = true;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 释放 reentry guard。仅在 acquire 成功后调用。 */
|
|
28
|
+
export function releaseReentryGuard(guard: ReentryGuardRef): void {
|
|
29
|
+
guard.isProcessing = false;
|
|
30
|
+
}
|