@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.17.0-fork.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/src/agent-execution.js +36 -51
  3. package/dist/src/background-widget.d.ts +56 -0
  4. package/dist/src/background-widget.js +22 -15
  5. package/dist/src/decoders.js +2 -1
  6. package/dist/src/host-navigator.d.ts +18 -0
  7. package/dist/src/host-navigator.js +71 -13
  8. package/dist/src/host-view.d.ts +16 -1
  9. package/dist/src/host-view.js +109 -42
  10. package/dist/src/host.js +57 -24
  11. package/dist/src/index.d.ts +3 -1
  12. package/dist/src/index.js +867 -545
  13. package/dist/src/index.js.map +3 -3
  14. package/dist/src/registry.js +2 -1
  15. package/dist/src/roles.d.ts +70 -0
  16. package/dist/src/roles.js +325 -0
  17. package/dist/src/settings.d.ts +13 -0
  18. package/dist/src/settings.js +253 -0
  19. package/dist/src/trajectory.d.ts +29 -4
  20. package/dist/src/trajectory.js +58 -3
  21. package/dist/src/types.d.ts +0 -1
  22. package/dist/src/utils.d.ts +2 -1
  23. package/dist/src/utils.js +10 -0
  24. package/dist/src/validation.d.ts +5 -17
  25. package/dist/src/validation.js +9 -458
  26. package/dist/subagents/index.js +1132 -694
  27. package/dist/subagents/index.js.map +3 -3
  28. package/dist/subagents/src/contracts.d.ts +4 -0
  29. package/dist/subagents/src/index.d.ts +1 -1
  30. package/dist/subagents/src/index.js +2 -1
  31. package/dist/subagents/src/manager.js +29 -10
  32. package/dist/subagents/src/navigator.js +396 -247
  33. package/dist/subagents/src/view.d.ts +23 -2
  34. package/dist/subagents/src/view.js +96 -30
  35. package/dist/trajectory/assets/index.html +347 -231
  36. package/dist/trajectory/assets/prism.min.js +13 -0
  37. package/dist/trajectory/index.js +377 -257
  38. package/dist/trajectory/index.js.map +3 -3
  39. package/dist/trajectory/src/assets/index.html +347 -231
  40. package/dist/trajectory/src/assets/prism.min.js +13 -0
  41. package/dist/trajectory/src/export.js +4 -2
  42. package/dist/trajectory/src/index.d.ts +2 -0
  43. package/dist/trajectory/src/index.js +143 -42
  44. package/dist/trajectory/src/server.js +134 -9
  45. package/dist/trajectory/src/server.js.map +1 -1
  46. package/package.json +2 -1
  47. package/src/agent-execution.ts +36 -42
  48. package/src/background-widget.ts +25 -18
  49. package/src/decoders.ts +2 -1
  50. package/src/host-navigator.ts +58 -14
  51. package/src/host-view.ts +93 -37
  52. package/src/host.ts +51 -21
  53. package/src/index.ts +3 -1
  54. package/src/registry.ts +2 -1
  55. package/src/roles.ts +326 -0
  56. package/src/settings.ts +206 -0
  57. package/src/trajectory.ts +76 -7
  58. package/src/types.ts +1 -1
  59. package/src/utils.ts +8 -1
  60. package/src/validation.ts +12 -356
  61. package/subagents/README.md +1 -1
  62. package/subagents/src/contracts.ts +4 -0
  63. package/subagents/src/index.ts +3 -2
  64. package/subagents/src/manager.ts +28 -10
  65. package/subagents/src/navigator.ts +336 -180
  66. package/subagents/src/view.ts +97 -28
  67. package/trajectory/src/assets/index.html +347 -231
  68. package/trajectory/src/assets/prism.min.js +13 -0
  69. package/trajectory/src/export.ts +4 -2
  70. package/trajectory/src/index.ts +118 -38
  71. package/trajectory/src/server.ts +112 -12
