@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.17.0-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 (71) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/src/agent-execution.js +36 -51
  3. package/dist/src/background-widget.d.ts +56 -0
  4. package/dist/src/background-widget.js +22 -15
  5. package/dist/src/decoders.js +2 -1
  6. package/dist/src/host-navigator.d.ts +18 -0
  7. package/dist/src/host-navigator.js +71 -13
  8. package/dist/src/host-view.d.ts +16 -1
  9. package/dist/src/host-view.js +109 -42
  10. package/dist/src/host.js +57 -24
  11. package/dist/src/index.d.ts +3 -1
  12. package/dist/src/index.js +867 -545
  13. package/dist/src/index.js.map +3 -3
  14. package/dist/src/registry.js +2 -1
  15. package/dist/src/roles.d.ts +70 -0
  16. package/dist/src/roles.js +325 -0
  17. package/dist/src/settings.d.ts +13 -0
  18. package/dist/src/settings.js +253 -0
  19. package/dist/src/trajectory.d.ts +29 -4
  20. package/dist/src/trajectory.js +58 -3
  21. package/dist/src/types.d.ts +0 -1
  22. package/dist/src/utils.d.ts +2 -1
  23. package/dist/src/utils.js +10 -0
  24. package/dist/src/validation.d.ts +5 -17
  25. package/dist/src/validation.js +9 -458
  26. package/dist/subagents/index.js +1132 -694
  27. package/dist/subagents/index.js.map +3 -3
  28. package/dist/subagents/src/contracts.d.ts +4 -0
  29. package/dist/subagents/src/index.d.ts +1 -1
  30. package/dist/subagents/src/index.js +2 -1
  31. package/dist/subagents/src/manager.js +29 -10
  32. package/dist/subagents/src/navigator.js +396 -247
  33. package/dist/subagents/src/view.d.ts +23 -2
  34. package/dist/subagents/src/view.js +96 -30
  35. package/dist/trajectory/assets/index.html +347 -231
  36. package/dist/trajectory/assets/prism.min.js +13 -0
  37. package/dist/trajectory/index.js +377 -257
  38. package/dist/trajectory/index.js.map +3 -3
  39. package/dist/trajectory/src/assets/index.html +347 -231
  40. package/dist/trajectory/src/assets/prism.min.js +13 -0
  41. package/dist/trajectory/src/export.js +4 -2
  42. package/dist/trajectory/src/index.d.ts +2 -0
  43. package/dist/trajectory/src/index.js +143 -42
  44. package/dist/trajectory/src/server.js +134 -9
  45. package/dist/trajectory/src/server.js.map +1 -1
  46. package/package.json +2 -1
  47. package/src/agent-execution.ts +36 -42
  48. package/src/background-widget.ts +25 -18
  49. package/src/decoders.ts +2 -1
  50. package/src/host-navigator.ts +58 -14
  51. package/src/host-view.ts +93 -37
  52. package/src/host.ts +51 -21
  53. package/src/index.ts +3 -1
  54. package/src/registry.ts +2 -1
  55. package/src/roles.ts +326 -0
  56. package/src/settings.ts +206 -0
  57. package/src/trajectory.ts +76 -7
  58. package/src/types.ts +1 -1
  59. package/src/utils.ts +8 -1
  60. package/src/validation.ts +12 -356
  61. package/subagents/README.md +1 -1
  62. package/subagents/src/contracts.ts +4 -0
  63. package/subagents/src/index.ts +3 -2
  64. package/subagents/src/manager.ts +28 -10
  65. package/subagents/src/navigator.ts +336 -180
  66. package/subagents/src/view.ts +97 -28
  67. package/trajectory/src/assets/index.html +347 -231
  68. package/trajectory/src/assets/prism.min.js +13 -0
  69. package/trajectory/src/export.ts +4 -2
  70. package/trajectory/src/index.ts +118 -38
  71. package/trajectory/src/server.ts +112 -12
@@ -1,12 +1,11 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { copyToClipboard, getAgentDir, SettingsManager, type ExtensionAPI, type ExtensionCommandContext, type Theme } from "@earendil-works/pi-coding-agent";
3
+ import { copyToClipboard, getAgentDir, SettingsManager, type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
4
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";
5
+ import { agentActionLabels, createInlineConfirm, deepFreeze, errorText, formatAgentDetail, formatAgentError, formatCost, formatNavigatorColumns, formatWorkflowRuntime, jsonValue, loadingRegistry, navigatorAttentionSortByState, openWorkflowArtifact, PLAIN_WORKFLOW_PROGRESS_STYLES, progressStyleForState, runStateGlyph, themeWorkflowProgressStyles, visibleStandaloneAgentAttemptActions, workflowKeyLabel, workflowKeyMatches, workflowPromptArtifact, workflowResultArtifact, type AgentAttemptSummary, type AgentDetailPresentation, type StandaloneAgentAttemptActionContext, type WorkflowArtifact, type WorkflowProgressStyles } from "../../src/index.js";
6
6
  import { normalizeSubagentRunRequest, type SubagentManager, type SubagentManagerContext, type SubagentProgress, type SubagentRunRequest, type SubagentStatus } from "./contracts.js";
7
7
  import { attemptValue, statusValue } from "./decode.js";
8
8
  const MAX_DETAIL_TEXT = 4000;
9
- const MAX_DETAIL_TOOL_CALLS = 32;
10
9
 
11
10
  type NavigatorEntry = {
12
11
  readonly status: SubagentStatus;
@@ -27,8 +26,9 @@ function inspectionValue(value: unknown): { status: SubagentStatus; record: Reco
27
26
  return record && status ? { status, record } : undefined;
28
27
  }
29
28
 
29
+ // Detail inspections only feed this UI, so they carry the live activity that tool results leave out.
30
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 };
31
+ return { toolCallId: "subagents-command", signal: undefined, onUpdate: undefined, ...(waitForForeground ? {} : { waitForForeground: false }), ...(includeAttemptMetadata ? { includeAttemptMetadata: true, includeActivity: true } : {}), extensionContext: context };
32
32
  }
