@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.
- package/CHANGELOG.md +67 -0
- package/dist/src/agent-execution.js +36 -51
- package/dist/src/background-widget.d.ts +56 -0
- package/dist/src/background-widget.js +22 -15
- package/dist/src/decoders.js +2 -1
- package/dist/src/host-navigator.d.ts +18 -0
- package/dist/src/host-navigator.js +71 -13
- package/dist/src/host-view.d.ts +16 -1
- package/dist/src/host-view.js +109 -42
- package/dist/src/host.js +57 -24
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +867 -545
- package/dist/src/index.js.map +3 -3
- package/dist/src/registry.js +2 -1
- package/dist/src/roles.d.ts +70 -0
- package/dist/src/roles.js +325 -0
- package/dist/src/settings.d.ts +13 -0
- package/dist/src/settings.js +253 -0
- package/dist/src/trajectory.d.ts +29 -4
- package/dist/src/trajectory.js +58 -3
- package/dist/src/types.d.ts +0 -1
- package/dist/src/utils.d.ts +2 -1
- package/dist/src/utils.js +10 -0
- package/dist/src/validation.d.ts +5 -17
- package/dist/src/validation.js +9 -458
- package/dist/subagents/index.js +1132 -694
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +4 -0
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +2 -1
- package/dist/subagents/src/manager.js +29 -10
- package/dist/subagents/src/navigator.js +396 -247
- package/dist/subagents/src/view.d.ts +23 -2
- package/dist/subagents/src/view.js +96 -30
- package/dist/trajectory/assets/index.html +347 -231
- package/dist/trajectory/assets/prism.min.js +13 -0
- package/dist/trajectory/index.js +377 -257
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +347 -231
- 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 +143 -42
- package/dist/trajectory/src/server.js +134 -9
- package/dist/trajectory/src/server.js.map +1 -1
- package/package.json +2 -1
- package/src/agent-execution.ts +36 -42
- package/src/background-widget.ts +25 -18
- package/src/decoders.ts +2 -1
- package/src/host-navigator.ts +58 -14
- package/src/host-view.ts +93 -37
- package/src/host.ts +51 -21
- package/src/index.ts +3 -1
- package/src/registry.ts +2 -1
- package/src/roles.ts +326 -0
- package/src/settings.ts +206 -0
- package/src/trajectory.ts +76 -7
- package/src/types.ts +1 -1
- package/src/utils.ts +8 -1
- package/src/validation.ts +12 -356
- package/subagents/README.md +1 -1
- package/subagents/src/contracts.ts +4 -0
- package/subagents/src/index.ts +3 -2
- package/subagents/src/manager.ts +28 -10
- package/subagents/src/navigator.ts +336 -180
- package/subagents/src/view.ts +97 -28
- package/trajectory/src/assets/index.html +347 -231
- package/trajectory/src/assets/prism.min.js +13 -0
- package/trajectory/src/export.ts +4 -2
- package/trajectory/src/index.ts +118 -38
- package/trajectory/src/server.ts +112 -12
|
@@ -2,11 +2,10 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { copyToClipboard, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { Editor, truncateToWidth } from "@earendil-works/pi-tui";
|
|
5
|
-
import { agentActionLabels, deepFreeze, errorText, formatAgentDetail, jsonValue, loadingRegistry, navigatorAttentionSortByState, openWorkflowArtifact, themeWorkflowProgressStyles, visibleStandaloneAgentAttemptActions, workflowPromptArtifact, workflowResultArtifact } 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 } from "../../src/index.js";
|
|
6
6
|
import { normalizeSubagentRunRequest } 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
|
function objectValue(value) { return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined; }
|
|
11
10
|
function isFileNotFound(error) { return objectValue(error)?.code === "ENOENT"; }
|
|
12
11
|
function safeRunId(id) { return id !== "." && id !== ".." && /^[A-Za-z0-9._-]+$/.test(id); }
|
|
@@ -15,8 +14,9 @@ function inspectionValue(value) {
|
|
|
15
14
|
const status = statusValue(value);
|
|
16
15
|
return record && status ? { status, record } : undefined;
|
|
17
16
|
}
|
|
17
|
+
// Detail inspections only feed this UI, so they carry the live activity that tool results leave out.
|
|
18
18
|
function managerContext(context, waitForForeground = true, includeAttemptMetadata = false) {
|
|
19
|
-
return { toolCallId: "subagents-command", signal: undefined, onUpdate: undefined, ...(waitForForeground ? {} : { waitForForeground: false }), ...(includeAttemptMetadata ? { includeAttemptMetadata: true } : {}), extensionContext: context };
|
|
19
|
+
return { toolCallId: "subagents-command", signal: undefined, onUpdate: undefined, ...(waitForForeground ? {} : { waitForForeground: false }), ...(includeAttemptMetadata ? { includeAttemptMetadata: true, includeActivity: true } : {}), extensionContext: context };
|
|
20
20
|
}
|
|
21
21
|
async function loadRequest(storageDirectory, id) {
|
|
22
22
|
if (!safeRunId(id))
|
|
@@ -60,7 +60,6 @@ async function inspectEntry(manager, storageDirectory, entry, context) {
|
|
|
60
60
|
entry: { status: inspected.status, ...(request.request === undefined ? {} : { request: request.request }), ...(request.error === undefined ? {} : { requestError: request.error }) },
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
-
function requestLabel(request) { return request?.label?.trim() || "none"; }
|
|
64
63
|
function requestRole(request) {
|
|
65
64
|
const role = request?.role;
|
|
66
65
|
if (typeof role === "string" && role.trim())
|
|
@@ -69,10 +68,32 @@ function requestRole(request) {
|
|
|
69
68
|
return typeof override?.name === "string" && override.name.trim() ? override.name.trim() : "none";
|
|
70
69
|
}
|
|
71
70
|
function shortId(id) { return id.length > 12 ? id.slice(0, 8) : id; }
|
|
72
|
-
function
|
|
71
|
+
function entryName(entry) {
|
|
72
|
+
const role = requestRole(entry.request);
|
|
73
|
+
return boundedText(entry.request?.label?.trim() || (role === "none" ? shortId(entry.status.id) : role), 256);
|
|
74
|
+
}
|
|
75
|
+
/** Names for a list of runs; a repeated name carries its short ID, as in the `/workflow` picker, so every row stays unique. */
|
|
76
|
+
function listNames(entries) {
|
|
77
|
+
const names = entries.map(entryName);
|
|
78
|
+
return entries.map(({ status }, index) => {
|
|
79
|
+
const name = names[index] ?? "";
|
|
80
|
+
return names.indexOf(name) === names.lastIndexOf(name) ? name : `${name} ${shortId(status.id)}`;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/** Picker rows in the `/workflow` style. */
|
|
84
|
+
function pickerLabels(entries) {
|
|
85
|
+
const names = listNames(entries);
|
|
86
|
+
return entries.map(({ status }, index) => {
|
|
87
|
+
const name = names[index] ?? "";
|
|
88
|
+
const model = status.progress?.state?.model;
|
|
89
|
+
const cost = formatCost(status.progress?.accounting.cost);
|
|
90
|
+
const runtime = status.startedAt === undefined ? "" : ` runtime=${formatWorkflowRuntime((status.finishedAt ?? Date.now()) - status.startedAt)}`;
|
|
91
|
+
return `${runStateGlyph(status.state, "⠦")} ${name} ${status.state}${model ? ` ${boundedText(model.model, 256)}${model.thinking ? `:${model.thinking}` : ""}` : ""}${cost ? ` ${cost}` : ""}${runtime}`;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
73
94
|
function boundedText(value, limit = MAX_DETAIL_TEXT) {
|
|
74
95
|
const text = typeof value === "string" ? value : (() => { try {
|
|
75
|
-
const serialized = JSON.stringify(value);
|
|
96
|
+
const serialized = JSON.stringify(value, null, 2);
|
|
76
97
|
return typeof serialized === "string" ? serialized : String(value);
|
|
77
98
|
}
|
|
78
99
|
catch {
|
|
@@ -80,12 +101,6 @@ function boundedText(value, limit = MAX_DETAIL_TEXT) {
|
|
|
80
101
|
} })();
|
|
81
102
|
return text.length > limit ? `${text.slice(0, limit)}…` : text;
|
|
82
103
|
}
|
|
83
|
-
function timestamp(value) { return typeof value === "number" && Number.isFinite(value) ? new Date(value).toISOString() : undefined; }
|
|
84
|
-
function appendValue(lines, title, value) {
|
|
85
|
-
lines.push(`${title}:`);
|
|
86
|
-
for (const line of boundedText(value).split("\n"))
|
|
87
|
-
lines.push(` ${line}`);
|
|
88
|
-
}
|
|
89
104
|
function latestAttempt(status) {
|
|
90
105
|
return [...(status.attemptDetails ?? [])].sort((left, right) => right.attempt - left.attempt)[0];
|
|
91
106
|
}
|
|
@@ -118,36 +133,31 @@ function detailPresentation(inspection) {
|
|
|
118
133
|
...(error === undefined ? {} : { error: { code: boundedText(error.code, 256), message: boundedText(error.message) } }),
|
|
119
134
|
};
|
|
120
135
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
`
|
|
128
|
-
...formatAgentDetail(
|
|
136
|
+
/** 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. */
|
|
137
|
+
function detailRows(inspection, styles, menu) {
|
|
138
|
+
const { entry } = inspection;
|
|
139
|
+
const presentation = detailPresentation(inspection);
|
|
140
|
+
return [
|
|
141
|
+
styles.bold(`Selected subagent: ${entryName(entry)}`),
|
|
142
|
+
`ID: ${boundedText(entry.status.id, 256)}`,
|
|
143
|
+
...formatAgentDetail(presentation, styles, entry.status.finishedAt ?? Date.now(), { includeError: false }),
|
|
144
|
+
...(entry.requestError === undefined ? [] : [styles.warning(`Request unavailable: ${boundedText(entry.requestError)}`)]),
|
|
145
|
+
...(menu === "hint" ? [styles.muted("enter agent actions")] : []),
|
|
146
|
+
...(presentation.error === undefined ? [] : [formatAgentError(presentation.error, styles)]),
|
|
147
|
+
...(typeof menu === "object" ? [styles.bold("Agent actions"), ...menu.options.map((option, index) => index === menu.index ? `→ ${styles.accent(option)}` : ` ${option}`)] : []),
|
|
129
148
|
];
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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))
|
|
146
|
-
lines.push(` ${boundedText(call.name, 256)} [${call.state}]`);
|
|
147
|
-
}
|
|
148
|
-
if (Object.prototype.hasOwnProperty.call(record, "value"))
|
|
149
|
-
appendValue(lines, "value", record.value);
|
|
150
|
-
return lines;
|
|
149
|
+
}
|
|
150
|
+
function listRows(entries, selectedId, styles) {
|
|
151
|
+
const names = listNames(entries);
|
|
152
|
+
return [styles.bold("Runs"), ...entries.map((entry, index) => `${entry.status.id === selectedId ? "→" : " "} • ${names[index] ?? ""} · ${progressStyleForState(entry.status.state, styles)(runStateGlyph(entry.status.state, "⠦"))}`)];
|
|
153
|
+
}
|
|
154
|
+
function headerRows(entries, styles) {
|
|
155
|
+
const counts = ["running", "failed", "stopped", "completed"].flatMap((state) => {
|
|
156
|
+
const count = entries.filter((entry) => entry.status.state === state).length;
|
|
157
|
+
return count ? [`${String(count)} ${state}`] : [];
|
|
158
|
+
});
|
|
159
|
+
const cost = formatCost(entries.reduce((sum, entry) => sum + (entry.status.progress?.accounting.cost ?? 0), 0));
|
|
160
|
+
return [styles.bold(styles.accent("Subagents")), [...counts, ...(cost ? [cost] : [])].join(" · ")];
|
|
151
161
|
}
|
|
152
162
|
function tuiRows(tui) { return typeof tui.terminal?.rows === "number" && Number.isFinite(tui.terminal.rows) ? tui.terminal.rows : 24; }
|
|
153
163
|
function unrefTimer(timer) {
|
|
@@ -156,6 +166,8 @@ function unrefTimer(timer) {
|
|
|
156
166
|
return;
|
|
157
167
|
Reflect.apply(value.unref, value, []);
|
|
158
168
|
}
|
|
169
|
+
// Pi keeps its footer below a custom component; the dashboard leaves room for it as /workflow does.
|
|
170
|
+
const DASHBOARD_FOOTER_ROWS = 2;
|
|
159
171
|
function standaloneActionContext(manager, inspection, context) {
|
|
160
172
|
const status = inspection.entry.status;
|
|
161
173
|
const request = inspection.entry.request;
|
|
@@ -189,11 +201,19 @@ function standaloneActionContext(manager, inspection, context) {
|
|
|
189
201
|
function liveSystemPrompt(manager, status) {
|
|
190
202
|
return manager.getAttemptActionData?.(status.id)?.prepared?.systemPrompt;
|
|
191
203
|
}
|
|
192
|
-
|
|
204
|
+
const BULK_DELETE = { "Delete all completed": "completed", "Delete all failed": "failed" };
|
|
205
|
+
/** The /workflow picker's bulk deletions, offered for the states present in the list. */
|
|
206
|
+
function bulkDeleteLabels(manager, entries) {
|
|
207
|
+
if (manager.delete === undefined)
|
|
208
|
+
return [];
|
|
209
|
+
return Object.entries(BULK_DELETE).filter(([, state]) => entries.some((entry) => entry.status.state === state)).map(([label]) => label);
|
|
210
|
+
}
|
|
211
|
+
/** Agent actions plus the /workflow run actions that apply to a standalone run: Delete and Copy run path, before Copy agent ID. */
|
|
212
|
+
function actionOptions(manager, inspection, context, bulk = []) {
|
|
193
213
|
const actionContext = standaloneActionContext(manager, inspection, context);
|
|
194
214
|
const extensionLabels = actionContext === undefined ? [] : visibleStandaloneAgentAttemptActions(loadingRegistry().agentAttemptActions(), actionContext).map(([, action]) => action.label);
|
|
195
215
|
const value = inspection.record.value;
|
|
196
|
-
|
|
216
|
+
const labels = agentActionLabels({
|
|
197
217
|
extensionLabels,
|
|
198
218
|
hasWorktree: inspection.entry.status.worktree !== undefined,
|
|
199
219
|
openPrompt: context.mode === "tui" && inspection.entry.request?.prompt !== undefined,
|
|
@@ -201,6 +221,31 @@ function actionOptions(manager, inspection, context) {
|
|
|
201
221
|
openResult: context.mode === "tui" && Object.prototype.hasOwnProperty.call(inspection.record, "value") && jsonValue(value),
|
|
202
222
|
standaloneState: inspection.entry.status.state,
|
|
203
223
|
});
|
|
224
|
+
const runActions = [...(manager.delete !== undefined && inspection.entry.status.state !== "running" ? ["Delete"] : []), ...bulk, ...(context.mode === "tui" ? ["Copy run path"] : [])];
|
|
225
|
+
labels.splice(labels.indexOf("Copy agent ID"), 0, ...runActions);
|
|
226
|
+
return labels;
|
|
227
|
+
}
|
|
228
|
+
async function deleteAll(manager, storageDirectory, context, state, confirm) {
|
|
229
|
+
if (!await confirm(`Delete ${state} runs?`, `Delete all ${state} subagent runs and their records? This cannot be undone.`))
|
|
230
|
+
return false;
|
|
231
|
+
const skipped = [];
|
|
232
|
+
let deleted = 0;
|
|
233
|
+
for (const entry of await loadEntries(manager, storageDirectory, context)) {
|
|
234
|
+
if (entry.status.state !== state)
|
|
235
|
+
continue;
|
|
236
|
+
try {
|
|
237
|
+
await manager.delete?.({ id: entry.status.id }, managerContext(context));
|
|
238
|
+
deleted += 1;
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
skipped.push(`${entry.status.id} (${errorText(error)})`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (skipped.length)
|
|
245
|
+
context.ui.notify(`Skipped ${state} runs: ${skipped.join(", ")}.`, "warning");
|
|
246
|
+
if (deleted)
|
|
247
|
+
context.ui.notify(`Deleted ${String(deleted)} ${state} subagent run(s).`, "info");
|
|
248
|
+
return true;
|
|
204
249
|
}
|
|
205
250
|
function retryResult(value) {
|
|
206
251
|
const record = objectValue(value);
|
|
@@ -223,7 +268,7 @@ async function steerSubagent(manager, storageDirectory, entry, context, message)
|
|
|
223
268
|
await manager.steer({ id: entry.status.id, message }, managerContext(context));
|
|
224
269
|
context.ui.notify(`Steered subagent ${entry.status.id}.`, "info");
|
|
225
270
|
}
|
|
226
|
-
async function performAction(manager, storageDirectory, entry, action, context, tui, clipboard) {
|
|
271
|
+
async function performAction(manager, storageDirectory, entry, action, context, tui, clipboard, confirm) {
|
|
227
272
|
const fresh = await inspectEntry(manager, storageDirectory, entry, context);
|
|
228
273
|
const available = actionOptions(manager, fresh, context);
|
|
229
274
|
if (!available.includes(action))
|
|
@@ -239,41 +284,55 @@ async function performAction(manager, storageDirectory, entry, action, context,
|
|
|
239
284
|
if (action === "Copy agent ID") {
|
|
240
285
|
await clipboard(fresh.entry.status.id);
|
|
241
286
|
context.ui.notify("Copied agent ID.", "info");
|
|
242
|
-
return "
|
|
287
|
+
return "unchanged";
|
|
288
|
+
}
|
|
289
|
+
if (action === "Copy run path") {
|
|
290
|
+
await clipboard(join(storageDirectory, fresh.entry.status.id));
|
|
291
|
+
context.ui.notify("Copied run path.", "info");
|
|
292
|
+
return "unchanged";
|
|
293
|
+
}
|
|
294
|
+
if (action === "Delete" && manager.delete !== undefined) {
|
|
295
|
+
if (!await confirm("Delete subagent?", `Delete ${entryName(fresh.entry)} (${fresh.entry.status.id}) and its record? This cannot be undone.`))
|
|
296
|
+
return "unchanged";
|
|
297
|
+
await manager.delete({ id: fresh.entry.status.id }, managerContext(context));
|
|
298
|
+
context.ui.notify(`Deleted subagent ${fresh.entry.status.id}.`, "info");
|
|
299
|
+
return "deleted";
|
|
243
300
|
}
|
|
244
301
|
if (action === "Copy branch" && fresh.entry.status.worktree) {
|
|
245
302
|
await clipboard(fresh.entry.status.worktree.branch);
|
|
246
303
|
context.ui.notify("Copied branch.", "info");
|
|
247
|
-
return "
|
|
304
|
+
return "unchanged";
|
|
248
305
|
}
|
|
249
306
|
if (action === "Copy worktree path" && fresh.entry.status.worktree) {
|
|
250
307
|
await clipboard(fresh.entry.status.worktree.path);
|
|
251
308
|
context.ui.notify("Copied worktree path.", "info");
|
|
252
|
-
return "
|
|
309
|
+
return "unchanged";
|
|
253
310
|
}
|
|
254
311
|
if (action === "Open prompt in editor" && tui && fresh.entry.request?.prompt !== undefined) {
|
|
255
312
|
await openNavigatorArtifact(context, tui, workflowPromptArtifact(fresh.entry.request.prompt), "agent prompt");
|
|
256
|
-
return "
|
|
313
|
+
return "unchanged";
|
|
257
314
|
}
|
|
258
315
|
if (action === "Open system prompt in editor" && tui) {
|
|
259
316
|
const systemPrompt = liveSystemPrompt(manager, fresh.entry.status);
|
|
260
317
|
if (systemPrompt !== undefined) {
|
|
261
318
|
await openNavigatorArtifact(context, tui, workflowPromptArtifact(systemPrompt), "agent system prompt");
|
|
262
|
-
return "
|
|
319
|
+
return "unchanged";
|
|
263
320
|
}
|
|
264
321
|
}
|
|
265
322
|
if (action === "Open result in editor" && tui && Object.prototype.hasOwnProperty.call(fresh.record, "value") && jsonValue(fresh.record.value)) {
|
|
266
323
|
await openNavigatorArtifact(context, tui, workflowResultArtifact(fresh.record.value), "agent result");
|
|
267
|
-
return "
|
|
324
|
+
return "unchanged";
|
|
268
325
|
}
|
|
269
326
|
if (action === "Steer") {
|
|
270
327
|
const message = await context.ui.input("Steer subagent", "Message for the running subagent");
|
|
271
328
|
if (message === undefined)
|
|
272
|
-
return "
|
|
329
|
+
return "unchanged";
|
|
273
330
|
await steerSubagent(manager, storageDirectory, entry, context, message);
|
|
274
331
|
return "stay";
|
|
275
332
|
}
|
|
276
333
|
if (action === "Stop") {
|
|
334
|
+
if (!await confirm("Stop subagent?", `Stop subagent ${entryName(fresh.entry)} (${fresh.entry.status.id})? This cannot be undone.`))
|
|
335
|
+
return "unchanged";
|
|
277
336
|
const current = await inspectEntry(manager, storageDirectory, entry, context);
|
|
278
337
|
if (current.entry.status.state !== "running")
|
|
279
338
|
throw new Error(`Subagent ${entry.status.id} is no longer running`);
|
|
@@ -289,34 +348,42 @@ async function performAction(manager, storageDirectory, entry, action, context,
|
|
|
289
348
|
if (!result)
|
|
290
349
|
throw new Error("Retry returned an invalid subagent result");
|
|
291
350
|
context.ui.notify(`Retried subagent ${entry.status.id} as ${result.id}.`, "info");
|
|
292
|
-
return
|
|
351
|
+
return { retryId: result.id };
|
|
293
352
|
}
|
|
294
353
|
return "stay";
|
|
295
354
|
}
|
|
296
355
|
async function showDetail(manager, storageDirectory, entry, context, clipboard) {
|
|
297
356
|
let inspection = await inspectEntry(manager, storageDirectory, entry, context);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
357
|
+
for (;;) {
|
|
358
|
+
const action = await context.ui.select(detailRows(inspection, PLAIN_WORKFLOW_PROGRESS_STYLES).join("\n"), actionOptions(manager, inspection, context));
|
|
359
|
+
if (!action || action === "Back")
|
|
360
|
+
return;
|
|
361
|
+
try {
|
|
362
|
+
const outcome = await performAction(manager, storageDirectory, entry, action, context, undefined, clipboard, (title, message) => context.ui.confirm(title, message));
|
|
363
|
+
if (outcome === "deleted" || typeof outcome === "object")
|
|
302
364
|
return;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
catch (error) {
|
|
309
|
-
context.ui.notify(`Cannot ${action.toLowerCase()}: ${errorText(error)}`, "warning");
|
|
310
|
-
}
|
|
365
|
+
inspection = await inspectEntry(manager, storageDirectory, entry, context);
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
context.ui.notify(`Cannot ${action.toLowerCase()}: ${errorText(error)}`, "warning");
|
|
311
369
|
}
|
|
312
370
|
}
|
|
371
|
+
}
|
|
372
|
+
/** The `/workflow` dashboard layout for standalone runs: the run list beside the selected run's details. */
|
|
373
|
+
async function showDashboard(manager, storageDirectory, initial, context, clipboard) {
|
|
374
|
+
const first = initial[0];
|
|
375
|
+
if (first === undefined)
|
|
376
|
+
return;
|
|
377
|
+
let initialInspection;
|
|
378
|
+
try {
|
|
379
|
+
initialInspection = await inspectEntry(manager, storageDirectory, first, context);
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
context.ui.notify(`Cannot inspect subagent ${first.status.id}: ${errorText(error)}`, "warning");
|
|
383
|
+
initialInspection = { entry: first, record: {} };
|
|
384
|
+
}
|
|
313
385
|
const result = await context.ui.custom((tui, theme, keybindings, done) => {
|
|
314
|
-
|
|
315
|
-
let actionMode = false;
|
|
316
|
-
let actionIndex = 0;
|
|
317
|
-
let actionRunning = false;
|
|
318
|
-
let disposed = false;
|
|
319
|
-
let steerMode = false;
|
|
386
|
+
const styles = themeWorkflowProgressStyles(theme);
|
|
320
387
|
const editorTheme = {
|
|
321
388
|
borderColor: (text) => theme.fg("accent", text),
|
|
322
389
|
selectList: {
|
|
@@ -328,258 +395,327 @@ async function showDetail(manager, storageDirectory, entry, context, clipboard)
|
|
|
328
395
|
},
|
|
329
396
|
};
|
|
330
397
|
const steerEditor = new Editor(tui, editorTheme);
|
|
331
|
-
let
|
|
398
|
+
let entries = initial;
|
|
399
|
+
let inspection = initialInspection;
|
|
400
|
+
let offset = 0;
|
|
401
|
+
let detailsMode = false;
|
|
402
|
+
let actionMode = false;
|
|
403
|
+
let actionIndex = 0;
|
|
404
|
+
let steerMode = false;
|
|
405
|
+
const inlineConfirm = createInlineConfirm(() => { if (!disposed)
|
|
406
|
+
tui.requestRender(); });
|
|
407
|
+
const confirmInline = (title, message) => inlineConfirm.ask(title, message);
|
|
408
|
+
let actionRunning = false;
|
|
332
409
|
let refreshing = false;
|
|
333
|
-
let
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
410
|
+
let disposed = false;
|
|
411
|
+
let generation = 0;
|
|
412
|
+
let renderedWidth = 80;
|
|
413
|
+
let selectionNeedsScroll = true;
|
|
414
|
+
let timer;
|
|
415
|
+
const keyLabel = (binding, fallback) => workflowKeyLabel(keybindings, binding, fallback);
|
|
416
|
+
const matches = (data, binding) => workflowKeyMatches(keybindings, data, binding);
|
|
417
|
+
const requestRender = () => { if (!disposed)
|
|
418
|
+
tui.requestRender(); };
|
|
419
|
+
const options = () => actionOptions(manager, inspection, context, bulkDeleteLabels(manager, entries));
|
|
420
|
+
const warn = (message) => {
|
|
421
|
+
if (disposed)
|
|
422
|
+
return;
|
|
423
|
+
try {
|
|
424
|
+
context.ui.notify(message, "warning");
|
|
342
425
|
}
|
|
343
|
-
|
|
426
|
+
catch { /* The session UI may already be closing. */ }
|
|
344
427
|
};
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
];
|
|
351
|
-
const viewportRows = () => Math.max(1, tuiRows(tui) - 1);
|
|
352
|
-
const maxOffsetFor = (rows) => Math.max(0, rows.length - viewportRows());
|
|
353
|
-
const clampOffset = (rows) => {
|
|
354
|
-
offset = Math.max(0, Math.min(maxOffsetFor(rows), offset));
|
|
428
|
+
const stopTimer = () => {
|
|
429
|
+
if (timer === undefined)
|
|
430
|
+
return;
|
|
431
|
+
clearInterval(timer);
|
|
432
|
+
timer = undefined;
|
|
355
433
|
};
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
434
|
+
// Refresh while anything runs; a settled list has nothing left to change.
|
|
435
|
+
const syncTimer = () => {
|
|
436
|
+
if (!entries.some((entry) => entry.status.state === "running") && inspection.entry.status.state !== "running")
|
|
437
|
+
stopTimer();
|
|
438
|
+
else if (timer === undefined && !disposed) {
|
|
439
|
+
timer = setInterval(() => { if (timer !== undefined)
|
|
440
|
+
void refresh(); }, 1000);
|
|
441
|
+
unrefTimer(timer);
|
|
442
|
+
}
|
|
360
443
|
};
|
|
361
|
-
const
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
444
|
+
const apply = (nextEntries, next) => {
|
|
445
|
+
const selectedAction = actionMode ? options()[actionIndex] : undefined;
|
|
446
|
+
const position = (list) => list.findIndex((entry) => entry.status.id === next.entry.status.id);
|
|
447
|
+
if (position(entries) !== position(nextEntries))
|
|
448
|
+
selectionNeedsScroll = true;
|
|
449
|
+
entries = nextEntries;
|
|
450
|
+
inspection = next;
|
|
451
|
+
if (selectedAction !== undefined) {
|
|
452
|
+
const nextOptions = options();
|
|
453
|
+
const kept = nextOptions.indexOf(selectedAction);
|
|
454
|
+
actionIndex = kept >= 0 ? kept : Math.min(actionIndex, Math.max(0, nextOptions.length - 1));
|
|
366
455
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (actionRow < offset)
|
|
370
|
-
offset = actionRow;
|
|
371
|
-
else if (actionRow >= offset + viewport)
|
|
372
|
-
offset = actionRow - viewport + 1;
|
|
373
|
-
clampOffset(rows);
|
|
456
|
+
syncTimer();
|
|
457
|
+
requestRender();
|
|
374
458
|
};
|
|
375
|
-
const
|
|
376
|
-
|
|
459
|
+
const reload = async (id = inspection.entry.status.id) => {
|
|
460
|
+
const current = ++generation;
|
|
461
|
+
const nextEntries = await loadEntries(manager, storageDirectory, context);
|
|
462
|
+
if (current !== generation)
|
|
377
463
|
return;
|
|
464
|
+
const selected = nextEntries.find((entry) => entry.status.id === id) ?? nextEntries[0];
|
|
465
|
+
if (selected === undefined) {
|
|
466
|
+
context.ui.notify("No durable subagent runs in this session.", "info");
|
|
467
|
+
close(undefined);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
let next;
|
|
378
471
|
try {
|
|
379
|
-
|
|
472
|
+
next = await inspectEntry(manager, storageDirectory, selected, context);
|
|
380
473
|
}
|
|
381
|
-
catch {
|
|
474
|
+
catch (error) {
|
|
475
|
+
// The fresh list still shows, with the row's own data as when moving the selection.
|
|
476
|
+
if (current === generation)
|
|
477
|
+
apply(nextEntries, { entry: selected, record: {} });
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
if (current === generation)
|
|
481
|
+
apply(nextEntries, next);
|
|
382
482
|
};
|
|
383
|
-
|
|
384
|
-
|
|
483
|
+
// A tick re-reads only the active runs and the selection; the full list is read on open and after an action.
|
|
484
|
+
// A failed read keeps the last known row, as the /workflow refresh does, so one unreadable run cannot hold back the rest.
|
|
485
|
+
// Moving the selection bumps the generation, so a tick that survives it still has the selection it read.
|
|
486
|
+
//NOTE: a running run whose files vanish keeps the timer alive until the dashboard closes.
|
|
487
|
+
const refresh = async () => {
|
|
488
|
+
if (disposed || actionRunning || refreshing)
|
|
385
489
|
return;
|
|
386
490
|
refreshing = true;
|
|
387
|
-
const
|
|
388
|
-
const
|
|
389
|
-
const viewBeforeRefresh = actionMode ? actionView() : undefined;
|
|
390
|
-
const actionRowBeforeRefresh = viewBeforeRefresh === undefined ? -1 : viewBeforeRefresh.rows.length - viewBeforeRefresh.options.length + actionIndex;
|
|
391
|
-
const wasVisible = actionRowBeforeRefresh >= offset && actionRowBeforeRefresh < offset + viewportRows();
|
|
491
|
+
const current = generation;
|
|
492
|
+
const selectedId = inspection.entry.status.id;
|
|
392
493
|
try {
|
|
393
|
-
const
|
|
394
|
-
|
|
494
|
+
const stale = entries.filter((entry) => entry.status.state === "running" || entry.status.id === selectedId);
|
|
495
|
+
const results = await Promise.allSettled(stale.map((entry) => inspectEntry(manager, storageDirectory, entry, context)));
|
|
496
|
+
if (current !== generation)
|
|
395
497
|
return;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
stopRefresh();
|
|
399
|
-
if (actionMode) {
|
|
400
|
-
const options = actionOptions(manager, inspection, context);
|
|
401
|
-
actionIndex = Math.min(actionIndex, Math.max(0, options.length - 1));
|
|
402
|
-
const view = actionView();
|
|
403
|
-
if (wasVisible || actionIndex !== actionIndexBeforeRefresh)
|
|
404
|
-
scrollActionIntoView(view);
|
|
405
|
-
else
|
|
406
|
-
clampOffset(view.rows);
|
|
407
|
-
}
|
|
408
|
-
requestRender();
|
|
409
|
-
}
|
|
410
|
-
catch (error) {
|
|
411
|
-
if (generation === refreshGeneration)
|
|
412
|
-
reportRefreshError(error);
|
|
498
|
+
const fresh = new Map(results.flatMap((result) => result.status === "fulfilled" ? [[result.value.entry.status.id, result.value]] : []));
|
|
499
|
+
apply(attentionSort(entries.map((entry) => fresh.get(entry.status.id)?.entry ?? entry)), fresh.get(selectedId) ?? inspection);
|
|
413
500
|
}
|
|
414
501
|
finally {
|
|
415
502
|
refreshing = false;
|
|
416
503
|
}
|
|
417
504
|
};
|
|
505
|
+
const select = (delta) => {
|
|
506
|
+
const index = Math.max(0, entries.findIndex((entry) => entry.status.id === inspection.entry.status.id));
|
|
507
|
+
const entry = entries[(index + delta + entries.length) % entries.length];
|
|
508
|
+
if (entry === undefined || entry.status.id === inspection.entry.status.id)
|
|
509
|
+
return;
|
|
510
|
+
// The list row already holds the state; the full inspection adds the result and attempt details.
|
|
511
|
+
inspection = { entry, record: {} };
|
|
512
|
+
selectionNeedsScroll = true;
|
|
513
|
+
const current = ++generation;
|
|
514
|
+
void inspectEntry(manager, storageDirectory, entry, context).then((next) => {
|
|
515
|
+
if (current === generation)
|
|
516
|
+
apply(entries, next);
|
|
517
|
+
}, (error) => { if (current === generation)
|
|
518
|
+
warn(`Cannot inspect subagent ${entry.status.id}: ${errorText(error)}`); });
|
|
519
|
+
};
|
|
520
|
+
// Reads still in flight check the generation, so closing invalidates them.
|
|
521
|
+
const shutDown = () => {
|
|
522
|
+
disposed = true;
|
|
523
|
+
generation += 1;
|
|
524
|
+
stopTimer();
|
|
525
|
+
inlineConfirm.cancel();
|
|
526
|
+
};
|
|
418
527
|
const close = (value) => {
|
|
419
528
|
if (disposed)
|
|
420
529
|
return;
|
|
421
|
-
|
|
422
|
-
stopRefresh();
|
|
530
|
+
shutDown();
|
|
423
531
|
done(value);
|
|
424
532
|
};
|
|
425
533
|
steerEditor.onSubmit = (value) => {
|
|
426
534
|
const message = value.trim();
|
|
427
535
|
if (message)
|
|
428
|
-
close({
|
|
536
|
+
close({ entry: inspection.entry, message });
|
|
429
537
|
};
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const rows = steerMode ? [...detail, "", theme.bold("Steer subagent"), ...steerEditor.render(renderWidth)] : action?.rows ?? detail;
|
|
438
|
-
const viewport = viewportRows();
|
|
439
|
-
const maxOffset = maxOffsetFor(rows);
|
|
440
|
-
const visibleOffset = steerMode ? maxOffset : Math.max(0, Math.min(maxOffset, offset));
|
|
441
|
-
const hint = theme.fg("dim", steerMode ? "enter submit · esc back" : actionMode ? "↑/↓ actions · enter run · esc back" : "↑/↓ scroll · a actions · enter actions · esc back");
|
|
442
|
-
return [...rows.slice(visibleOffset, visibleOffset + viewport), hint].map((line) => truncateToWidth(line, renderWidth, "…"));
|
|
538
|
+
const reloadAfterAction = async (id) => {
|
|
539
|
+
try {
|
|
540
|
+
await reload(id);
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
warn(`Cannot refresh subagents: ${errorText(error)}`);
|
|
544
|
+
}
|
|
443
545
|
};
|
|
444
546
|
const runAction = (action) => {
|
|
445
|
-
if (disposed)
|
|
446
|
-
return;
|
|
447
547
|
actionRunning = true;
|
|
448
548
|
requestRender();
|
|
449
|
-
|
|
450
|
-
|
|
549
|
+
const bulk = BULK_DELETE[action];
|
|
550
|
+
const pending = bulk === undefined ? performAction(manager, storageDirectory, inspection.entry, action, context, tui, clipboard, confirmInline) : deleteAll(manager, storageDirectory, context, bulk, confirmInline).then((done) => done ? "stay" : "unchanged");
|
|
551
|
+
void pending.then(async (outcome) => {
|
|
552
|
+
if (disposed || outcome === "unchanged")
|
|
451
553
|
return;
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
const next = await inspectEntry(manager, storageDirectory, entry, context);
|
|
457
|
-
if (isDisposed())
|
|
458
|
-
return;
|
|
459
|
-
inspection = next;
|
|
460
|
-
if (inspection.entry.status.state !== "running")
|
|
461
|
-
stopRefresh();
|
|
554
|
+
// The action already happened: a failed reload must not read as a failed action that invites a second retry.
|
|
555
|
+
await reloadAfterAction(typeof outcome === "object" ? outcome.retryId : undefined);
|
|
462
556
|
actionMode = false;
|
|
463
557
|
actionIndex = 0;
|
|
464
558
|
offset = 0;
|
|
465
|
-
|
|
559
|
+
selectionNeedsScroll = true;
|
|
560
|
+
}, async (error) => {
|
|
561
|
+
warn(`Cannot ${action.toLowerCase()}: ${errorText(error)}`);
|
|
562
|
+
// The menu stays open on fresh options: a failed Stop usually means the run has already settled.
|
|
466
563
|
if (!disposed)
|
|
467
|
-
|
|
564
|
+
await reloadAfterAction();
|
|
468
565
|
}).finally(() => {
|
|
469
566
|
actionRunning = false;
|
|
470
|
-
|
|
471
|
-
requestRender();
|
|
567
|
+
requestRender();
|
|
472
568
|
});
|
|
473
569
|
};
|
|
474
|
-
|
|
475
|
-
refreshTimer = setInterval(() => { void refreshInspection().catch(reportRefreshError); }, 1000);
|
|
476
|
-
unrefTimer(refreshTimer);
|
|
477
|
-
}
|
|
570
|
+
syncTimer();
|
|
478
571
|
return {
|
|
479
|
-
render
|
|
572
|
+
render(width) {
|
|
573
|
+
if (disposed)
|
|
574
|
+
return [];
|
|
575
|
+
renderedWidth = width;
|
|
576
|
+
const narrow = width < 80;
|
|
577
|
+
const actions = actionMode ? options() : [];
|
|
578
|
+
const menu = actionMode ? { options: actions, index: actionIndex } : steerMode ? undefined : "hint";
|
|
579
|
+
const layout = narrow ? detailsMode || actionMode || steerMode ? { detailsOnly: true } : { treeOnly: true } : {};
|
|
580
|
+
const content = [...headerRows(entries, styles), ...formatNavigatorColumns(listRows(entries, inspection.entry.status.id, styles), detailRows(inspection, styles, menu), width, layout)];
|
|
581
|
+
const footer = steerMode ? [styles.bold("Steer subagent"), ...steerEditor.render(width)] : inlineConfirm.rows(width, styles);
|
|
582
|
+
const rows = Math.max(1, tuiRows(tui) - DASHBOARD_FOOTER_ROWS);
|
|
583
|
+
const hintRows = rows >= 3 ? 1 : 0;
|
|
584
|
+
const viewport = Math.max(1, rows - hintRows - footer.length);
|
|
585
|
+
const keepVisible = (row) => {
|
|
586
|
+
if (row < 0)
|
|
587
|
+
return;
|
|
588
|
+
if (row < offset)
|
|
589
|
+
offset = row;
|
|
590
|
+
else if (row >= offset + viewport)
|
|
591
|
+
offset = row - viewport + 1;
|
|
592
|
+
};
|
|
593
|
+
// The selected action is the first arrow in the details column: it precedes the prompt and result.
|
|
594
|
+
if (actionMode)
|
|
595
|
+
keepVisible(content.findIndex((line) => narrow ? line.startsWith("→ ") : line.includes(" | → ")));
|
|
596
|
+
else if (selectionNeedsScroll && !(narrow && detailsMode)) {
|
|
597
|
+
keepVisible(content.findIndex((line) => line.startsWith("→")));
|
|
598
|
+
selectionNeedsScroll = false;
|
|
599
|
+
}
|
|
600
|
+
offset = Math.max(0, Math.min(Math.max(0, content.length - viewport), offset));
|
|
601
|
+
const up = keyLabel("tui.select.up", "↑");
|
|
602
|
+
const down = keyLabel("tui.select.down", "↓");
|
|
603
|
+
const enter = keyLabel("tui.select.confirm", "enter");
|
|
604
|
+
const esc = keyLabel("tui.select.cancel", "esc");
|
|
605
|
+
const scroll = content.length > viewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : "";
|
|
606
|
+
const refreshHint = timer === undefined ? "" : " · auto-refresh 1s";
|
|
607
|
+
const back = narrow && detailsMode ? "details" : "list";
|
|
608
|
+
const hint = steerMode ? "enter submit · esc back"
|
|
609
|
+
: inlineConfirm.active() ? inlineConfirm.hint(keybindings)
|
|
610
|
+
: actionMode ? `${up}/${down} actions · ${enter} run · ${keyLabel("tui.editor.cursorLeft", "←")} ${back} · ${esc} ${back}`
|
|
611
|
+
: narrow && detailsMode ? `${up}/${down} scroll · ${enter} actions · a actions · ${esc} list${scroll}${refreshHint}`
|
|
612
|
+
: `${up}/${down} select · ${enter} ${narrow ? "details" : "actions"} · a actions · ${esc} close${scroll}${refreshHint}`;
|
|
613
|
+
return [...content.slice(offset, offset + viewport), ...footer, ...(hintRows ? [styles.dim(hint)] : [])].map((line) => truncateToWidth(line, Math.max(1, width), "…"));
|
|
614
|
+
},
|
|
480
615
|
invalidate() { },
|
|
481
616
|
handleInput(data) {
|
|
482
|
-
if (disposed
|
|
617
|
+
if (disposed)
|
|
618
|
+
return;
|
|
619
|
+
if (inlineConfirm.active()) {
|
|
620
|
+
inlineConfirm.handleInput(keybindings, data);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (actionRunning)
|
|
483
624
|
return;
|
|
484
625
|
if (steerMode) {
|
|
485
626
|
if (keybindings.matches(data, "tui.select.cancel")) {
|
|
486
627
|
steerMode = false;
|
|
487
628
|
actionMode = true;
|
|
488
629
|
steerEditor.setText("");
|
|
489
|
-
offset = 0;
|
|
490
|
-
scrollActionIntoView();
|
|
491
630
|
}
|
|
492
631
|
else
|
|
493
632
|
steerEditor.handleInput(data);
|
|
494
633
|
requestRender();
|
|
495
634
|
return;
|
|
496
635
|
}
|
|
497
|
-
|
|
636
|
+
const narrow = renderedWidth < 80;
|
|
637
|
+
const page = Math.max(1, tuiRows(tui) - DASHBOARD_FOOTER_ROWS - 1);
|
|
638
|
+
if (matches(data, "tui.select.pageUp"))
|
|
639
|
+
offset = Math.max(0, offset - page);
|
|
640
|
+
else if (matches(data, "tui.select.pageDown"))
|
|
641
|
+
offset += page;
|
|
642
|
+
else if (actionMode) {
|
|
643
|
+
const actions = options();
|
|
644
|
+
if (matches(data, "tui.select.cancel") || matches(data, "tui.editor.cursorLeft")) {
|
|
645
|
+
actionMode = false;
|
|
646
|
+
offset = 0;
|
|
647
|
+
selectionNeedsScroll = true;
|
|
648
|
+
}
|
|
649
|
+
else if (matches(data, "tui.select.up"))
|
|
650
|
+
actionIndex = (actionIndex + actions.length - 1) % actions.length;
|
|
651
|
+
else if (matches(data, "tui.select.down"))
|
|
652
|
+
actionIndex = (actionIndex + 1) % actions.length;
|
|
653
|
+
else if (matches(data, "tui.select.confirm")) {
|
|
654
|
+
const action = actions[actionIndex];
|
|
655
|
+
if (action === "Steer") {
|
|
656
|
+
steerMode = true;
|
|
657
|
+
actionMode = false;
|
|
658
|
+
steerEditor.setText("");
|
|
659
|
+
}
|
|
660
|
+
else if (action && action !== "Back")
|
|
661
|
+
runAction(action);
|
|
662
|
+
else {
|
|
663
|
+
actionMode = false;
|
|
664
|
+
offset = 0;
|
|
665
|
+
selectionNeedsScroll = true;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
else if (data === "a" || data === "A") {
|
|
498
670
|
actionMode = true;
|
|
499
671
|
actionIndex = 0;
|
|
500
672
|
offset = 0;
|
|
501
|
-
scrollActionIntoView();
|
|
502
|
-
requestRender();
|
|
503
|
-
return;
|
|
504
673
|
}
|
|
505
|
-
if (
|
|
506
|
-
if (
|
|
507
|
-
actionMode = false;
|
|
508
|
-
actionIndex = 0;
|
|
509
|
-
offset = 0;
|
|
510
|
-
requestRender();
|
|
511
|
-
}
|
|
512
|
-
else
|
|
674
|
+
else if (matches(data, "tui.select.cancel")) {
|
|
675
|
+
if (!narrow || !detailsMode) {
|
|
513
676
|
close(undefined);
|
|
514
|
-
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
detailsMode = false;
|
|
680
|
+
offset = 0;
|
|
681
|
+
selectionNeedsScroll = true;
|
|
515
682
|
}
|
|
516
|
-
if (
|
|
517
|
-
if (
|
|
683
|
+
else if (narrow && detailsMode) {
|
|
684
|
+
if (matches(data, "tui.select.up"))
|
|
685
|
+
offset = Math.max(0, offset - 1);
|
|
686
|
+
else if (matches(data, "tui.select.down"))
|
|
687
|
+
offset += 1;
|
|
688
|
+
else if (matches(data, "tui.select.confirm")) {
|
|
518
689
|
actionMode = true;
|
|
519
690
|
actionIndex = 0;
|
|
520
691
|
offset = 0;
|
|
521
|
-
scrollActionIntoView();
|
|
522
|
-
requestRender();
|
|
523
|
-
}
|
|
524
|
-
else if (keybindings.matches(data, "tui.select.up")) {
|
|
525
|
-
offset = Math.max(0, offset - 1);
|
|
526
|
-
clampOffset(detailLines(inspection, theme));
|
|
527
|
-
requestRender();
|
|
528
|
-
}
|
|
529
|
-
else if (keybindings.matches(data, "tui.select.down")) {
|
|
530
|
-
offset += 1;
|
|
531
|
-
clampOffset(detailLines(inspection, theme));
|
|
532
|
-
requestRender();
|
|
533
692
|
}
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
const options = actionOptions(manager, inspection, context);
|
|
537
|
-
if (keybindings.matches(data, "tui.select.up")) {
|
|
538
|
-
actionIndex = (actionIndex + options.length - 1) % options.length;
|
|
539
|
-
scrollActionIntoView();
|
|
540
693
|
}
|
|
541
|
-
else if (
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
else if (
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
steerMode = true;
|
|
549
|
-
actionMode = false;
|
|
550
|
-
steerEditor.setText("");
|
|
551
|
-
offset = 0;
|
|
552
|
-
}
|
|
553
|
-
else if (action && action !== "Back")
|
|
554
|
-
runAction(action);
|
|
694
|
+
else if (matches(data, "tui.select.up"))
|
|
695
|
+
select(-1);
|
|
696
|
+
else if (matches(data, "tui.select.down"))
|
|
697
|
+
select(1);
|
|
698
|
+
else if (matches(data, "tui.select.confirm")) {
|
|
699
|
+
if (narrow)
|
|
700
|
+
detailsMode = true;
|
|
555
701
|
else
|
|
556
|
-
actionMode =
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
offset = Math.max(0, offset - viewportRows());
|
|
560
|
-
clampOffset(actionView().rows);
|
|
561
|
-
}
|
|
562
|
-
else if (keybindings.matches(data, "tui.select.pageDown")) {
|
|
563
|
-
offset += viewportRows();
|
|
564
|
-
clampOffset(actionView().rows);
|
|
702
|
+
actionMode = true;
|
|
703
|
+
actionIndex = 0;
|
|
704
|
+
offset = 0;
|
|
565
705
|
}
|
|
566
706
|
requestRender();
|
|
567
707
|
},
|
|
568
|
-
dispose() {
|
|
569
|
-
disposed = true;
|
|
570
|
-
stopRefresh();
|
|
571
|
-
} },
|
|
708
|
+
dispose() { shutDown(); },
|
|
572
709
|
};
|
|
573
710
|
});
|
|
574
|
-
if (
|
|
711
|
+
if (result === undefined)
|
|
575
712
|
return;
|
|
576
713
|
try {
|
|
577
|
-
await steerSubagent(manager, storageDirectory, entry, context, result.message);
|
|
714
|
+
await steerSubagent(manager, storageDirectory, result.entry, context, result.message);
|
|
578
715
|
}
|
|
579
716
|
catch (error) {
|
|
580
717
|
context.ui.notify(`Cannot steer: ${errorText(error)}`, "warning");
|
|
581
718
|
}
|
|
582
|
-
return "exit";
|
|
583
719
|
}
|
|
584
720
|
async function runNavigator(manager, storageDirectory, args, context, clipboard) {
|
|
585
721
|
if (args.trim()) {
|
|
@@ -592,20 +728,33 @@ async function runNavigator(manager, storageDirectory, args, context, clipboard)
|
|
|
592
728
|
context.ui.notify("No durable subagent runs in this session.", "info");
|
|
593
729
|
return;
|
|
594
730
|
}
|
|
731
|
+
const labels = pickerLabels(entries);
|
|
595
732
|
if (!context.hasUI) {
|
|
596
|
-
context.ui.notify(
|
|
733
|
+
context.ui.notify(labels.join("\n"), "info");
|
|
597
734
|
return;
|
|
598
735
|
}
|
|
599
|
-
|
|
600
|
-
|
|
736
|
+
if (context.mode === "tui") {
|
|
737
|
+
await showDashboard(manager, storageDirectory, entries, context, clipboard);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const choice = await context.ui.select("Subagents\n", [...labels, "Close", ...bulkDeleteLabels(manager, entries)]);
|
|
601
741
|
if (!choice || choice === "Close")
|
|
602
742
|
return;
|
|
743
|
+
const bulk = BULK_DELETE[choice];
|
|
744
|
+
if (bulk !== undefined) {
|
|
745
|
+
try {
|
|
746
|
+
await deleteAll(manager, storageDirectory, context, bulk, (title, message) => context.ui.confirm(title, message));
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
context.ui.notify(`Cannot delete ${bulk} runs: ${errorText(error)}`, "warning");
|
|
750
|
+
}
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
603
753
|
const selected = entries[labels.indexOf(choice)];
|
|
604
754
|
if (!selected)
|
|
605
755
|
return;
|
|
606
756
|
try {
|
|
607
|
-
|
|
608
|
-
return;
|
|
757
|
+
await showDetail(manager, storageDirectory, selected, context, clipboard);
|
|
609
758
|
}
|
|
610
759
|
catch (error) {
|
|
611
760
|
context.ui.notify(`Cannot inspect subagent ${selected.status.id}: ${errorText(error)}`, "warning");
|