@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.19.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.
- package/CHANGELOG.md +135 -0
- package/dist/src/agent-execution.js +114 -87
- package/dist/src/background-widget.d.ts +61 -0
- package/dist/src/background-widget.js +23 -16
- package/dist/src/decoders.js +28 -1
- package/dist/src/execution.d.ts +4 -2
- package/dist/src/execution.js +43 -2
- package/dist/src/host-delivery.js +1 -0
- package/dist/src/host-navigator.d.ts +18 -0
- package/dist/src/host-navigator.js +71 -13
- package/dist/src/host-recovery.d.ts +5 -2
- package/dist/src/host-recovery.js +3 -3
- package/dist/src/host-view.d.ts +21 -1
- package/dist/src/host-view.js +138 -47
- package/dist/src/host.d.ts +2 -2
- package/dist/src/host.js +246 -60
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +1346 -622
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/registry.js +3 -2
- package/dist/src/roles.d.ts +70 -0
- package/dist/src/roles.js +326 -0
- package/dist/src/script-tools.d.ts +27 -0
- package/dist/src/script-tools.js +87 -0
- package/dist/src/settings.d.ts +26 -0
- package/dist/src/settings.js +277 -0
- package/dist/src/trajectory.d.ts +39 -4
- package/dist/src/trajectory.js +82 -5
- package/dist/src/types.d.ts +30 -3
- package/dist/src/types.js +1 -1
- package/dist/src/utils.d.ts +21 -1
- package/dist/src/utils.js +40 -0
- package/dist/src/validation.d.ts +10 -17
- package/dist/src/validation.js +29 -458
- package/dist/starter/index.js +0 -2
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.d.ts +0 -13
- package/dist/starter/review-loop.js +0 -2
- package/dist/subagents/index.js +1344 -798
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +49 -29
- package/dist/subagents/src/contracts.js +18 -7
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +15 -7
- package/dist/subagents/src/manager.js +32 -13
- package/dist/subagents/src/navigator.js +396 -247
- package/dist/subagents/src/view.d.ts +24 -2
- package/dist/subagents/src/view.js +107 -80
- package/dist/trajectory/assets/index.html +370 -232
- package/dist/trajectory/assets/prism.min.js +13 -0
- package/dist/trajectory/index.js +445 -277
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +370 -232
- package/dist/trajectory/src/assets/prism.min.js +13 -0
- package/dist/trajectory/src/export.js +4 -2
- package/dist/trajectory/src/index.d.ts +2 -0
- package/dist/trajectory/src/index.js +151 -60
- package/dist/trajectory/src/server.js +138 -11
- package/dist/trajectory/src/server.js.map +2 -2
- package/package.json +7 -6
- package/skills/pi-extensible-workflows/SKILL.md +2 -0
- package/src/agent-execution.ts +110 -80
- package/src/background-widget.ts +26 -19
- package/src/decoders.ts +22 -1
- package/src/execution.ts +41 -4
- package/src/host-delivery.ts +1 -0
- package/src/host-navigator.ts +58 -14
- package/src/host-recovery.ts +8 -7
- package/src/host-view.ts +116 -42
- package/src/host.ts +223 -55
- package/src/index.ts +3 -1
- package/src/paths.ts +3 -0
- package/src/registry.ts +3 -2
- package/src/roles.ts +327 -0
- package/src/script-tools.ts +76 -0
- package/src/settings.ts +224 -0
- package/src/trajectory.ts +97 -9
- package/src/types.ts +12 -5
- package/src/utils.ts +36 -1
- package/src/validation.ts +30 -356
- package/starter/review-loop.ts +0 -2
- package/subagents/README.md +3 -3
- package/subagents/src/contracts.ts +22 -7
- package/subagents/src/index.ts +18 -7
- package/subagents/src/manager.ts +31 -13
- package/subagents/src/navigator.ts +336 -180
- package/subagents/src/view.ts +108 -76
- package/trajectory/src/assets/index.html +370 -232
- package/trajectory/src/assets/prism.min.js +13 -0
- package/trajectory/src/export.ts +4 -2
- package/trajectory/src/index.ts +124 -47
- package/trajectory/src/server.ts +114 -14
|
@@ -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
|
|
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
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
`
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
233
|
-
if (action === "Copy
|
|
234
|
-
if (action === "
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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 "
|
|
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
|
|
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<
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
|
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
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
336
|
-
|
|
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
|
|
339
|
-
const
|
|
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
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|
363
|
-
|
|
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
|
-
|
|
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({
|
|
473
|
+
if (message) close({ entry: inspection.entry, message });
|
|
371
474
|
};
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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
|
-
|
|
498
|
+
requestRender();
|
|
404
499
|
});
|
|
405
500
|
};
|
|
406
|
-
|
|
407
|
-
refreshTimer = setInterval(() => { void refreshInspection().catch(reportRefreshError); }, 1000);
|
|
408
|
-
unrefTimer(refreshTimer);
|
|
409
|
-
}
|
|
501
|
+
syncTimer();
|
|
410
502
|
return {
|
|
411
|
-
render:
|
|
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
|
|
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("");
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (
|
|
428
|
-
else if (
|
|
429
|
-
else if (
|
|
430
|
-
|
|
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() {
|
|
587
|
+
dispose() { shutDown(); },
|
|
441
588
|
};
|
|
442
589
|
});
|
|
443
|
-
if (
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
}
|