@marcoscale98/pi-extensible-workflows 5.14.1-fork.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.
Files changed (204) hide show
  1. package/CHANGELOG.md +536 -0
  2. package/README.md +64 -0
  3. package/dist/src/agent-execution.d.ts +338 -0
  4. package/dist/src/agent-execution.js +1633 -0
  5. package/dist/src/background-widget.d.ts +74 -0
  6. package/dist/src/background-widget.js +1340 -0
  7. package/dist/src/budget.d.ts +38 -0
  8. package/dist/src/budget.js +188 -0
  9. package/dist/src/changelog.d.ts +6 -0
  10. package/dist/src/changelog.js +112 -0
  11. package/dist/src/decoders.d.ts +96 -0
  12. package/dist/src/decoders.js +629 -0
  13. package/dist/src/execution.d.ts +19 -0
  14. package/dist/src/execution.js +749 -0
  15. package/dist/src/herdr.d.ts +53 -0
  16. package/dist/src/herdr.js +270 -0
  17. package/dist/src/host-delivery.d.ts +95 -0
  18. package/dist/src/host-delivery.js +376 -0
  19. package/dist/src/host-navigator.d.ts +83 -0
  20. package/dist/src/host-navigator.js +1087 -0
  21. package/dist/src/host-phases.d.ts +89 -0
  22. package/dist/src/host-phases.js +320 -0
  23. package/dist/src/host-recovery.d.ts +141 -0
  24. package/dist/src/host-recovery.js +356 -0
  25. package/dist/src/host-runtime.d.ts +78 -0
  26. package/dist/src/host-runtime.js +378 -0
  27. package/dist/src/host-view.d.ts +116 -0
  28. package/dist/src/host-view.js +868 -0
  29. package/dist/src/host.d.ts +40 -0
  30. package/dist/src/host.js +1641 -0
  31. package/dist/src/index.d.ts +29 -0
  32. package/dist/src/index.js +13725 -0
  33. package/dist/src/index.js.map +6 -0
  34. package/dist/src/io.d.ts +14 -0
  35. package/dist/src/io.js +82 -0
  36. package/dist/src/package-identity.d.ts +4 -0
  37. package/dist/src/package-identity.js +6 -0
  38. package/dist/src/paths.d.ts +14 -0
  39. package/dist/src/paths.js +51 -0
  40. package/dist/src/persistence.d.ts +5 -0
  41. package/dist/src/persistence.js +5 -0
  42. package/dist/src/pi-runtime-adapter.d.ts +27 -0
  43. package/dist/src/pi-runtime-adapter.js +243 -0
  44. package/dist/src/pi-runtime-runner.d.ts +42 -0
  45. package/dist/src/pi-runtime-runner.js +505 -0
  46. package/dist/src/registry.d.ts +48 -0
  47. package/dist/src/registry.js +379 -0
  48. package/dist/src/retention.d.ts +17 -0
  49. package/dist/src/retention.js +195 -0
  50. package/dist/src/runtime/agent-runner.d.ts +143 -0
  51. package/dist/src/runtime/agent-runner.js +12 -0
  52. package/dist/src/runtime/index.d.ts +2 -0
  53. package/dist/src/runtime/index.js +2 -0
  54. package/dist/src/runtime/namespace.d.ts +14 -0
  55. package/dist/src/runtime/namespace.js +1 -0
  56. package/dist/src/runtime/workflow-result.d.ts +2 -0
  57. package/dist/src/runtime/workflow-result.js +6 -0
  58. package/dist/src/session-handoff.d.ts +2 -0
  59. package/dist/src/session-handoff.js +82 -0
  60. package/dist/src/session-lease.d.ts +12 -0
  61. package/dist/src/session-lease.js +180 -0
  62. package/dist/src/store.d.ts +95 -0
  63. package/dist/src/store.js +951 -0
  64. package/dist/src/subagent-manager-handle.d.ts +4 -0
  65. package/dist/src/subagent-manager-handle.js +6 -0
  66. package/dist/src/tool-timing.d.ts +12 -0
  67. package/dist/src/tool-timing.js +33 -0
  68. package/dist/src/trajectory-contracts.d.ts +8 -0
  69. package/dist/src/trajectory-contracts.js +9 -0
  70. package/dist/src/trajectory-host-handle.d.ts +12 -0
  71. package/dist/src/trajectory-host-handle.js +6 -0
  72. package/dist/src/trajectory.d.ts +128 -0
  73. package/dist/src/trajectory.js +594 -0
  74. package/dist/src/types.d.ts +960 -0
  75. package/dist/src/types.js +60 -0
  76. package/dist/src/utils.d.ts +57 -0
  77. package/dist/src/utils.js +225 -0
  78. package/dist/src/validation.d.ts +29 -0
  79. package/dist/src/validation.js +1100 -0
  80. package/dist/src/workflow-artifacts.d.ts +14 -0
  81. package/dist/src/workflow-artifacts.js +40 -0
  82. package/dist/starter/index.d.ts +1 -0
  83. package/dist/starter/index.js +103 -0
  84. package/dist/starter/index.js.map +6 -0
  85. package/dist/starter/review-loop.d.ts +72 -0
  86. package/dist/starter/review-loop.js +43 -0
  87. package/dist/starter/roles/developer.md +6 -0
  88. package/dist/starter/roles/oracle.md +18 -0
  89. package/dist/starter/roles/researcher.md +20 -0
  90. package/dist/starter/roles/reviewer.md +19 -0
  91. package/dist/starter/roles/scout.md +18 -0
  92. package/dist/subagents/index.d.ts +8 -0
  93. package/dist/subagents/index.js +8021 -0
  94. package/dist/subagents/index.js.map +6 -0
  95. package/dist/subagents/src/contracts.d.ts +173 -0
  96. package/dist/subagents/src/contracts.js +68 -0
  97. package/dist/subagents/src/decode.d.ts +15 -0
  98. package/dist/subagents/src/decode.js +182 -0
  99. package/dist/subagents/src/index.d.ts +12 -0
  100. package/dist/subagents/src/index.js +180 -0
  101. package/dist/subagents/src/manager.d.ts +3 -0
  102. package/dist/subagents/src/manager.js +1636 -0
  103. package/dist/subagents/src/navigator.d.ts +5 -0
  104. package/dist/subagents/src/navigator.js +627 -0
  105. package/dist/subagents/src/view.d.ts +70 -0
  106. package/dist/subagents/src/view.js +387 -0
  107. package/dist/subagents/src/worktree.d.ts +34 -0
  108. package/dist/subagents/src/worktree.js +68 -0
  109. package/dist/trajectory/assets/favicon.png +0 -0
  110. package/dist/trajectory/assets/index.html +772 -0
  111. package/dist/trajectory/assets/marked.min.js +6 -0
  112. package/dist/trajectory/assets/morphdom.min.js +1 -0
  113. package/dist/trajectory/index.d.ts +5 -0
  114. package/dist/trajectory/index.js +3459 -0
  115. package/dist/trajectory/index.js.map +6 -0
  116. package/dist/trajectory/src/assets/favicon.png +0 -0
  117. package/dist/trajectory/src/assets/index.html +772 -0
  118. package/dist/trajectory/src/assets/marked.min.js +6 -0
  119. package/dist/trajectory/src/assets/morphdom.min.js +1 -0
  120. package/dist/trajectory/src/export.d.ts +25 -0
  121. package/dist/trajectory/src/export.js +80 -0
  122. package/dist/trajectory/src/index.d.ts +26 -0
  123. package/dist/trajectory/src/index.js +732 -0
  124. package/dist/trajectory/src/server.d.ts +7 -0
  125. package/dist/trajectory/src/server.js +2534 -0
  126. package/dist/trajectory/src/server.js.map +6 -0
  127. package/examples/advanced-patterns/auto-model-router.mjs +78 -0
  128. package/examples/workflow-extension-template/README.md +37 -0
  129. package/examples/workflow-extension-template/extension.test.mjs +59 -0
  130. package/examples/workflow-extension-template/index.js +51 -0
  131. package/examples/workflow-extension-template/roles/reviewer.md +4 -0
  132. package/package.json +105 -0
  133. package/skills/pi-extensible-workflows/SKILL.md +160 -0
  134. package/src/agent-execution.ts +1533 -0
  135. package/src/background-widget.ts +1496 -0
  136. package/src/budget.ts +109 -0
  137. package/src/changelog.ts +114 -0
  138. package/src/decoders.ts +567 -0
  139. package/src/execution.ts +652 -0
  140. package/src/herdr.ts +240 -0
  141. package/src/host-delivery.ts +346 -0
  142. package/src/host-navigator.ts +871 -0
  143. package/src/host-phases.ts +264 -0
  144. package/src/host-recovery.ts +333 -0
  145. package/src/host-runtime.ts +373 -0
  146. package/src/host-view.ts +828 -0
  147. package/src/host.ts +1502 -0
  148. package/src/index.ts +29 -0
  149. package/src/io.ts +73 -0
  150. package/src/package-identity.ts +7 -0
  151. package/src/paths.ts +49 -0
  152. package/src/persistence.ts +11 -0
  153. package/src/pi-runtime-adapter.ts +247 -0
  154. package/src/pi-runtime-runner.ts +436 -0
  155. package/src/registry.ts +321 -0
  156. package/src/retention.ts +178 -0
  157. package/src/runtime/agent-runner.ts +158 -0
  158. package/src/runtime/index.ts +2 -0
  159. package/src/runtime/namespace.ts +16 -0
  160. package/src/runtime/workflow-result.ts +8 -0
  161. package/src/session-handoff.ts +78 -0
  162. package/src/session-lease.ts +125 -0
  163. package/src/store.ts +832 -0
  164. package/src/subagent-manager-handle.ts +8 -0
  165. package/src/tool-timing.ts +47 -0
  166. package/src/trajectory-contracts.ts +13 -0
  167. package/src/trajectory-host-handle.ts +16 -0
  168. package/src/trajectory.ts +543 -0
  169. package/src/types.ts +374 -0
  170. package/src/utils.ts +192 -0
  171. package/src/validation.ts +910 -0
  172. package/src/workflow-artifacts.ts +35 -0
  173. package/starter/index.ts +35 -0
  174. package/starter/prompts/council.md +16 -0
  175. package/starter/prompts/deep-research.md +16 -0
  176. package/starter/prompts/oracle.md +14 -0
  177. package/starter/prompts/parallel-review.md +22 -0
  178. package/starter/prompts/parallel-scout.md +16 -0
  179. package/starter/prompts/review-loop.md +14 -0
  180. package/starter/prompts/review.md +14 -0
  181. package/starter/prompts/scout.md +14 -0
  182. package/starter/review-loop.ts +73 -0
  183. package/starter/roles/developer.md +6 -0
  184. package/starter/roles/oracle.md +18 -0
  185. package/starter/roles/researcher.md +20 -0
  186. package/starter/roles/reviewer.md +19 -0
  187. package/starter/roles/scout.md +18 -0
  188. package/subagents/README.md +145 -0
  189. package/subagents/index.ts +8 -0
  190. package/subagents/src/contracts.ts +176 -0
  191. package/subagents/src/decode.ts +158 -0
  192. package/subagents/src/index.ts +194 -0
  193. package/subagents/src/manager.ts +1560 -0
  194. package/subagents/src/navigator.ts +488 -0
  195. package/subagents/src/view.ts +376 -0
  196. package/subagents/src/worktree.ts +99 -0
  197. package/trajectory/index.ts +5 -0
  198. package/trajectory/src/assets/favicon.png +0 -0
  199. package/trajectory/src/assets/index.html +772 -0
  200. package/trajectory/src/assets/marked.min.js +6 -0
  201. package/trajectory/src/assets/morphdom.min.js +1 -0
  202. package/trajectory/src/export.ts +85 -0
  203. package/trajectory/src/index.ts +604 -0
  204. package/trajectory/src/server.ts +447 -0