@@ -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 pickerLabel(entry, index) { return `${String(index + 1)}. label=${boundedText(requestLabel(entry.request), 256)} role=${boundedText(requestRole(entry.request), 256)} [${entry.status.state}] ${shortId(entry.status.id)}`; }
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
- function detailLines(inspection, theme) {
122
- const { entry, record } = inspection;
123
- const { status, request } = entry;
124
- const styles = theme === undefined ? undefined : themeWorkflowProgressStyles(theme);
125
- const lines = [
126
- theme?.bold(theme.fg("accent", `Subagent ${boundedText(status.id, 256)}`)) ?? `Subagent ${boundedText(status.id, 256)}`,
127
- `label=${boundedText(requestLabel(request), 256)} role=${boundedText(requestRole(request), 256)}`,
128
- ...formatAgentDetail(detailPresentation(inspection), styles, status.finishedAt ?? Date.now()),
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
- const startedAt = timestamp(status.startedAt);
131
- const finishedAt = timestamp(status.finishedAt);
132
- if (startedAt)
133
- lines.push(`startedAt=${startedAt}`);
134
- if (finishedAt)
135
- lines.push(`finishedAt=${finishedAt}`);
136
- if (request?.prompt)
137
- appendValue(lines, "prompt", request.prompt);
138
- if (entry.requestError)
139
- lines.push(`request=unavailable: ${boundedText(entry.requestError)}`);
140
- if (status.worktree)
141
- lines.push(`worktree=${boundedText(status.worktree.path)} branch=${boundedText(status.worktree.branch)}`);
142
- const toolCalls = status.progress?.toolCalls;
143
- if (toolCalls?.length) {
144
- lines.push(`toolCalls=${String(toolCalls.length)}`);
145
- for (const call of toolCalls.slice(-MAX_DETAIL_TOOL_CALLS))
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
- function actionOptions(manager, inspection, context) {
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
- return agentActionLabels({
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 "stay";
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 "stay";
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 "stay";
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 "stay";
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 "stay";
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 "stay";
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 "stay";
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 "retry";
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
- if (context.mode !== "tui") {
299
- for (;;) {
300
- const action = await context.ui.select(detailLines(inspection).join("\n"), actionOptions(manager, inspection, context));
301
- if (!action || action === "Back")
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
- try {
304
- if (await performAction(manager, storageDirectory, entry, action, context, undefined, clipboard) === "retry")
305
- return;
306
- inspection = await inspectEntry(manager, storageDirectory, entry, context);
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
- let offset = 0;
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 refreshTimer;
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 refreshGeneration = 0;
334
- const requestRender = () => {
335
- if (!disposed)
336
- tui.requestRender();
337
- };
338
- const stopRefresh = () => {
339
- if (refreshTimer !== undefined) {
340
- clearInterval(refreshTimer);
341
- refreshTimer = undefined;
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
- refreshGeneration += 1;
426
+ catch { /* The session UI may already be closing. */ }
344
427
  };
345
- const buildActionRows = (detail, options) => [
346
- ...detail,
347
- "",
348
- theme.bold("Agent actions"),
349
- ...options.map((option, index) => `${index === actionIndex ? "→ " : " "}${index === actionIndex ? theme.fg("accent", option) : option}`),
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
- const actionView = () => {
357
- const detail = detailLines(inspection, theme);
358
- const options = actionOptions(manager, inspection, context);
359
- return { detail, options, rows: buildActionRows(detail, options) };
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 scrollActionIntoView = (view = actionView()) => {
362
- const { options, rows } = view;
363
- if (actionIndex >= options.length) {
364
- clampOffset(rows);
365
- return;
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
- const viewport = viewportRows();
368
- const actionRow = rows.length - options.length + actionIndex;
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 reportRefreshError = (error) => {
376
- if (disposed)
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
- context.ui.notify(`Cannot refresh subagent ${entry.status.id}: ${errorText(error)}`, "warning");
472
+ next = await inspectEntry(manager, storageDirectory, selected, context);
380
473
  }
381
- catch { /* The session UI may already be closing. */ }
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
- const refreshInspection = async () => {
384
- if (disposed || actionRunning || refreshing || inspection.entry.status.state !== "running")
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 generation = ++refreshGeneration;
388
- const actionIndexBeforeRefresh = actionIndex;
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 next = await inspectEntry(manager, storageDirectory, entry, context);
394
- if (generation !== refreshGeneration)
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
- inspection = next;
397
- if (inspection.entry.status.state !== "running")
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
- disposed = true;
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({ kind: "steer", message });
536
+ close({ entry: inspection.entry, message });
429
537
  };
430
- const isDisposed = () => disposed;
431
- const renderLines = (width) => {
432
- if (disposed)
433
- return [];
434
- const renderWidth = Math.max(1, width);
435
- const action = actionMode ? actionView() : undefined;
436
- const detail = (action?.detail ?? detailLines(inspection, theme)).map((line) => truncateToWidth(line, renderWidth, "…"));
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
- void performAction(manager, storageDirectory, entry, action, context, tui, clipboard).then(async (outcome) => {
450
- if (disposed)
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
- if (outcome === "retry") {
453
- close("retry");
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
- }).catch((error) => {
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
- context.ui.notify(`Cannot ${action.toLowerCase()}: ${errorText(error)}`, "warning");
564
+ await reloadAfterAction();
468
565
  }).finally(() => {
469
566
  actionRunning = false;
470
- if (!disposed)
471
- requestRender();
567
+ requestRender();
472
568
  });
473
569
  };
474
- if (inspection.entry.status.state === "running") {
475
- refreshTimer = setInterval(() => { void refreshInspection().catch(reportRefreshError); }, 1000);
476
- unrefTimer(refreshTimer);
477
- }
570
+ syncTimer();
478
571
  return {
479
- render: renderLines,
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 || actionRunning)
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
- if (!actionMode && data === "a") {
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 (keybindings.matches(data, "tui.select.cancel")) {
506
- if (actionMode) {
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
- return;
677
+ return;
678
+ }
679
+ detailsMode = false;
680
+ offset = 0;
681
+ selectionNeedsScroll = true;
515
682
  }
516
- if (!actionMode) {
517
- if (keybindings.matches(data, "tui.select.confirm")) {
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 (keybindings.matches(data, "tui.select.down")) {
542
- actionIndex = (actionIndex + 1) % options.length;
543
- scrollActionIntoView();
544
- }
545
- else if (keybindings.matches(data, "tui.select.confirm")) {
546
- const action = options[actionIndex];
547
- if (action === "Steer") {
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 = false;
557
- }
558
- else if (keybindings.matches(data, "tui.select.pageUp")) {
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() { if (!disposed) {
569
- disposed = true;
570
- stopRefresh();
571
- } },
708
+ dispose() { shutDown(); },
572
709
  };
573
710
  });
574
- if (!result || result === "retry")
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(entries.map((entry, index) => pickerLabel(entry, index)).join("\n"), "info");
733
+ context.ui.notify(labels.join("\n"), "info");
597
734
  return;
598
735
  }
599
- const labels = entries.map(pickerLabel);
600
- const choice = await context.ui.select("Subagents\n", [...labels, "Close"]);
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
- if (await showDetail(manager, storageDirectory, selected, context, clipboard) === "exit")
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");