33
33
 
34
34
  async function loadRequest(storageDirectory: string, id: string): Promise<{ request?: SubagentRunRequest; error?: string }> {
@@ -71,7 +71,6 @@ async function inspectEntry(manager: SubagentManager, storageDirectory: string,
71
71
  };
72
72
  }
73
73
 
74
- function requestLabel(request: SubagentRunRequest | undefined): string { return request?.label?.trim() || "none"; }
75
74
  function requestRole(request: SubagentRunRequest | undefined): string {
76
75
  const role: unknown = request?.role;
77
76
  if (typeof role === "string" && role.trim()) return role.trim();
@@ -79,17 +78,33 @@ function requestRole(request: SubagentRunRequest | undefined): string {
79
78
  return typeof override?.name === "string" && override.name.trim() ? override.name.trim() : "none";
80
79
  }
81
80
  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)}`; }
81
+ function entryName(entry: NavigatorEntry): string {
82
+ const role = requestRole(entry.request);
83
+ return boundedText(entry.request?.label?.trim() || (role === "none" ? shortId(entry.status.id) : role), 256);
84
+ }
85
+ /** Names for a list of runs; a repeated name carries its short ID, as in the `/workflow` picker, so every row stays unique. */
86
+ function listNames(entries: readonly NavigatorEntry[]): string[] {
87
+ const names = entries.map(entryName);
88
+ return entries.map(({ status }, index) => {
89
+ const name = names[index] ?? "";
90
+ return names.indexOf(name) === names.lastIndexOf(name) ? name : `${name} ${shortId(status.id)}`;
91
+ });
92
+ }
93
+ /** Picker rows in the `/workflow` style. */
94
+ function pickerLabels(entries: readonly NavigatorEntry[]): string[] {
95
+ const names = listNames(entries);
96
+ return entries.map(({ status }, index) => {
97
+ const name = names[index] ?? "";
98
+ const model = status.progress?.state?.model;
99
+ const cost = formatCost(status.progress?.accounting.cost);
100
+ const runtime = status.startedAt === undefined ? "" : ` runtime=${formatWorkflowRuntime((status.finishedAt ?? Date.now()) - status.startedAt)}`;
101
+ return `${runStateGlyph(status.state, "⠦")} ${name} ${status.state}${model ? ` ${boundedText(model.model, 256)}${model.thinking ? `:${model.thinking}` : ""}` : ""}${cost ? ` ${cost}` : ""}${runtime}`;
102
+ });
103
+ }
83
104
  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); } })();
105
+ const text = typeof value === "string" ? value : (() => { try { const serialized: unknown = JSON.stringify(value, null, 2); return typeof serialized === "string" ? serialized : String(value); } catch { return String(value); } })();
85
106
  return text.length > limit ? `${text.slice(0, limit)}…` : text;
86
107
  }
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
108
 
94
109
  function latestAttempt(status: SubagentStatus): AgentAttemptSummary | undefined {
95
110
  return [...(status.attemptDetails ?? [])].sort((left, right) => right.attempt - left.attempt)[0];
@@ -123,29 +138,32 @@ function detailPresentation(inspection: Inspection): AgentDetailPresentation {
123
138
  ...(error === undefined ? {} : { error: { code: boundedText(error.code, 256), message: boundedText(error.message) } }),
124
139
  };
125
140
  }
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()),
141
+ type DetailMenu = "hint" | { readonly options: readonly string[]; readonly index: number } | undefined;
142
+ /** Details of the selected run in the order of a /workflow agent: fields, hint, error, then the action menu. The prompt and result open in the editor. */
143
+ function detailRows(inspection: Inspection, styles: WorkflowProgressStyles, menu?: DetailMenu): string[] {
144
+ const { entry } = inspection;
145
+ const presentation = detailPresentation(inspection);
146
+ return [
147
+ styles.bold(`Selected subagent: ${entryName(entry)}`),
148
+ `ID: ${boundedText(entry.status.id, 256)}`,
149
+ ...formatAgentDetail(presentation, styles, entry.status.finishedAt ?? Date.now(), { includeError: false }),
150
+ ...(entry.requestError === undefined ? [] : [styles.warning(`Request unavailable: ${boundedText(entry.requestError)}`)]),
151
+ ...(menu === "hint" ? [styles.muted("enter agent actions")] : []),
152
+ ...(presentation.error === undefined ? [] : [formatAgentError(presentation.error, styles)]),
153
+ ...(typeof menu === "object" ? [styles.bold("Agent actions"), ...menu.options.map((option, index) => index === menu.index ? `→ ${styles.accent(option)}` : ` ${option}`)] : []),
134
154
  ];
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;
155
+ }
156
+ function listRows(entries: readonly NavigatorEntry[], selectedId: string, styles: WorkflowProgressStyles): string[] {
157
+ const names = listNames(entries);
158
+ return [styles.bold("Runs"), ...entries.map((entry, index) => `${entry.status.id === selectedId ? "→" : " "} • ${names[index] ?? ""} · ${progressStyleForState(entry.status.state, styles)(runStateGlyph(entry.status.state, "⠦"))}`)];
159
+ }
160
+ function headerRows(entries: readonly NavigatorEntry[], styles: WorkflowProgressStyles): string[] {
161
+ const counts = (["running", "failed", "stopped", "completed"] as const).flatMap((state) => {
162
+ const count = entries.filter((entry) => entry.status.state === state).length;
163
+ return count ? [`${String(count)} ${state}`] : [];
164
+ });
165
+ const cost = formatCost(entries.reduce((sum, entry) => sum + (entry.status.progress?.accounting.cost ?? 0), 0));
166
+ return [styles.bold(styles.accent("Subagents")), [...counts, ...(cost ? [cost] : [])].join(" · ")];
149
167
  }
150
168
 
151
169
  function tuiRows(tui: { terminal?: { rows?: number } }): number { return typeof tui.terminal?.rows === "number" && Number.isFinite(tui.terminal.rows) ? tui.terminal.rows : 24; }
@@ -156,7 +174,9 @@ function unrefTimer(timer: ReturnType<typeof setInterval>): void {
156
174
  }
157
175
 
158
176
  type NavigatorTui = Parameters<typeof openWorkflowArtifact>[0];
159
- type DetailResult = { readonly kind: "steer"; readonly message: string } | "retry" | undefined;
177
+ type DashboardResult = { readonly entry: NavigatorEntry; readonly message: string } | undefined;
178
+ // Pi keeps its footer below a custom component; the dashboard leaves room for it as /workflow does.
179
+ const DASHBOARD_FOOTER_ROWS = 2;
160
180
  function standaloneActionContext(manager: SubagentManager, inspection: Inspection, context: ExtensionCommandContext): StandaloneAgentAttemptActionContext | undefined {
161
181
  const status = inspection.entry.status;
162
182
  const request = inspection.entry.request;
@@ -189,11 +209,19 @@ function standaloneActionContext(manager: SubagentManager, inspection: Inspectio
189
209
  function liveSystemPrompt(manager: SubagentManager, status: SubagentStatus): string | undefined {
190
210
  return manager.getAttemptActionData?.(status.id)?.prepared?.systemPrompt;
191
211
  }
192
- function actionOptions(manager: SubagentManager, inspection: Inspection, context: ExtensionCommandContext): string[] {
212
+ type BulkDeleteState = "completed" | "failed";
213
+ const BULK_DELETE: Readonly<Record<string, BulkDeleteState>> = { "Delete all completed": "completed", "Delete all failed": "failed" };
214
+ /** The /workflow picker's bulk deletions, offered for the states present in the list. */
215
+ function bulkDeleteLabels(manager: SubagentManager, entries: readonly NavigatorEntry[]): string[] {
216
+ if (manager.delete === undefined) return [];
217
+ return Object.entries(BULK_DELETE).filter(([, state]) => entries.some((entry) => entry.status.state === state)).map(([label]) => label);
218
+ }
219
+ /** Agent actions plus the /workflow run actions that apply to a standalone run: Delete and Copy run path, before Copy agent ID. */
220
+ function actionOptions(manager: SubagentManager, inspection: Inspection, context: ExtensionCommandContext, bulk: readonly string[] = []): string[] {
193
221
  const actionContext = standaloneActionContext(manager, inspection, context);
194
222
  const extensionLabels = actionContext === undefined ? [] : visibleStandaloneAgentAttemptActions(loadingRegistry().agentAttemptActions(), actionContext).map(([, action]) => action.label);
195
223
  const value = inspection.record.value;
196
- return agentActionLabels({
224
+ const labels = agentActionLabels({
197
225
  extensionLabels,
198
226
  hasWorktree: inspection.entry.status.worktree !== undefined,
199
227
  openPrompt: context.mode === "tui" && inspection.entry.request?.prompt !== undefined,
@@ -201,6 +229,27 @@ function actionOptions(manager: SubagentManager, inspection: Inspection, context
201
229
  openResult: context.mode === "tui" && Object.prototype.hasOwnProperty.call(inspection.record, "value") && jsonValue(value),
202
230
  standaloneState: inspection.entry.status.state,
203
231
  });
232
+ const runActions = [...(manager.delete !== undefined && inspection.entry.status.state !== "running" ? ["Delete"] : []), ...bulk, ...(context.mode === "tui" ? ["Copy run path"] : [])];
233
+ labels.splice(labels.indexOf("Copy agent ID"), 0, ...runActions);
234
+ return labels;
235
+ }
236
+ type Confirm = (title: string, message: string) => Promise<boolean>;
237
+ async function deleteAll(manager: SubagentManager, storageDirectory: string, context: ExtensionCommandContext, state: BulkDeleteState, confirm: Confirm): Promise<boolean> {
238
+ if (!await confirm(`Delete ${state} runs?`, `Delete all ${state} subagent runs and their records? This cannot be undone.`)) return false;
239
+ const skipped: string[] = [];
240
+ let deleted = 0;
241
+ for (const entry of await loadEntries(manager, storageDirectory, context)) {
242
+ if (entry.status.state !== state) continue;
243
+ try {
244
+ await manager.delete?.({ id: entry.status.id }, managerContext(context));
245
+ deleted += 1;
246
+ } catch (error) {
247
+ skipped.push(`${entry.status.id} (${errorText(error)})`);
248
+ }
249
+ }
250
+ if (skipped.length) context.ui.notify(`Skipped ${state} runs: ${skipped.join(", ")}.`, "warning");
251
+ if (deleted) context.ui.notify(`Deleted ${String(deleted)} ${state} subagent run(s).`, "info");
252
+ return true;
204
253
  }
205
254
  function retryResult(value: unknown): { readonly id: string; readonly state: "running" } | undefined {
206
255
  const record = objectValue(value);
@@ -218,7 +267,9 @@ async function steerSubagent(manager: SubagentManager, storageDirectory: string,
218
267
  await manager.steer({ id: entry.status.id, message }, managerContext(context));
219
268
  context.ui.notify(`Steered subagent ${entry.status.id}.`, "info");
220
269
  }
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"> {
270
+ /** `unchanged` leaves the menu where it was, as /workflow does after an editor or copy action: the user declined, or the action does not touch the run. */
271
+ type ActionOutcome = "stay" | "unchanged" | "deleted" | { readonly retryId: string };
272
+ async function performAction(manager: SubagentManager, storageDirectory: string, entry: NavigatorEntry, action: string, context: ExtensionCommandContext, tui: NavigatorTui | undefined, clipboard: (value: string) => Promise<void>, confirm: Confirm): Promise<ActionOutcome> {
222
273
  const fresh = await inspectEntry(manager, storageDirectory, entry, context);
223
274
  const available = actionOptions(manager, fresh, context);
224
275
  if (!available.includes(action)) throw new Error(`Action ${action} is no longer available`);
@@ -229,19 +280,27 @@ async function performAction(manager: SubagentManager, storageDirectory: string,
229
280
  await registered[1].runStandalone(actionContext);
230
281
  return "stay";
231
282
  }
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"; }
283
+ if (action === "Copy agent ID") { await clipboard(fresh.entry.status.id); context.ui.notify("Copied agent ID.", "info"); return "unchanged"; }
284
+ if (action === "Copy run path") { await clipboard(join(storageDirectory, fresh.entry.status.id)); context.ui.notify("Copied run path.", "info"); return "unchanged"; }
285
+ if (action === "Delete" && manager.delete !== undefined) {
286
+ if (!await confirm("Delete subagent?", `Delete ${entryName(fresh.entry)} (${fresh.entry.status.id}) and its record? This cannot be undone.`)) return "unchanged";
287
+ await manager.delete({ id: fresh.entry.status.id }, managerContext(context));
288
+ context.ui.notify(`Deleted subagent ${fresh.entry.status.id}.`, "info");
289
+ return "deleted";
290
+ }
291
+ if (action === "Copy branch" && fresh.entry.status.worktree) { await clipboard(fresh.entry.status.worktree.branch); context.ui.notify("Copied branch.", "info"); return "unchanged"; }
292
+ if (action === "Copy worktree path" && fresh.entry.status.worktree) { await clipboard(fresh.entry.status.worktree.path); context.ui.notify("Copied worktree path.", "info"); return "unchanged"; }
293
+ if (action === "Open prompt in editor" && tui && fresh.entry.request?.prompt !== undefined) { await openNavigatorArtifact(context, tui, workflowPromptArtifact(fresh.entry.request.prompt), "agent prompt"); return "unchanged"; }
294
+ 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 "unchanged"; } }
295
+ 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 "unchanged"; }
238
296
  if (action === "Steer") {
239
297
  const message = await context.ui.input("Steer subagent", "Message for the running subagent");
240
- if (message === undefined) return "stay";
298
+ if (message === undefined) return "unchanged";
241
299
  await steerSubagent(manager, storageDirectory, entry, context, message);
242
300
  return "stay";
243
301
  }
244
302
  if (action === "Stop") {
303
+ if (!await confirm("Stop subagent?", `Stop subagent ${entryName(fresh.entry)} (${fresh.entry.status.id})? This cannot be undone.`)) return "unchanged";
245
304
  const current = await inspectEntry(manager, storageDirectory, entry, context);
246
305
  if (current.entry.status.state !== "running") throw new Error(`Subagent ${entry.status.id} is no longer running`);
247
306
  await manager.stop({ id: entry.status.id }, managerContext(context));
@@ -254,31 +313,37 @@ async function performAction(manager: SubagentManager, storageDirectory: string,
254
313
  const result = retryResult(await manager.retry({ id: entry.status.id }, managerContext(context, false)));
255
314
  if (!result) throw new Error("Retry returned an invalid subagent result");
256
315
  context.ui.notify(`Retried subagent ${entry.status.id} as ${result.id}.`, "info");
257
- return "retry";
316
+ return { retryId: result.id };
258
317
  }
259
318
  return "stay";
260
319
  }
261
- async function showDetail(manager: SubagentManager, storageDirectory: string, entry: NavigatorEntry, context: ExtensionCommandContext, clipboard: (value: string) => Promise<void>): Promise<"exit" | undefined> {
320
+ async function showDetail(manager: SubagentManager, storageDirectory: string, entry: NavigatorEntry, context: ExtensionCommandContext, clipboard: (value: string) => Promise<void>): Promise<void> {
262
321
  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
- }
322
+ for (;;) {
323
+ const action = await context.ui.select(detailRows(inspection, PLAIN_WORKFLOW_PROGRESS_STYLES).join("\n"), actionOptions(manager, inspection, context));
324
+ if (!action || action === "Back") return;
325
+ try {
326
+ const outcome = await performAction(manager, storageDirectory, entry, action, context, undefined, clipboard, (title, message) => context.ui.confirm(title, message));
327
+ if (outcome === "deleted" || typeof outcome === "object") return;
328
+ inspection = await inspectEntry(manager, storageDirectory, entry, context);
329
+ } catch (error) {
330
+ context.ui.notify(`Cannot ${action.toLowerCase()}: ${errorText(error)}`, "warning");
273
331
  }
274
332
  }
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;
333
+ }
334
+
335
+ /** The `/workflow` dashboard layout for standalone runs: the run list beside the selected run's details. */
336
+ async function showDashboard(manager: SubagentManager, storageDirectory: string, initial: readonly NavigatorEntry[], context: ExtensionCommandContext, clipboard: (value: string) => Promise<void>): Promise<void> {
337
+ const first = initial[0];
338
+ if (first === undefined) return;
339
+ let initialInspection: Inspection;
340
+ try { initialInspection = await inspectEntry(manager, storageDirectory, first, context); }
341
+ catch (error) {
342
+ context.ui.notify(`Cannot inspect subagent ${first.status.id}: ${errorText(error)}`, "warning");
343
+ initialInspection = { entry: first, record: {} };
344
+ }
345
+ const result = await context.ui.custom<DashboardResult>((tui, theme, keybindings, done) => {
346
+ const styles = themeWorkflowProgressStyles(theme);
282
347
  const editorTheme: EditorTheme = {
283
348
  borderColor: (text) => theme.fg("accent", text),
284
349
  selectList: {
@@ -290,160 +355,241 @@ async function showDetail(manager: SubagentManager, storageDirectory: string, en
290
355
  },
291
356
  };
292
357
  const steerEditor = new Editor(tui, editorTheme);
293
- let refreshTimer: ReturnType<typeof setInterval> | undefined;
358
+ let entries = initial;
359
+ let inspection = initialInspection;
360
+ let offset = 0;
361
+ let detailsMode = false;
362
+ let actionMode = false;
363
+ let actionIndex = 0;
364
+ let steerMode = false;
365
+ const inlineConfirm = createInlineConfirm(() => { if (!disposed) tui.requestRender(); });
366
+ const confirmInline: Confirm = (title, message) => inlineConfirm.ask(title, message);
367
+ let actionRunning = false;
294
368
  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;
369
+ let disposed = false;
370
+ let generation = 0;
371
+ let renderedWidth = 80;
372
+ let selectionNeedsScroll = true;
373
+ let timer: ReturnType<typeof setInterval> | undefined;
374
+ const keyLabel = (binding: string, fallback: string): string => workflowKeyLabel(keybindings, binding, fallback);
375
+ const matches = (data: string, binding: string): boolean => workflowKeyMatches(keybindings, data, binding);
376
+ const requestRender = (): void => { if (!disposed) tui.requestRender(); };
377
+ const options = (): string[] => actionOptions(manager, inspection, context, bulkDeleteLabels(manager, entries));
378
+ const warn = (message: string): void => {
379
+ if (disposed) return;
380
+ try { context.ui.notify(message, "warning"); } catch { /* The session UI may already be closing. */ }
305
381
  };
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));
382
+ const stopTimer = (): void => {
383
+ if (timer === undefined) return;
384
+ clearInterval(timer);
385
+ timer = undefined;
316
386
  };
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) };
387
+ // Refresh while anything runs; a settled list has nothing left to change.
388
+ const syncTimer = (): void => {
389
+ if (!entries.some((entry) => entry.status.state === "running") && inspection.entry.status.state !== "running") stopTimer();
390
+ else if (timer === undefined && !disposed) {
391
+ timer = setInterval(() => { if (timer !== undefined) void refresh(); }, 1000);
392
+ unrefTimer(timer);
393
+ }
321
394
  };
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);
395
+ const apply = (nextEntries: readonly NavigatorEntry[], next: Inspection): void => {
396
+ const selectedAction = actionMode ? options()[actionIndex] : undefined;
397
+ const position = (list: readonly NavigatorEntry[]): number => list.findIndex((entry) => entry.status.id === next.entry.status.id);
398
+ if (position(entries) !== position(nextEntries)) selectionNeedsScroll = true;
399
+ entries = nextEntries;
400
+ inspection = next;
401
+ if (selectedAction !== undefined) {
402
+ const nextOptions = options();
403
+ const kept = nextOptions.indexOf(selectedAction);
404
+ actionIndex = kept >= 0 ? kept : Math.min(actionIndex, Math.max(0, nextOptions.length - 1));
405
+ }
406
+ syncTimer();
407
+ requestRender();
330
408
  };
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. */ }
409
+ const reload = async (id = inspection.entry.status.id): Promise<void> => {
410
+ const current = ++generation;
411
+ const nextEntries = await loadEntries(manager, storageDirectory, context);
412
+ if (current !== generation) return;
413
+ const selected = nextEntries.find((entry) => entry.status.id === id) ?? nextEntries[0];
414
+ if (selected === undefined) {
415
+ context.ui.notify("No durable subagent runs in this session.", "info");
416
+ close(undefined);
417
+ return;
418
+ }
419
+ let next: Inspection;
420
+ try { next = await inspectEntry(manager, storageDirectory, selected, context); }
421
+ catch (error: unknown) {
422
+ // The fresh list still shows, with the row's own data as when moving the selection.
423
+ if (current === generation) apply(nextEntries, { entry: selected, record: {} });
424
+ throw error;
425
+ }
426
+ if (current === generation) apply(nextEntries, next);
334
427
  };
335
- const refreshInspection = async (): Promise<void> => {
336
- if (disposed || actionRunning || refreshing || inspection.entry.status.state !== "running") return;
428
+ // A tick re-reads only the active runs and the selection; the full list is read on open and after an action.
429
+ // A failed read keeps the last known row, as the /workflow refresh does, so one unreadable run cannot hold back the rest.
430
+ // Moving the selection bumps the generation, so a tick that survives it still has the selection it read.
431
+ //NOTE: a running run whose files vanish keeps the timer alive until the dashboard closes.
432
+ const refresh = async (): Promise<void> => {
433
+ if (disposed || actionRunning || refreshing) return;
337
434
  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();
435
+ const current = generation;
436
+ const selectedId = inspection.entry.status.id;
343
437
  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);
438
+ const stale = entries.filter((entry) => entry.status.state === "running" || entry.status.id === selectedId);
439
+ const results = await Promise.allSettled(stale.map((entry) => inspectEntry(manager, storageDirectory, entry, context)));
440
+ if (current !== generation) return;
441
+ const fresh = new Map(results.flatMap((result) => result.status === "fulfilled" ? [[result.value.entry.status.id, result.value] as const] : []));
442
+ apply(attentionSort(entries.map((entry) => fresh.get(entry.status.id)?.entry ?? entry)), fresh.get(selectedId) ?? inspection);
358
443
  } finally {
359
444
  refreshing = false;
360
445
  }
361
446
  };
362
- const close = (value: DetailResult): void => {
363
- if (disposed) return;
447
+ const select = (delta: number): void => {
448
+ const index = Math.max(0, entries.findIndex((entry) => entry.status.id === inspection.entry.status.id));
449
+ const entry = entries[(index + delta + entries.length) % entries.length];
450
+ if (entry === undefined || entry.status.id === inspection.entry.status.id) return;
451
+ // The list row already holds the state; the full inspection adds the result and attempt details.
452
+ inspection = { entry, record: {} };
453
+ selectionNeedsScroll = true;
454
+ const current = ++generation;
455
+ void inspectEntry(manager, storageDirectory, entry, context).then((next) => {
456
+ if (current === generation) apply(entries, next);
457
+ }, (error: unknown) => { if (current === generation) warn(`Cannot inspect subagent ${entry.status.id}: ${errorText(error)}`); });
458
+ };
459
+ // Reads still in flight check the generation, so closing invalidates them.
460
+ const shutDown = (): void => {
364
461
  disposed = true;
365
- stopRefresh();
462
+ generation += 1;
463
+ stopTimer();
464
+ inlineConfirm.cancel();
465
+ };
466
+ const close = (value: DashboardResult): void => {
467
+ if (disposed) return;
468
+ shutDown();
366
469
  done(value);
367
470
  };
368
471
  steerEditor.onSubmit = (value) => {
369
472
  const message = value.trim();
370
- if (message) close({ kind: "steer", message });
473
+ if (message) close({ entry: inspection.entry, message });
371
474
  };
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, "…"));
475
+ const reloadAfterAction = async (id?: string): Promise<void> => {
476
+ try { await reload(id); }
477
+ catch (error: unknown) { warn(`Cannot refresh subagents: ${errorText(error)}`); }
384
478
  };
385
479
  const runAction = (action: string): void => {
386
- if (disposed) return;
387
480
  actionRunning = true;
388
481
  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();
482
+ const bulk = BULK_DELETE[action];
483
+ const pending = bulk === undefined ? performAction(manager, storageDirectory, inspection.entry, action, context, tui, clipboard, confirmInline) : deleteAll(manager, storageDirectory, context, bulk, confirmInline).then((done): ActionOutcome => done ? "stay" : "unchanged");
484
+ void pending.then(async (outcome) => {
485
+ if (disposed || outcome === "unchanged") return;
486
+ // The action already happened: a failed reload must not read as a failed action that invites a second retry.
487
+ await reloadAfterAction(typeof outcome === "object" ? outcome.retryId : undefined);
396
488
  actionMode = false;
397
489
  actionIndex = 0;
398
490
  offset = 0;
399
- }).catch((error: unknown) => {
400
- if (!disposed) context.ui.notify(`Cannot ${action.toLowerCase()}: ${errorText(error)}`, "warning");
491
+ selectionNeedsScroll = true;
492
+ }, async (error: unknown) => {
493
+ warn(`Cannot ${action.toLowerCase()}: ${errorText(error)}`);
494
+ // The menu stays open on fresh options: a failed Stop usually means the run has already settled.
495
+ if (!disposed) await reloadAfterAction();
401
496
  }).finally(() => {
402
497
  actionRunning = false;
403
- if (!disposed) requestRender();
498
+ requestRender();
404
499
  });
405
500
  };
406
- if (inspection.entry.status.state === "running") {
407
- refreshTimer = setInterval(() => { void refreshInspection().catch(reportRefreshError); }, 1000);
408
- unrefTimer(refreshTimer);
409
- }
501
+ syncTimer();
410
502
  return {
411
- render: renderLines,
503
+ render(width: number): string[] {
504
+ if (disposed) return [];
505
+ renderedWidth = width;
506
+ const narrow = width < 80;
507
+ const actions = actionMode ? options() : [];
508
+ const menu: DetailMenu = actionMode ? { options: actions, index: actionIndex } : steerMode ? undefined : "hint";
509
+ const layout = narrow ? detailsMode || actionMode || steerMode ? { detailsOnly: true } : { treeOnly: true } : {};
510
+ const content = [...headerRows(entries, styles), ...formatNavigatorColumns(listRows(entries, inspection.entry.status.id, styles), detailRows(inspection, styles, menu), width, layout)];
511
+ const footer = steerMode ? [styles.bold("Steer subagent"), ...steerEditor.render(width)] : inlineConfirm.rows(width, styles);
512
+ const rows = Math.max(1, tuiRows(tui) - DASHBOARD_FOOTER_ROWS);
513
+ const hintRows = rows >= 3 ? 1 : 0;
514
+ const viewport = Math.max(1, rows - hintRows - footer.length);
515
+ const keepVisible = (row: number): void => {
516
+ if (row < 0) return;
517
+ if (row < offset) offset = row;
518
+ else if (row >= offset + viewport) offset = row - viewport + 1;
519
+ };
520
+ // The selected action is the first arrow in the details column: it precedes the prompt and result.
521
+ if (actionMode) keepVisible(content.findIndex((line) => narrow ? line.startsWith("→ ") : line.includes(" | → ")));
522
+ else if (selectionNeedsScroll && !(narrow && detailsMode)) {
523
+ keepVisible(content.findIndex((line) => line.startsWith("→")));
524
+ selectionNeedsScroll = false;
525
+ }
526
+ offset = Math.max(0, Math.min(Math.max(0, content.length - viewport), offset));
527
+ const up = keyLabel("tui.select.up", "↑");
528
+ const down = keyLabel("tui.select.down", "↓");
529
+ const enter = keyLabel("tui.select.confirm", "enter");
530
+ const esc = keyLabel("tui.select.cancel", "esc");
531
+ const scroll = content.length > viewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : "";
532
+ const refreshHint = timer === undefined ? "" : " · auto-refresh 1s";
533
+ const back = narrow && detailsMode ? "details" : "list";
534
+ const hint = steerMode ? "enter submit · esc back"
535
+ : inlineConfirm.active() ? inlineConfirm.hint(keybindings)
536
+ : actionMode ? `${up}/${down} actions · ${enter} run · ${keyLabel("tui.editor.cursorLeft", "←")} ${back} · ${esc} ${back}`
537
+ : narrow && detailsMode ? `${up}/${down} scroll · ${enter} actions · a actions · ${esc} list${scroll}${refreshHint}`
538
+ : `${up}/${down} select · ${enter} ${narrow ? "details" : "actions"} · a actions · ${esc} close${scroll}${refreshHint}`;
539
+ return [...content.slice(offset, offset + viewport), ...footer, ...(hintRows ? [styles.dim(hint)] : [])].map((line) => truncateToWidth(line, Math.max(1, width), "…"));
540
+ },
412
541
  invalidate() {},
413
542
  handleInput(data: string) {
414
- if (disposed || actionRunning) return;
543
+ if (disposed) return;
544
+ if (inlineConfirm.active()) { inlineConfirm.handleInput(keybindings, data); return; }
545
+ if (actionRunning) return;
415
546
  if (steerMode) {
416
- if (keybindings.matches(data, "tui.select.cancel")) { steerMode = false; actionMode = true; steerEditor.setText(""); offset = 0; scrollActionIntoView(); }
547
+ if (keybindings.matches(data, "tui.select.cancel")) { steerMode = false; actionMode = true; steerEditor.setText(""); }
417
548
  else steerEditor.handleInput(data);
418
549
  requestRender();
419
550
  return;
420
551
  }
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;
552
+ const narrow = renderedWidth < 80;
553
+ const page = Math.max(1, tuiRows(tui) - DASHBOARD_FOOTER_ROWS - 1);
554
+ if (matches(data, "tui.select.pageUp")) offset = Math.max(0, offset - page);
555
+ else if (matches(data, "tui.select.pageDown")) offset += page;
556
+ else if (actionMode) {
557
+ const actions = options();
558
+ if (matches(data, "tui.select.cancel") || matches(data, "tui.editor.cursorLeft")) { actionMode = false; offset = 0; selectionNeedsScroll = true; }
559
+ else if (matches(data, "tui.select.up")) actionIndex = (actionIndex + actions.length - 1) % actions.length;
560
+ else if (matches(data, "tui.select.down")) actionIndex = (actionIndex + 1) % actions.length;
561
+ else if (matches(data, "tui.select.confirm")) {
562
+ const action = actions[actionIndex];
563
+ if (action === "Steer") { steerMode = true; actionMode = false; steerEditor.setText(""); }
564
+ else if (action && action !== "Back") runAction(action);
565
+ else { actionMode = false; offset = 0; selectionNeedsScroll = true; }
566
+ }
567
+ } else if (data === "a" || data === "A") { actionMode = true; actionIndex = 0; offset = 0; }
568
+ else if (matches(data, "tui.select.cancel")) {
569
+ if (!narrow || !detailsMode) { close(undefined); return; }
570
+ detailsMode = false;
571
+ offset = 0;
572
+ selectionNeedsScroll = true;
573
+ } else if (narrow && detailsMode) {
574
+ if (matches(data, "tui.select.up")) offset = Math.max(0, offset - 1);
575
+ else if (matches(data, "tui.select.down")) offset += 1;
576
+ else if (matches(data, "tui.select.confirm")) { actionMode = true; actionIndex = 0; offset = 0; }
577
+ } else if (matches(data, "tui.select.up")) select(-1);
578
+ else if (matches(data, "tui.select.down")) select(1);
579
+ else if (matches(data, "tui.select.confirm")) {
580
+ if (narrow) detailsMode = true;
581
+ else actionMode = true;
582
+ actionIndex = 0;
583
+ offset = 0;
431
584
  }
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
585
  requestRender();
439
586
  },
440
- dispose() { if (!disposed) { disposed = true; stopRefresh(); } },
587
+ dispose() { shutDown(); },
441
588
  };
442
589
  });
443
- if (!result || result === "retry") return;
444
- try { await steerSubagent(manager, storageDirectory, entry, context, result.message); }
590
+ if (result === undefined) return;
591
+ try { await steerSubagent(manager, storageDirectory, result.entry, context, result.message); }
445
592
  catch (error) { context.ui.notify(`Cannot steer: ${errorText(error)}`, "warning"); }
446
- return "exit";
447
593
  }
448
594
 
449
595
  async function runNavigator(manager: SubagentManager, storageDirectory: string, args: string, context: ExtensionCommandContext, clipboard: (value: string) => Promise<void>): Promise<void> {
@@ -457,17 +603,27 @@ async function runNavigator(manager: SubagentManager, storageDirectory: string,
457
603
  context.ui.notify("No durable subagent runs in this session.", "info");
458
604
  return;
459
605
  }
606
+ const labels = pickerLabels(entries);
460
607
  if (!context.hasUI) {
461
- context.ui.notify(entries.map((entry, index) => pickerLabel(entry, index)).join("\n"), "info");
608
+ context.ui.notify(labels.join("\n"), "info");
609
+ return;
610
+ }
611
+ if (context.mode === "tui") {
612
+ await showDashboard(manager, storageDirectory, entries, context, clipboard);
462
613
  return;
463
614
  }
464
- const labels = entries.map(pickerLabel);
465
- const choice = await context.ui.select("Subagents\n", [...labels, "Close"]);
615
+ const choice = await context.ui.select("Subagents\n", [...labels, "Close", ...bulkDeleteLabels(manager, entries)]);
466
616
  if (!choice || choice === "Close") return;
617
+ const bulk = BULK_DELETE[choice];
618
+ if (bulk !== undefined) {
619
+ try { await deleteAll(manager, storageDirectory, context, bulk, (title, message) => context.ui.confirm(title, message)); }
620
+ catch (error) { context.ui.notify(`Cannot delete ${bulk} runs: ${errorText(error)}`, "warning"); }
621
+ continue;
622
+ }
467
623
  const selected = entries[labels.indexOf(choice)];
468
624
  if (!selected) return;
469
625
  try {
470
- if (await showDetail(manager, storageDirectory, selected, context, clipboard) === "exit") return;
626
+ await showDetail(manager, storageDirectory, selected, context, clipboard);
471
627
  } catch (error) {
472
628
  context.ui.notify(`Cannot inspect subagent ${selected.status.id}: ${errorText(error)}`, "warning");
473
629
  }