@@ -0,0 +1,488 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { copyToClipboard, getAgentDir, SettingsManager, type ExtensionAPI, type ExtensionCommandContext, type Theme } from "@earendil-works/pi-coding-agent";
4
+ import { Editor, truncateToWidth, type EditorTheme } from "@earendil-works/pi-tui";
5
+ import { agentActionLabels, deepFreeze, errorText, formatAgentDetail, jsonValue, loadingRegistry, navigatorAttentionSortByState, openWorkflowArtifact, themeWorkflowProgressStyles, visibleStandaloneAgentAttemptActions, workflowPromptArtifact, workflowResultArtifact, type AgentAttemptSummary, type AgentDetailPresentation, type StandaloneAgentAttemptActionContext, type WorkflowArtifact } from "../../src/index.js";
6
+ import { normalizeSubagentRunRequest, type SubagentManager, type SubagentManagerContext, type SubagentProgress, type SubagentRunRequest, type SubagentStatus } from "./contracts.js";
7
+ import { attemptValue, statusValue } from "./decode.js";
8
+ const MAX_DETAIL_TEXT = 4000;
9
+ const MAX_DETAIL_TOOL_CALLS = 32;
10
+
11
+ type NavigatorEntry = {
12
+ readonly status: SubagentStatus;
13
+ readonly request?: SubagentRunRequest;
14
+ readonly requestError?: string;
15
+ };
16
+ type Inspection = { readonly entry: NavigatorEntry; readonly record: Record<string, unknown> };
17
+
18
+ type RegisterCommand = ExtensionAPI["registerCommand"];
19
+
20
+ function objectValue(value: unknown): Record<string, unknown> | undefined { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : undefined; }
21
+ function isFileNotFound(error: unknown): boolean { return objectValue(error)?.code === "ENOENT"; }
22
+ function safeRunId(id: string): boolean { return id !== "." && id !== ".." && /^[A-Za-z0-9._-]+$/.test(id); }
23
+
24
+ function inspectionValue(value: unknown): { status: SubagentStatus; record: Record<string, unknown> } | undefined {
25
+ const record = objectValue(value);
26
+ const status = statusValue(value);
27
+ return record && status ? { status, record } : undefined;
28
+ }
29
+
30
+ function managerContext(context: ExtensionCommandContext, waitForForeground = true, includeAttemptMetadata = false): SubagentManagerContext {
31
+ return { toolCallId: "subagents-command", signal: undefined, onUpdate: undefined, ...(waitForForeground ? {} : { waitForForeground: false }), ...(includeAttemptMetadata ? { includeAttemptMetadata: true } : {}), extensionContext: context };
32
+ }
33
+
34
+ async function loadRequest(storageDirectory: string, id: string): Promise<{ request?: SubagentRunRequest; error?: string }> {
35
+ if (!safeRunId(id)) return {};
36
+ try {
37
+ const value: unknown = JSON.parse(await readFile(join(storageDirectory, id, "request.json"), "utf8"));
38
+ return { request: normalizeSubagentRunRequest(value) };
39
+ } catch (error) {
40
+ if (isFileNotFound(error)) return {};
41
+ return { error: errorText(error) };
42
+ }
43
+ }
44
+
45
+ const ATTENTION_ORDER: Readonly<Record<SubagentStatus["state"], number>> = { running: 0, failed: 1, stopped: 2, completed: 3 };
46
+ function attentionSort(entries: readonly NavigatorEntry[]): NavigatorEntry[] {
47
+ return navigatorAttentionSortByState(entries, (entry) => entry.status.state, (entry) => entry.status.finishedAt, ATTENTION_ORDER);
48
+ }
49
+
50
+ async function loadEntries(manager: SubagentManager, storageDirectory: string, context: ExtensionCommandContext): Promise<NavigatorEntry[]> {
51
+ const value = await manager.inspect({}, managerContext(context));
52
+ if (!Array.isArray(value)) return [];
53
+ const entries: NavigatorEntry[] = [];
54
+ const sessionId = context.sessionManager.getSessionId();
55
+ for (const candidate of value) {
56
+ const status = statusValue(candidate);
57
+ if (!status || status.sessionId !== sessionId) continue;
58
+ const request = await loadRequest(storageDirectory, status.id);
59
+ entries.push({ status, ...(request.request === undefined ? {} : { request: request.request }), ...(request.error === undefined ? {} : { requestError: request.error }) });
60
+ }
61
+ return attentionSort(entries);
62
+ }
63
+
64
+ async function inspectEntry(manager: SubagentManager, storageDirectory: string, entry: NavigatorEntry, context: ExtensionCommandContext): Promise<Inspection> {
65
+ const inspected = inspectionValue(await manager.inspect({ id: entry.status.id }, managerContext(context, true, true)));
66
+ if (!inspected) throw new Error(`Subagent ${entry.status.id} returned an invalid inspection`);
67
+ const request = await loadRequest(storageDirectory, entry.status.id);
68
+ return {
69
+ record: inspected.record,
70
+ entry: { status: inspected.status, ...(request.request === undefined ? {} : { request: request.request }), ...(request.error === undefined ? {} : { requestError: request.error }) },
71
+ };
72
+ }
73
+
74
+ function requestLabel(request: SubagentRunRequest | undefined): string { return request?.label?.trim() || "none"; }
75
+ function requestRole(request: SubagentRunRequest | undefined): string {
76
+ const role: unknown = request?.role;
77
+ if (typeof role === "string" && role.trim()) return role.trim();
78
+ const override = objectValue(role);
79
+ return typeof override?.name === "string" && override.name.trim() ? override.name.trim() : "none";
80
+ }
81
+ function shortId(id: string): string { return id.length > 12 ? id.slice(0, 8) : id; }
82
+ function pickerLabel(entry: NavigatorEntry, index: number): string { return `${String(index + 1)}. label=${boundedText(requestLabel(entry.request), 256)} role=${boundedText(requestRole(entry.request), 256)} [${entry.status.state}] ${shortId(entry.status.id)}`; }
83
+ function boundedText(value: unknown, limit = MAX_DETAIL_TEXT): string {
84
+ const text = typeof value === "string" ? value : (() => { try { const serialized: unknown = JSON.stringify(value); return typeof serialized === "string" ? serialized : String(value); } catch { return String(value); } })();
85
+ return text.length > limit ? `${text.slice(0, limit)}…` : text;
86
+ }
87
+ function timestamp(value: unknown): string | undefined { return typeof value === "number" && Number.isFinite(value) ? new Date(value).toISOString() : undefined; }
88
+
89
+ function appendValue(lines: string[], title: string, value: unknown): void {
90
+ lines.push(`${title}:`);
91
+ for (const line of boundedText(value).split("\n")) lines.push(` ${line}`);
92
+ }
93
+
94
+ function latestAttempt(status: SubagentStatus): AgentAttemptSummary | undefined {
95
+ return [...(status.attemptDetails ?? [])].sort((left, right) => right.attempt - left.attempt)[0];
96
+ }
97
+ function usageAccounting(status: SubagentStatus): SubagentProgress["accounting"] | undefined {
98
+ return status.progress?.accounting;
99
+ }
100
+ function detailPresentation(inspection: Inspection): AgentDetailPresentation {
101
+ const { status, request } = inspection.entry;
102
+ const attempt = latestAttempt(status);
103
+ const state = status.progress?.state;
104
+ const activity = status.progress?.activity;
105
+ const model = state?.model ?? attempt?.setup.model;
106
+ const tools = state?.tools ?? attempt?.setup.tools;
107
+ const lastEventAt = status.progress?.lastEventAt;
108
+ const attempts = status.attempts ?? attempt?.attempt;
109
+ const accounting = usageAccounting(status);
110
+ const role = requestRole(request);
111
+ const error = status.error ?? attempt?.error;
112
+ return {
113
+ state: status.state,
114
+ ...(activity === undefined ? {} : { activity: { kind: activity.kind, text: boundedText(activity.text) } }),
115
+ ...(lastEventAt === undefined ? {} : { lastEventAt }),
116
+ ...(model === undefined ? {} : { model: { provider: boundedText(model.provider, 256), model: boundedText(model.model, 256), ...(model.thinking === undefined ? {} : { thinking: model.thinking }) } }),
117
+ ...(role === "none" ? {} : { role: boundedText(role, 256) }),
118
+ ...(tools === undefined ? {} : { tools: tools.slice(0, 256).map((tool) => boundedText(tool, 256)) }),
119
+ ...(attempts === undefined ? {} : { attempts }),
120
+ ...(status.startedAt === undefined ? {} : { startedAt: status.startedAt }),
121
+ ...(status.finishedAt === undefined ? {} : { finishedAt: status.finishedAt }),
122
+ ...(accounting === undefined ? {} : { accounting }),
123
+ ...(error === undefined ? {} : { error: { code: boundedText(error.code, 256), message: boundedText(error.message) } }),
124
+ };
125
+ }
126
+ function detailLines(inspection: Inspection, theme?: Theme): string[] {
127
+ const { entry, record } = inspection;
128
+ const { status, request } = entry;
129
+ const styles = theme === undefined ? undefined : themeWorkflowProgressStyles(theme);
130
+ const lines = [
131
+ theme?.bold(theme.fg("accent", `Subagent ${boundedText(status.id, 256)}`)) ?? `Subagent ${boundedText(status.id, 256)}`,
132
+ `label=${boundedText(requestLabel(request), 256)} role=${boundedText(requestRole(request), 256)}`,
133
+ ...formatAgentDetail(detailPresentation(inspection), styles, status.finishedAt ?? Date.now()),
134
+ ];
135
+ const startedAt = timestamp(status.startedAt);
136
+ const finishedAt = timestamp(status.finishedAt);
137
+ if (startedAt) lines.push(`startedAt=${startedAt}`);
138
+ if (finishedAt) lines.push(`finishedAt=${finishedAt}`);
139
+ if (request?.prompt) appendValue(lines, "prompt", request.prompt);
140
+ if (entry.requestError) lines.push(`request=unavailable: ${boundedText(entry.requestError)}`);
141
+ if (status.worktree) lines.push(`worktree=${boundedText(status.worktree.path)} branch=${boundedText(status.worktree.branch)}`);
142
+ const toolCalls = status.progress?.toolCalls;
143
+ if (toolCalls?.length) {
144
+ lines.push(`toolCalls=${String(toolCalls.length)}`);
145
+ for (const call of toolCalls.slice(-MAX_DETAIL_TOOL_CALLS)) lines.push(` ${boundedText(call.name, 256)} [${call.state}]`);
146
+ }
147
+ if (Object.prototype.hasOwnProperty.call(record, "value")) appendValue(lines, "value", record.value);
148
+ return lines;
149
+ }
150
+
151
+ function tuiRows(tui: { terminal?: { rows?: number } }): number { return typeof tui.terminal?.rows === "number" && Number.isFinite(tui.terminal.rows) ? tui.terminal.rows : 24; }
152
+ function unrefTimer(timer: ReturnType<typeof setInterval>): void {
153
+ const value: unknown = timer;
154
+ if (typeof value !== "object" || value === null || !("unref" in value) || typeof value.unref !== "function") return;
155
+ Reflect.apply(value.unref, value, []);
156
+ }
157
+
158
+ type NavigatorTui = Parameters<typeof openWorkflowArtifact>[0];
159
+ type DetailResult = { readonly kind: "steer"; readonly message: string } | "retry" | undefined;
160
+ function standaloneActionContext(manager: SubagentManager, inspection: Inspection, context: ExtensionCommandContext): StandaloneAgentAttemptActionContext | undefined {
161
+ const status = inspection.entry.status;
162
+ const request = inspection.entry.request;
163
+ const liveData = manager.getAttemptActionData?.(status.id);
164
+ const attempt = attemptValue(liveData?.attempt) ?? latestAttempt(status);
165
+ if (!attempt) return undefined;
166
+ const session = attempt.session;
167
+ const live = liveData?.liveSession && session && liveData.liveSession.reference.transport === session.transport && liveData.liveSession.reference.sessionId === session.sessionId ? liveData.liveSession : undefined;
168
+ const label = request?.label?.trim();
169
+ const role = requestRole(request);
170
+ const name = label || (role === "none" ? "subagent" : role);
171
+ const ui = {
172
+ notify: (message: string, level: "info" | "warning" | "error" = "info") => { context.ui.notify(message, level); },
173
+ confirm: (title: string, message: string) => context.ui.confirm(title, message),
174
+ select: (title: string, options: readonly string[]) => context.ui.select(title, [...options]),
175
+ input: (title: string, placeholder?: string) => context.ui.input(title, placeholder),
176
+ setWorkingMessage: (message?: string) => { context.ui.setWorkingMessage(message); },
177
+ };
178
+ const actionAttempt = deepFreeze(attempt);
179
+ const actionAgent = deepFreeze({ id: status.id, name, state: status.state, ...(label === undefined ? {} : { label }) });
180
+ return {
181
+ agent: actionAgent,
182
+ attempt: actionAttempt,
183
+ ...(actionAttempt.session === undefined ? {} : { session: actionAttempt.session }),
184
+ ...(live === undefined ? {} : { liveSession: live, ...(liveData?.prepared === undefined ? {} : { prepared: liveData.prepared }), ...(liveData?.handoff === undefined ? {} : { handoff: liveData.handoff }) }),
185
+ signal: liveData?.signal ?? new AbortController().signal,
186
+ ui: Object.freeze(ui),
187
+ };
188
+ }
189
+ function liveSystemPrompt(manager: SubagentManager, status: SubagentStatus): string | undefined {
190
+ return manager.getAttemptActionData?.(status.id)?.prepared?.systemPrompt;
191
+ }
192
+ function actionOptions(manager: SubagentManager, inspection: Inspection, context: ExtensionCommandContext): string[] {
193
+ const actionContext = standaloneActionContext(manager, inspection, context);
194
+ const extensionLabels = actionContext === undefined ? [] : visibleStandaloneAgentAttemptActions(loadingRegistry().agentAttemptActions(), actionContext).map(([, action]) => action.label);
195
+ const value = inspection.record.value;
196
+ return agentActionLabels({
197
+ extensionLabels,
198
+ hasWorktree: inspection.entry.status.worktree !== undefined,
199
+ openPrompt: context.mode === "tui" && inspection.entry.request?.prompt !== undefined,
200
+ openSystemPrompt: context.mode === "tui" && liveSystemPrompt(manager, inspection.entry.status) !== undefined,
201
+ openResult: context.mode === "tui" && Object.prototype.hasOwnProperty.call(inspection.record, "value") && jsonValue(value),
202
+ standaloneState: inspection.entry.status.state,
203
+ });
204
+ }
205
+ function retryResult(value: unknown): { readonly id: string; readonly state: "running" } | undefined {
206
+ const record = objectValue(value);
207
+ return record && typeof record.id === "string" && record.id.trim() && record.state === "running" ? { id: record.id, state: "running" } : undefined;
208
+ }
209
+ async function openNavigatorArtifact(context: ExtensionCommandContext, tui: NavigatorTui, artifact: WorkflowArtifact, label: string): Promise<void> {
210
+ const command = SettingsManager.create(context.cwd, getAgentDir(), { projectTrusted: context.isProjectTrusted() }).getExternalEditorCommand();
211
+ if (!command) { context.ui.notify(`Cannot open ${label}: no external editor is configured.`, "warning"); return; }
212
+ const exitCode = await openWorkflowArtifact(tui, command, artifact);
213
+ if (exitCode !== 0) context.ui.notify(`Cannot open ${label}: external editor ${exitCode === null ? "could not be started" : `exited with code ${String(exitCode)}`}.`, "warning");
214
+ }
215
+ async function steerSubagent(manager: SubagentManager, storageDirectory: string, entry: NavigatorEntry, context: ExtensionCommandContext, message: string): Promise<void> {
216
+ const current = await inspectEntry(manager, storageDirectory, entry, context);
217
+ if (current.entry.status.state !== "running") throw new Error(`Subagent ${entry.status.id} is no longer running`);
218
+ await manager.steer({ id: entry.status.id, message }, managerContext(context));
219
+ context.ui.notify(`Steered subagent ${entry.status.id}.`, "info");
220
+ }
221
+ async function performAction(manager: SubagentManager, storageDirectory: string, entry: NavigatorEntry, action: string, context: ExtensionCommandContext, tui: NavigatorTui | undefined, clipboard: (value: string) => Promise<void>): Promise<"stay" | "retry"> {
222
+ const fresh = await inspectEntry(manager, storageDirectory, entry, context);
223
+ const available = actionOptions(manager, fresh, context);
224
+ if (!available.includes(action)) throw new Error(`Action ${action} is no longer available`);
225
+ const actionContext = standaloneActionContext(manager, fresh, context);
226
+ const registered = actionContext === undefined ? undefined : visibleStandaloneAgentAttemptActions(loadingRegistry().agentAttemptActions(), actionContext).find(([, candidate]) => candidate.label === action);
227
+ if (registered) {
228
+ if (!actionContext || registered[1].runStandalone === undefined) throw new Error(`Action ${action} has no standalone implementation`);
229
+ await registered[1].runStandalone(actionContext);
230
+ return "stay";
231
+ }
232
+ if (action === "Copy agent ID") { await clipboard(fresh.entry.status.id); context.ui.notify("Copied agent ID.", "info"); return "stay"; }
233
+ if (action === "Copy branch" && fresh.entry.status.worktree) { await clipboard(fresh.entry.status.worktree.branch); context.ui.notify("Copied branch.", "info"); return "stay"; }
234
+ if (action === "Copy worktree path" && fresh.entry.status.worktree) { await clipboard(fresh.entry.status.worktree.path); context.ui.notify("Copied worktree path.", "info"); return "stay"; }
235
+ if (action === "Open prompt in editor" && tui && fresh.entry.request?.prompt !== undefined) { await openNavigatorArtifact(context, tui, workflowPromptArtifact(fresh.entry.request.prompt), "agent prompt"); return "stay"; }
236
+ if (action === "Open system prompt in editor" && tui) { const systemPrompt = liveSystemPrompt(manager, fresh.entry.status); if (systemPrompt !== undefined) { await openNavigatorArtifact(context, tui, workflowPromptArtifact(systemPrompt), "agent system prompt"); return "stay"; } }
237
+ if (action === "Open result in editor" && tui && Object.prototype.hasOwnProperty.call(fresh.record, "value") && jsonValue(fresh.record.value)) { await openNavigatorArtifact(context, tui, workflowResultArtifact(fresh.record.value), "agent result"); return "stay"; }
238
+ if (action === "Steer") {
239
+ const message = await context.ui.input("Steer subagent", "Message for the running subagent");
240
+ if (message === undefined) return "stay";
241
+ await steerSubagent(manager, storageDirectory, entry, context, message);
242
+ return "stay";
243
+ }
244
+ if (action === "Stop") {
245
+ const current = await inspectEntry(manager, storageDirectory, entry, context);
246
+ if (current.entry.status.state !== "running") throw new Error(`Subagent ${entry.status.id} is no longer running`);
247
+ await manager.stop({ id: entry.status.id }, managerContext(context));
248
+ context.ui.notify(`Stopped subagent ${entry.status.id}.`, "info");
249
+ return "stay";
250
+ }
251
+ if (action === "Retry") {
252
+ const current = await inspectEntry(manager, storageDirectory, entry, context);
253
+ if (current.entry.status.state !== "failed" && current.entry.status.state !== "stopped") throw new Error(`Subagent ${entry.status.id} is no longer retryable`);
254
+ const result = retryResult(await manager.retry({ id: entry.status.id }, managerContext(context, false)));
255
+ if (!result) throw new Error("Retry returned an invalid subagent result");
256
+ context.ui.notify(`Retried subagent ${entry.status.id} as ${result.id}.`, "info");
257
+ return "retry";
258
+ }
259
+ return "stay";
260
+ }
261
+ async function showDetail(manager: SubagentManager, storageDirectory: string, entry: NavigatorEntry, context: ExtensionCommandContext, clipboard: (value: string) => Promise<void>): Promise<"exit" | undefined> {
262
+ let inspection = await inspectEntry(manager, storageDirectory, entry, context);
263
+ if (context.mode !== "tui") {
264
+ for (;;) {
265
+ const action = await context.ui.select(detailLines(inspection).join("\n"), actionOptions(manager, inspection, context));
266
+ if (!action || action === "Back") return;
267
+ try {
268
+ if (await performAction(manager, storageDirectory, entry, action, context, undefined, clipboard) === "retry") return;
269
+ inspection = await inspectEntry(manager, storageDirectory, entry, context);
270
+ } catch (error) {
271
+ context.ui.notify(`Cannot ${action.toLowerCase()}: ${errorText(error)}`, "warning");
272
+ }
273
+ }
274
+ }
275
+ const result = await context.ui.custom<DetailResult>((tui, theme, keybindings, done) => {
276
+ let offset = 0;
277
+ let actionMode = false;
278
+ let actionIndex = 0;
279
+ let actionRunning = false;
280
+ let disposed = false;
281
+ let steerMode = false;
282
+ const editorTheme: EditorTheme = {
283
+ borderColor: (text) => theme.fg("accent", text),
284
+ selectList: {
285
+ selectedPrefix: (text) => theme.fg("accent", text),
286
+ selectedText: (text) => theme.fg("accent", text),
287
+ description: (text) => theme.fg("muted", text),
288
+ scrollInfo: (text) => theme.fg("dim", text),
289
+ noMatch: (text) => theme.fg("warning", text),
290
+ },
291
+ };
292
+ const steerEditor = new Editor(tui, editorTheme);
293
+ let refreshTimer: ReturnType<typeof setInterval> | undefined;
294
+ let refreshing = false;
295
+ let refreshGeneration = 0;
296
+ const requestRender = (): void => {
297
+ if (!disposed) tui.requestRender();
298
+ };
299
+ const stopRefresh = (): void => {
300
+ if (refreshTimer !== undefined) {
301
+ clearInterval(refreshTimer);
302
+ refreshTimer = undefined;
303
+ }
304
+ refreshGeneration += 1;
305
+ };
306
+ const buildActionRows = (detail: readonly string[], options: readonly string[]): string[] => [
307
+ ...detail,
308
+ "",
309
+ theme.bold("Agent actions"),
310
+ ...options.map((option, index) => `${index === actionIndex ? "→ " : " "}${index === actionIndex ? theme.fg("accent", option) : option}`),
311
+ ];
312
+ const viewportRows = (): number => Math.max(1, tuiRows(tui) - 1);
313
+ const maxOffsetFor = (rows: readonly string[]): number => Math.max(0, rows.length - viewportRows());
314
+ const clampOffset = (rows: readonly string[]): void => {
315
+ offset = Math.max(0, Math.min(maxOffsetFor(rows), offset));
316
+ };
317
+ const actionView = (): { readonly detail: string[]; readonly options: string[]; readonly rows: string[] } => {
318
+ const detail = detailLines(inspection, theme);
319
+ const options = actionOptions(manager, inspection, context);
320
+ return { detail, options, rows: buildActionRows(detail, options) };
321
+ };
322
+ const scrollActionIntoView = (view = actionView()): void => {
323
+ const { options, rows } = view;
324
+ if (actionIndex >= options.length) { clampOffset(rows); return; }
325
+ const viewport = viewportRows();
326
+ const actionRow = rows.length - options.length + actionIndex;
327
+ if (actionRow < offset) offset = actionRow;
328
+ else if (actionRow >= offset + viewport) offset = actionRow - viewport + 1;
329
+ clampOffset(rows);
330
+ };
331
+ const reportRefreshError = (error: unknown): void => {
332
+ if (disposed) return;
333
+ try { context.ui.notify(`Cannot refresh subagent ${entry.status.id}: ${errorText(error)}`, "warning"); } catch { /* The session UI may already be closing. */ }
334
+ };
335
+ const refreshInspection = async (): Promise<void> => {
336
+ if (disposed || actionRunning || refreshing || inspection.entry.status.state !== "running") return;
337
+ refreshing = true;
338
+ const generation = ++refreshGeneration;
339
+ const actionIndexBeforeRefresh = actionIndex;
340
+ const viewBeforeRefresh = actionMode ? actionView() : undefined;
341
+ const actionRowBeforeRefresh = viewBeforeRefresh === undefined ? -1 : viewBeforeRefresh.rows.length - viewBeforeRefresh.options.length + actionIndex;
342
+ const wasVisible = actionRowBeforeRefresh >= offset && actionRowBeforeRefresh < offset + viewportRows();
343
+ try {
344
+ const next = await inspectEntry(manager, storageDirectory, entry, context);
345
+ if (generation !== refreshGeneration) return;
346
+ inspection = next;
347
+ if (inspection.entry.status.state !== "running") stopRefresh();
348
+ if (actionMode) {
349
+ const options = actionOptions(manager, inspection, context);
350
+ actionIndex = Math.min(actionIndex, Math.max(0, options.length - 1));
351
+ const view = actionView();
352
+ if (wasVisible || actionIndex !== actionIndexBeforeRefresh) scrollActionIntoView(view);
353
+ else clampOffset(view.rows);
354
+ }
355
+ requestRender();
356
+ } catch (error: unknown) {
357
+ if (generation === refreshGeneration) reportRefreshError(error);
358
+ } finally {
359
+ refreshing = false;
360
+ }
361
+ };
362
+ const close = (value: DetailResult): void => {
363
+ if (disposed) return;
364
+ disposed = true;
365
+ stopRefresh();
366
+ done(value);
367
+ };
368
+ steerEditor.onSubmit = (value) => {
369
+ const message = value.trim();
370
+ if (message) close({ kind: "steer", message });
371
+ };
372
+ const isDisposed = (): boolean => disposed;
373
+ const renderLines = (width: number): string[] => {
374
+ if (disposed) return [];
375
+ const renderWidth = Math.max(1, width);
376
+ const action = actionMode ? actionView() : undefined;
377
+ const detail = (action?.detail ?? detailLines(inspection, theme)).map((line) => truncateToWidth(line, renderWidth, "…"));
378
+ const rows = steerMode ? [...detail, "", theme.bold("Steer subagent"), ...steerEditor.render(renderWidth)] : action?.rows ?? detail;
379
+ const viewport = viewportRows();
380
+ const maxOffset = maxOffsetFor(rows);
381
+ const visibleOffset = steerMode ? maxOffset : Math.max(0, Math.min(maxOffset, offset));
382
+ const hint = theme.fg("dim", steerMode ? "enter submit · esc back" : actionMode ? "↑/↓ actions · enter run · esc back" : "↑/↓ scroll · a actions · enter actions · esc back");
383
+ return [...rows.slice(visibleOffset, visibleOffset + viewport), hint].map((line) => truncateToWidth(line, renderWidth, "…"));
384
+ };
385
+ const runAction = (action: string): void => {
386
+ if (disposed) return;
387
+ actionRunning = true;
388
+ requestRender();
389
+ void performAction(manager, storageDirectory, entry, action, context, tui, clipboard).then(async (outcome) => {
390
+ if (disposed) return;
391
+ if (outcome === "retry") { close("retry"); return; }
392
+ const next = await inspectEntry(manager, storageDirectory, entry, context);
393
+ if (isDisposed()) return;
394
+ inspection = next;
395
+ if (inspection.entry.status.state !== "running") stopRefresh();
396
+ actionMode = false;
397
+ actionIndex = 0;
398
+ offset = 0;
399
+ }).catch((error: unknown) => {
400
+ if (!disposed) context.ui.notify(`Cannot ${action.toLowerCase()}: ${errorText(error)}`, "warning");
401
+ }).finally(() => {
402
+ actionRunning = false;
403
+ if (!disposed) requestRender();
404
+ });
405
+ };
406
+ if (inspection.entry.status.state === "running") {
407
+ refreshTimer = setInterval(() => { void refreshInspection().catch(reportRefreshError); }, 1000);
408
+ unrefTimer(refreshTimer);
409
+ }
410
+ return {
411
+ render: renderLines,
412
+ invalidate() {},
413
+ handleInput(data: string) {
414
+ if (disposed || actionRunning) return;
415
+ if (steerMode) {
416
+ if (keybindings.matches(data, "tui.select.cancel")) { steerMode = false; actionMode = true; steerEditor.setText(""); offset = 0; scrollActionIntoView(); }
417
+ else steerEditor.handleInput(data);
418
+ requestRender();
419
+ return;
420
+ }
421
+ if (!actionMode && data === "a") { actionMode = true; actionIndex = 0; offset = 0; scrollActionIntoView(); requestRender(); return; }
422
+ if (keybindings.matches(data, "tui.select.cancel")) {
423
+ if (actionMode) { actionMode = false; actionIndex = 0; offset = 0; requestRender(); } else close(undefined);
424
+ return;
425
+ }
426
+ if (!actionMode) {
427
+ if (keybindings.matches(data, "tui.select.confirm")) { actionMode = true; actionIndex = 0; offset = 0; scrollActionIntoView(); requestRender(); }
428
+ else if (keybindings.matches(data, "tui.select.up")) { offset = Math.max(0, offset - 1); clampOffset(detailLines(inspection, theme)); requestRender(); }
429
+ else if (keybindings.matches(data, "tui.select.down")) { offset += 1; clampOffset(detailLines(inspection, theme)); requestRender(); }
430
+ return;
431
+ }
432
+ const options = actionOptions(manager, inspection, context);
433
+ if (keybindings.matches(data, "tui.select.up")) { actionIndex = (actionIndex + options.length - 1) % options.length; scrollActionIntoView(); }
434
+ else if (keybindings.matches(data, "tui.select.down")) { actionIndex = (actionIndex + 1) % options.length; scrollActionIntoView(); }
435
+ else if (keybindings.matches(data, "tui.select.confirm")) { const action = options[actionIndex]; if (action === "Steer") { steerMode = true; actionMode = false; steerEditor.setText(""); offset = 0; } else if (action && action !== "Back") runAction(action); else actionMode = false; }
436
+ else if (keybindings.matches(data, "tui.select.pageUp")) { offset = Math.max(0, offset - viewportRows()); clampOffset(actionView().rows); }
437
+ else if (keybindings.matches(data, "tui.select.pageDown")) { offset += viewportRows(); clampOffset(actionView().rows); }
438
+ requestRender();
439
+ },
440
+ dispose() { if (!disposed) { disposed = true; stopRefresh(); } },
441
+ };
442
+ });
443
+ if (!result || result === "retry") return;
444
+ try { await steerSubagent(manager, storageDirectory, entry, context, result.message); }
445
+ catch (error) { context.ui.notify(`Cannot steer: ${errorText(error)}`, "warning"); }
446
+ return "exit";
447
+ }
448
+
449
+ async function runNavigator(manager: SubagentManager, storageDirectory: string, args: string, context: ExtensionCommandContext, clipboard: (value: string) => Promise<void>): Promise<void> {
450
+ if (args.trim()) {
451
+ context.ui.notify("Subagent slash commands do not accept arguments. Open the picker with /subagents to inspect and control current-session runs.", "warning");
452
+ return;
453
+ }
454
+ for (;;) {
455
+ const entries = await loadEntries(manager, storageDirectory, context);
456
+ if (!entries.length) {
457
+ context.ui.notify("No durable subagent runs in this session.", "info");
458
+ return;
459
+ }
460
+ if (!context.hasUI) {
461
+ context.ui.notify(entries.map((entry, index) => pickerLabel(entry, index)).join("\n"), "info");
462
+ return;
463
+ }
464
+ const labels = entries.map(pickerLabel);
465
+ const choice = await context.ui.select("Subagents\n", [...labels, "Close"]);
466
+ if (!choice || choice === "Close") return;
467
+ const selected = entries[labels.indexOf(choice)];
468
+ if (!selected) return;
469
+ try {
470
+ if (await showDetail(manager, storageDirectory, selected, context, clipboard) === "exit") return;
471
+ } catch (error) {
472
+ context.ui.notify(`Cannot inspect subagent ${selected.status.id}: ${errorText(error)}`, "warning");
473
+ }
474
+ }
475
+ }
476
+
477
+ export function registerSubagentNavigator(registerCommand: RegisterCommand, manager: SubagentManager, storageDirectory: string, clipboard: (value: string) => Promise<void> = copyToClipboard): void {
478
+ registerCommand("subagents", {
479
+ description: "Open the durable subagent picker and inspect run status",
480
+ handler: async (args, context) => {
481
+ try {
482
+ await runNavigator(manager, storageDirectory, args, context, clipboard);
483
+ } catch (error) {
484
+ context.ui.notify(`Cannot inspect subagents: ${errorText(error)}`, "warning");
485
+ }
486
+ },
487
+ });
488
+ }