pi-extensible-workflows 3.2.0 → 3.3.0

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/dist/src/host.js CHANGED
@@ -16,7 +16,7 @@ import { launchScriptForSnapshot, loadAgentDefinitions, preflight, resolveAgentR
16
16
  import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistry } from "./registry.js";
17
17
  import { agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath } from "./execution.js";
18
18
  import { openWorkflowArtifact, workflowResultArtifact, workflowScriptArtifact } from "./workflow-artifacts.js";
19
- import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WORKFLOW_AGENT_STATE_CHANGED_EVENT, WORKFLOW_BUDGET_EVENT, WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENT, WORKFLOW_PHASE_CHANGED_EVENT, WORKFLOW_RUN_COMPLETED_EVENT, WORKFLOW_RUN_FAILED_EVENT, WORKFLOW_RUN_RESUMED_EVENT, WORKFLOW_RUN_STARTED_EVENT, WORKFLOW_RUN_STATE_CHANGED_EVENT, WORKFLOW_WORKTREE_CREATED_EVENT, WorkflowError } from "./types.js";
19
+ import { ERROR_CODES, LAUNCH_SNAPSHOT_IDENTITY_VERSION, WORKFLOW_AGENT_STALL_THRESHOLD_MS, WORKFLOW_AGENT_STATE_CHANGED_EVENT, WORKFLOW_BUDGET_EVENT, WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENT, WORKFLOW_PHASE_CHANGED_EVENT, WORKFLOW_RUN_COMPLETED_EVENT, WORKFLOW_RUN_FAILED_EVENT, WORKFLOW_RUN_RESUMED_EVENT, WORKFLOW_RUN_STARTED_EVENT, WORKFLOW_RUN_STATE_CHANGED_EVENT, WORKFLOW_WORKTREE_CREATED_EVENT, WorkflowError } from "./types.js";
20
20
  const SETTLED_AGENT_STATES = new Set(["completed", "failed", "cancelled"]);
21
21
  const INTERNAL_WORKFLOW_TOOLS = ["workflow", "workflow_respond", "workflow_stop", "workflow_resume", "workflow_retry", "workflow_catalog"];
22
22
  const HARD_TERMINAL_RUN_STATES = new Set(["completed", "failed", "stopped"]);
@@ -88,6 +88,8 @@ function mainAgentError(error) {
88
88
  Object.assign(presented, typed);
89
89
  return presented;
90
90
  }
91
+ function workflowFailedAt(error) { return object(error) && typeof error.failedAt === "string" && error.failedAt ? error.failedAt : undefined; }
92
+ function persistedFailure(run, error) { const failedAt = workflowFailedAt(error); return { ...run, error: { code: error.code, message: error.message, ...(failedAt ? { failedAt } : {}) }, ...(failedAt ? { failedAt } : {}) }; }
91
93
  export class RunLifecycle {
92
94
  changed;
93
95
  #state;
@@ -170,7 +172,7 @@ export function formatWorkflowPreview(args) {
170
172
  }
171
173
  export const WORKFLOW_TOOL_LABEL = "Workflow";
172
174
  export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline parallel-to-summary path by default";
173
- export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Inline launches require an explicit non-empty name; registered function launches reject name and use workflow as the run name. Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Use workflow_retry with an explicit failed run ID; parentRunId only reuses named worktrees. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. Foreground results include the completed run ID. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId }) replays a failed run into a child; workflow_resume({ runId, budget? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes.";
175
+ export const WORKFLOW_TOOL_PROMPT_SNIPPET = "Run a deterministic, resumable JavaScript workflow. Prefer a named inline script that fans out independent work with parallel(...), awaits the keyed results before interpolating them into one summarizing agent(...), and returns. Inline launches require an explicit non-empty name; registered function launches reject name and use workflow as the run name. Advanced controls include registered functions, outputSchema, budgets, checkpoints, worktrees, retry/resume, CLI export, and pipelines. Use workflow_retry with an explicit failed run ID; parentRunId only reuses named worktrees. Runs are in the background by default; completion arrives as a follow-up message. Set foreground: true when the caller must wait for the final value. If a foreground call detaches before its result is accepted, its terminal success or failure is promoted to one follow-up message. Foreground results include the completed run ID. Recovery inherits the source launch mode; legacy snapshots without launchMode recover in the background. Set foreground: true or false on workflow_resume/workflow_retry to override it; foreground recovery waits for terminal value and run details, while background recovery returns immediately with a follow-up. Recovery map: agent(..., { retries }) reruns one agent call in the same run for transient failures; workflow_retry({ runId, foreground? }) replays a failed run into a child; workflow_resume({ runId, budget?, foreground? }) continues a budget_exhausted run; parentRunId on a new launch only borrows named worktrees and never replays or resumes.";
174
176
  function workflowRecoveryGuidance(action, state) {
175
177
  if (action === "resume") {
176
178
  if (state === "failed")
@@ -204,7 +206,7 @@ export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
204
206
  budget: Type.Optional(Type.Unknown({ description: "Advanced: optional aggregate soft and hard run budgets" })),
205
207
  parentRunId: Type.Optional(Type.String({ description: "Advanced: terminal run whose named worktrees may be reused" })),
206
208
  });
207
- export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }) });
209
+ export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) });
208
210
  function phaseNames(source) {
209
211
  const phases = source === undefined ? [] : Array.isArray(source) ? source : source.phases ?? [];
210
212
  return phases.filter((phase) => typeof phase === "string" && phase.trim() !== "").map((phase) => phase.trim());
@@ -547,7 +549,7 @@ function styleForState(map, state, styles) {
547
549
  function progressStyleForState(state, styles) { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
548
550
  function workflowIconStyle(state, styles) { return styleForState(WORKFLOW_ICON_STYLE, state, styles); }
549
551
  function phaseStyleForState(state, styles) { return styleForState(PHASE_STATE_STYLE, state, styles); }
550
- export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
552
+ export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
551
553
  const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
552
554
  const workflowIcon = runStateGlyph(run.state, spinner);
553
555
  const iconStyle = workflowIconStyle(run.state, styles);
@@ -555,11 +557,14 @@ export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORK
555
557
  const lines = [`${iconStyle(workflowIcon)} ${header}`];
556
558
  const budgetWarning = run.state === "budget_exhausted" || (run.budgetEvents ?? []).some((event) => event.type === "hard_exhausted");
557
559
  lines.push(...formatCompactBudgetStatus(run).map((line) => ` ${budgetWarning ? styles.warning(line) : line}`));
560
+ const activeShells = run.activeShells ?? 0;
561
+ if (activeShells > 0)
562
+ lines.push(` ${styles.accent(spinner)} shell ${styles.accent("[running]")} ${styles.dim(`(${String(activeShells)} active)`)}`);
558
563
  const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
559
564
  const renderAgents = (agents, offset, nested) => renderGroupedAgents(agents, ({ agent, index, depth }, grouped) => {
560
565
  const icon = agentStateGlyph(agent.state, spinner);
561
566
  const indent = " ".repeat((grouped ? 2 : 1) + depth);
562
- const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles);
567
+ const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles, now);
563
568
  const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
564
569
  const state = progressStyleForState(agent.state, styles);
565
570
  return `${indent}#${String(offset + index + 1)} ${state(icon)} ${name} ${state(`[${agent.state}]`)}${activity ? ` ${activity}` : ""}`;
@@ -623,7 +628,7 @@ function controlState(state, theme) {
623
628
  return theme.fg(color, state);
624
629
  }
625
630
  function controlAction(action, theme) {
626
- const color = /approved|stopped|started|resumed/.test(action) ? "success" : /rejected|failed/.test(action) ? "error" : "warning";
631
+ const color = /approved|completed|stopped|started|resumed/.test(action) ? "success" : /rejected|failed/.test(action) ? "error" : "warning";
627
632
  return theme.fg(color, action);
628
633
  }
629
634
  function budgetPatchEntries(value) {
@@ -681,14 +686,15 @@ function workflowControlResult(name, args, result, expanded, theme, isError) {
681
686
  if (name === "workflow_retry") {
682
687
  const childRunId = controlString(value.runId) ?? "(unknown)";
683
688
  const state = controlString(value.state) ?? "unknown";
689
+ const action = state === "completed" ? "completed" : "started";
684
690
  if (!expanded)
685
- return [title, `Source ${theme.fg("accent", runId)}`, `Child ${theme.fg("accent", childRunId)} · ${controlState(state, theme)} · ${controlAction("started", theme)}`].join("\n");
686
- return [title, `Source run: ${theme.fg("accent", runId)}`, `Retry run: ${theme.fg("accent", childRunId)}`, `State: ${controlState(state, theme)}`, `Action: ${controlAction("started; completed work will be replayed", theme)}`].join("\n");
691
+ return [title, `Source ${theme.fg("accent", runId)}`, `Child ${theme.fg("accent", childRunId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`].join("\n");
692
+ return [title, `Source run: ${theme.fg("accent", runId)}`, `Retry run: ${theme.fg("accent", childRunId)}`, `State: ${controlState(state, theme)}`, `Action: ${controlAction(action === "completed" ? "completed" : "started; completed work will be replayed", theme)}`].join("\n");
687
693
  }
688
694
  if (name === "workflow_resume") {
689
695
  const state = controlString(value.state) ?? "unknown";
690
696
  const proposalId = controlString(value.proposalId);
691
- const action = state === "awaiting_approval" ? "approval required" : state === "running" ? "resumed" : "no change";
697
+ const action = state === "awaiting_approval" ? "approval required" : state === "running" ? "resumed" : state === "completed" ? "completed" : "no change";
692
698
  if (!expanded)
693
699
  return [title, `Run ${theme.fg("accent", runId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`, ...(proposalId ? [`Proposal ${theme.fg("accent", proposalId)}`] : [])].join("\n");
694
700
  return [title, `Run: ${theme.fg("accent", runId)}`, `State: ${controlState(state, theme)}`, `Action: ${controlAction(action, theme)}`, ...(proposalId ? [`Proposal: ${theme.fg("accent", proposalId)}`] : []), ...budgetPatchDetails(args.budget, theme)].join("\n");
@@ -888,15 +894,41 @@ function styledAgentBreadcrumb(agent, byId, styles) {
888
894
  return parts[0] ?? "";
889
895
  return `${styles.muted(parts.slice(0, -1).join(" > "))} > ${styles.bold(parts[parts.length - 1] ?? "")}`;
890
896
  }
891
- function formatAgentActivity(agent, spinner, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
897
+ export function formatStalledDuration(durationMs) {
898
+ const minutes = Math.max(0, Math.floor(durationMs / 60_000));
899
+ if (minutes < 60)
900
+ return `${String(minutes)}m`;
901
+ const hours = Math.floor(minutes / 60);
902
+ const remainingMinutes = minutes % 60;
903
+ return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
904
+ }
905
+ function stalledDuration(agent, now) {
906
+ if (agent.state !== "running" || agent.lastEventAt === undefined || !Number.isFinite(agent.lastEventAt))
907
+ return undefined;
908
+ const duration = now - agent.lastEventAt;
909
+ return duration >= WORKFLOW_AGENT_STALL_THRESHOLD_MS ? duration : undefined;
910
+ }
911
+ function formatAgentActivity(agent, spinner, styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
892
912
  const label = agent.activity?.kind === "reasoning" ? "reasoning" : agent.activity?.kind === "text" ? "responding" : agent.activity?.kind === "tool" ? agent.activity.text : [...(agent.toolCalls ?? [])].reverse().find(({ state }) => state === "running")?.name ?? "";
893
- return label ? `${styles.accent(spinner)} ${styles.dim(label)}` : "";
913
+ const activity = label ? `${styles.accent(spinner)} ${styles.dim(label)}` : "";
914
+ const stalled = stalledDuration(agent, now);
915
+ if (stalled === undefined)
916
+ return activity;
917
+ const warning = `stalled? ${formatStalledDuration(stalled)}`;
918
+ return activity ? `${activity} ${styles.warning(`- ${warning}`)}` : styles.warning(warning);
919
+ }
920
+ function formatAccountingValue(value) {
921
+ return new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(value).toLowerCase();
894
922
  }
895
923
  function formatAccounting(accounting) {
896
924
  const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
897
- return `${new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(total).toLowerCase()} tok`;
925
+ return `${formatAccountingValue(total)} tok`;
926
+ }
927
+ function formatAgentAccounting(accounting) {
928
+ const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
929
+ return [`Tokens: ${formatAccountingValue(total)} (in=${formatAccountingValue(accounting.input)} out=${formatAccountingValue(accounting.output)} cache-read=${formatAccountingValue(accounting.cacheRead)} cache-write=${formatAccountingValue(accounting.cacheWrite)})`, `Cost: $${accounting.cost.toFixed(2)}`];
898
930
  }
899
- export function formatNavigatorDashboard(run, checkpoints, worktrees) {
931
+ export function formatNavigatorDashboard(run, checkpoints, worktrees, now = Date.now()) {
900
932
  void worktrees;
901
933
  const done = run.agents.filter((a) => SETTLED_AGENT_STATES.has(a.state)).length;
902
934
  const totalAccounting = run.agents.reduce((sum, a) => ({ input: sum.input + (a.accounting?.input ?? 0), output: sum.output + (a.accounting?.output ?? 0), cacheRead: sum.cacheRead + (a.accounting?.cacheRead ?? 0), cacheWrite: sum.cacheWrite + (a.accounting?.cacheWrite ?? 0), cost: sum.cost + (a.accounting?.cost ?? 0) }), { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 });
@@ -922,7 +954,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
922
954
  if (last?.error)
923
955
  result.push(`${indent} error: ${last.error.code}: ${last.error.message}`);
924
956
  }
925
- const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦") : "";
957
+ const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
926
958
  if (activity)
927
959
  result.push(`${indent} ${activity}`);
928
960
  return result.join("\n");
@@ -935,7 +967,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
935
967
  }
936
968
  return lines.join("\n");
937
969
  }
938
- export function formatNavigatorRun(loaded, checkpoints, worktrees) {
970
+ export function formatNavigatorRun(loaded, checkpoints, worktrees, now = Date.now()) {
939
971
  const { run, snapshot } = loaded;
940
972
  const lines = [
941
973
  `Workflow: ${run.workflowName}`,
@@ -971,6 +1003,9 @@ export function formatNavigatorRun(loaded, checkpoints, worktrees) {
971
1003
  result.push(`${indent} attempt ${String(attempt.attempt)}${attempt.error ? ` error=${attempt.error.code}: ${attempt.error.message}` : ""}`);
972
1004
  for (const call of agent.toolCalls ?? [])
973
1005
  result.push(`${indent} tool ${call.name} state=${call.state}`);
1006
+ const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
1007
+ if (activity)
1008
+ result.push(`${indent} ${activity}`);
974
1009
  return result.join("\n");
975
1010
  }));
976
1011
  lines.push("Checkpoints:");
@@ -982,7 +1017,7 @@ export function formatNavigatorRun(loaded, checkpoints, worktrees) {
982
1017
  lines.push(`Native Pi transcripts: ${String(run.nativeSessions.length)}`);
983
1018
  return lines.join("\n");
984
1019
  }
985
- export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {}, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
1020
+ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {}, styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
986
1021
  const safeWidth = Math.max(1, width);
987
1022
  const model = buildWorkflowPhaseModel(run, snapshot);
988
1023
  const tree = buildWorkflowPhaseTree(model);
@@ -1022,7 +1057,8 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
1022
1057
  const treeLine = (node) => {
1023
1058
  const selected = node.id === selectedNode?.id;
1024
1059
  const state = progressStyleForState(node.state, styles);
1025
- return `${selected ? "→" : " "} ${" ".repeat(node.depth)}${nodeIcon(node)} ${node.label} · ${state(node.state)}`;
1060
+ const activity = node.agent && !SETTLED_AGENT_STATES.has(node.agent.state) ? formatAgentActivity(node.agent, "⠦", styles, now) : "";
1061
+ return `${selected ? "→" : " "} ${" ".repeat(node.depth)}${nodeIcon(node)} ${node.label} · ${state(node.state)}${activity ? ` ${activity}` : ""}`;
1026
1062
  };
1027
1063
  const details = (node) => {
1028
1064
  if (!node)
@@ -1041,12 +1077,15 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
1041
1077
  if (!agent)
1042
1078
  return [styles.muted("Agent details are unavailable")];
1043
1079
  const byId = new Map(run.agents.map((candidate) => [candidate.id, candidate]));
1044
- const result = [styles.bold(`Selected agent: ${agentBreadcrumb(agent, byId, true)}`), `State: ${phaseStyle(agent.state)(agent.state)}`, `Structural path: ${agent.structuralPath?.join(" > ") || "(root)"}`, `Model: ${agent.model.provider}/${agent.model.model}${agent.model.thinking ? `:${agent.model.thinking}` : ""}`, `Role: ${agent.role ?? "(none)"}`, `Tools: ${agent.tools.join(", ") || "(none)"}`, `Attempts: ${String(agent.attempts)}`, ...(selection.actions ? [] : [styles.muted("enter for agent actions")])];
1080
+ const result = [styles.bold(`Selected agent: ${agentBreadcrumb(agent, byId, true)}`), `State: ${phaseStyle(agent.state)(agent.state)}`, `Structural path: ${agent.structuralPath?.join(" > ") || "(root)"}`, `Model: ${agent.model.provider}/${agent.model.model}${agent.model.thinking ? `:${agent.model.thinking}` : ""}`, `Role: ${agent.role ?? "(none)"}`, `Tools: ${agent.tools.join(", ") || "(none)"}`, `Attempts: ${String(agent.attempts)}`, ...(agent.accounting ? formatAgentAccounting(agent.accounting) : []), ...(selection.actions ? [] : [styles.muted("enter for agent actions")])];
1045
1081
  const error = agent.attemptDetails?.at(-1)?.error;
1046
1082
  if (error)
1047
1083
  result.push(styles.error(`Error: ${error.code}: ${error.message}`));
1048
1084
  if (agent.activity)
1049
1085
  result.push(`Activity: ${agent.activity.text}`);
1086
+ const stalled = stalledDuration(agent, now);
1087
+ if (stalled !== undefined)
1088
+ result.push(styles.warning(`stalled? ${formatStalledDuration(stalled)}`));
1050
1089
  return result;
1051
1090
  };
1052
1091
  const stateNames = ["not started", "running", "completed", "failed", "cancelled", "interrupted", "budget_exhausted"];
@@ -1288,15 +1327,35 @@ export function formatWorkflowFailureDiagnostics(diagnostic) {
1288
1327
  const retry = diagnostic.retry ? [` Retry: ${diagnostic.retry.action}`, ` Replayable completed paths: ${diagnostic.retry.completedPaths.join(", ") || "(none)"}`, ` Incomplete paths: ${diagnostic.retry.incompletePaths.join(", ") || "(unknown)"}`, ` Named worktrees: ${diagnostic.retry.namedWorktrees.join(", ") || "(none)"}`, ` Warning: ${diagnostic.retry.warning}`] : [];
1289
1328
  return [`✗ Workflow: ${diagnostic.workflowName}`, ` Run: ${diagnostic.runId}`, ` State: ${diagnostic.state}`, ` Error: ${diagnostic.error.code}: ${diagnostic.error.message}`, ` Failed at: ${diagnostic.failedAt ?? "(unknown)"}`, ` Failed agent: ${failedAgent}`, ` Completed sibling ${siblingAgents ? "agents" : "paths"}: ${siblings}`, ...retry, ` Artifacts: state=${diagnostic.artifacts.statePath} journal=${diagnostic.artifacts.journalPath}`].join("\n");
1290
1329
  }
1330
+ function deliveryPart(value, maxBytes) { return utf8Prefix(value.replace(/\s+/g, " ").trim(), maxBytes) || "(unknown)"; }
1331
+ export function formatWorkflowFailureDelivery(diagnostic) {
1332
+ const name = deliveryPart(diagnostic.workflowName, 128);
1333
+ const runId = deliveryPart(diagnostic.runId, 128);
1334
+ const error = `${diagnostic.error.code}: ${deliveryPart(diagnostic.error.message, 768)}`;
1335
+ const failedPath = diagnostic.failedAt ? `; failed path=${deliveryPart(diagnostic.failedAt, 512)}` : "";
1336
+ const nextAction = diagnostic.retry ? `; next action: ${deliveryPart(diagnostic.retry.action, 256)}` : "";
1337
+ const artifacts = `; artifacts: runDirectory=${deliveryPart(diagnostic.artifacts.runDirectory, 512)} statePath=${deliveryPart(diagnostic.artifacts.statePath, 512)} journalPath=${deliveryPart(diagnostic.artifacts.journalPath, 512)}`;
1338
+ const line = `Workflow ${name} failed (runId=${runId}): error=${error}${failedPath}${nextAction}${artifacts}`;
1339
+ return Buffer.byteLength(line) <= DELIVERY_LIMIT_BYTES ? line : utf8Prefix(line, DELIVERY_LIMIT_BYTES);
1340
+ }
1341
+ function formatWorkflowFailureDeliveryFallback(workflowName, runId, runDirectory, error) {
1342
+ const code = errorCode(error) ?? "INTERNAL_ERROR";
1343
+ const failedPath = workflowFailedAt(error);
1344
+ const nextAction = code === "BUDGET_EXHAUSTED" || code === "CANCELLED" ? "" : `; next action: workflow_retry({ runId: ${JSON.stringify(runId)} })`;
1345
+ const line = `Workflow ${deliveryPart(workflowName, 128)} failed (runId=${deliveryPart(runId, 128)}): error=${code}: ${deliveryPart(formatWorkflowFailure(error), 768)}${failedPath ? `; failed path=${deliveryPart(failedPath, 512)}` : ""}${nextAction}; artifacts: runDirectory=${deliveryPart(runDirectory, 512)} statePath=${deliveryPart(join(runDirectory, "state.json"), 512)} journalPath=${deliveryPart(join(runDirectory, "journal.json"), 512)}`;
1346
+ return Buffer.byteLength(line) <= DELIVERY_LIMIT_BYTES ? line : utf8Prefix(line, DELIVERY_LIMIT_BYTES);
1347
+ }
1291
1348
  function serializeWorkflowFailureDiagnostics(diagnostic) { return JSON.stringify(diagnostic); }
1292
1349
  function isWorkflowFailureDiagnostics(value) {
1293
1350
  return object(value) && typeof value.runId === "string" && typeof value.workflowName === "string" && typeof value.state === "string" && "failedAt" in value && object(value.error) && object(value.artifacts);
1294
1351
  }
1295
1352
  function deliver(pi, content) {
1353
+ if (typeof pi.sendMessage !== "function")
1354
+ return;
1296
1355
  pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
1297
1356
  }
1298
1357
  function deliverFailure(pi, diagnostic) {
1299
- deliver(pi, `Workflow ${utf8Prefix(diagnostic.workflowName, 128)} failure diagnostics: ${serializeWorkflowFailureDiagnostics(diagnostic)}`);
1358
+ deliver(pi, formatWorkflowFailureDelivery(diagnostic));
1300
1359
  }
1301
1360
  function safeEventError(error) {
1302
1361
  const code = errorCode(error) ?? "INTERNAL_ERROR";
@@ -1633,6 +1692,7 @@ function tuiRows(tui) {
1633
1692
  const rows = object(tui) && object(tui.terminal) ? tui.terminal.rows : undefined;
1634
1693
  return typeof rows === "number" && Number.isFinite(rows) ? rows : 24;
1635
1694
  }
1695
+ const WORKFLOW_PANEL_FOOTER_ROWS = 2;
1636
1696
  const WORKFLOW_OVERLAY_BORDER_ROWS = 2;
1637
1697
  const WORKFLOW_OVERLAY_TOP_MARGIN = 1;
1638
1698
  const WORKFLOW_OVERLAY_OPTIONS = { anchor: "top-left", width: "100%", maxHeight: "100%", margin: { top: WORKFLOW_OVERLAY_TOP_MARGIN } };
@@ -1649,7 +1709,15 @@ function keybindingKeys(keybindings, name) {
1649
1709
  const getKeys = object(keybindings) ? asFn(keybindings.getKeys) : undefined;
1650
1710
  return getKeys ? getKeys.call(keybindings, name) : undefined;
1651
1711
  }
1652
- export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession, agentDir) {
1712
+ const WORKFLOW_VIM_KEYS = { "tui.select.up": "k", "tui.select.down": "j", "tui.editor.cursorLeft": "h", "tui.editor.cursorRight": "l" };
1713
+ function workflowKeyMatches(keybindings, data, binding) { return keybindings.matches(data, binding) || WORKFLOW_VIM_KEYS[binding] === data; }
1714
+ function workflowKeyLabel(keybindings, binding, fallback, labels) {
1715
+ const keys = keybindingKeys(keybindings, binding);
1716
+ const configured = keys?.length ? keys.map((key) => labels[key] ?? key) : [fallback];
1717
+ const vim = WORKFLOW_VIM_KEYS[binding];
1718
+ return [...new Set(vim ? [...configured, vim] : configured)].join("/");
1719
+ }
1720
+ export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession, agentDir, additionalSkillPaths = []) {
1653
1721
  beginWorkflowExtensionLoading();
1654
1722
  const registry = loadingRegistry();
1655
1723
  const extensionAgentDir = agentDir ?? getAgentDir();
@@ -1702,16 +1770,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1702
1770
  });
1703
1771
  };
1704
1772
  const pendingFailureDiagnostics = new Map();
1705
- pi.on("tool_result", (event) => {
1706
- if (event.toolName !== "workflow" || !event.isError)
1707
- return;
1708
- const diagnostic = pendingFailureDiagnostics.get(event.toolCallId);
1709
- if (!diagnostic)
1710
- return;
1711
- pendingFailureDiagnostics.delete(event.toolCallId);
1712
- return { content: [{ type: "text", text: serializeWorkflowFailureDiagnostics(diagnostic) }], details: diagnostic, isError: true };
1713
- });
1773
+ const foregroundDeliveries = new Map();
1714
1774
  const liveActivities = new Map();
1775
+ const liveEventTimes = new Map();
1715
1776
  const setLiveActivity = (runId, agentId, activity) => {
1716
1777
  const activities = liveActivities.get(runId);
1717
1778
  if (activity) {
@@ -1726,12 +1787,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1726
1787
  liveActivities.delete(runId);
1727
1788
  }
1728
1789
  };
1790
+ const setLiveEventTime = (runId, agentId, timestamp) => {
1791
+ if (timestamp === undefined)
1792
+ return;
1793
+ const timestamps = liveEventTimes.get(runId);
1794
+ if (timestamps)
1795
+ timestamps.set(agentId, timestamp);
1796
+ else
1797
+ liveEventTimes.set(runId, new Map([[agentId, timestamp]]));
1798
+ };
1729
1799
  const withLiveActivities = (run) => {
1730
1800
  const activities = liveActivities.get(run.id);
1731
- return activities?.size ? { ...run, agents: run.agents.map((agent) => {
1732
- const activity = activities.get(agent.id);
1733
- return activity ? { ...agent, activity } : agent;
1734
- }) } : run;
1801
+ const timestamps = liveEventTimes.get(run.id);
1802
+ if (!activities?.size && !timestamps?.size)
1803
+ return run;
1804
+ return { ...run, agents: run.agents.map((agent) => {
1805
+ const activity = activities?.get(agent.id);
1806
+ const lastEventAt = timestamps?.get(agent.id);
1807
+ if (activity === undefined && lastEventAt === undefined)
1808
+ return agent;
1809
+ return { ...agent, ...(activity === undefined ? {} : { activity }), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
1810
+ }) };
1735
1811
  };
1736
1812
  const terminalRunStates = new Map();
1737
1813
  let sessionLease;
@@ -1759,6 +1835,52 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1759
1835
  await eventPublisher.budget(store, metadata, persisted);
1760
1836
  return persisted;
1761
1837
  };
1838
+ pi.on("tool_result", async (event) => {
1839
+ const delivery = event.toolName === "workflow" ? foregroundDeliveries.get(event.toolCallId) : undefined;
1840
+ if (delivery) {
1841
+ if (delivery.timer)
1842
+ clearTimeout(delivery.timer);
1843
+ delivery.inline = true;
1844
+ await delivery.store.updateState((current) => {
1845
+ if (current.delivery?.toolCallId !== event.toolCallId || current.delivery.state === "delivered")
1846
+ return current;
1847
+ return { ...current, delivery: { ...current.delivery, state: "delivered" } };
1848
+ });
1849
+ foregroundDeliveries.delete(event.toolCallId);
1850
+ }
1851
+ if (event.toolName !== "workflow" || !event.isError)
1852
+ return;
1853
+ const diagnostic = pendingFailureDiagnostics.get(event.toolCallId);
1854
+ if (!diagnostic)
1855
+ return;
1856
+ pendingFailureDiagnostics.delete(event.toolCallId);
1857
+ return { content: [{ type: "text", text: serializeWorkflowFailureDiagnostics(diagnostic) }], details: diagnostic, isError: true };
1858
+ });
1859
+ const deliverTerminal = async (store, content) => {
1860
+ let claimed;
1861
+ await store.updateState((current) => {
1862
+ if (current.delivery?.state === "delivered")
1863
+ return current;
1864
+ if (!current.delivery) {
1865
+ claimed = true;
1866
+ return current;
1867
+ }
1868
+ claimed = true;
1869
+ return { ...current, delivery: { ...current.delivery, mode: "background", state: "delivered" } };
1870
+ });
1871
+ if (claimed === true)
1872
+ deliver(pi, content);
1873
+ };
1874
+ const scheduleForegroundDelivery = (toolCallId, send) => {
1875
+ const delivery = foregroundDeliveries.get(toolCallId);
1876
+ if (!delivery || delivery.inline || typeof pi.sendMessage !== "function")
1877
+ return;
1878
+ //NOTE: Give Pi one event-loop turn to deliver an uninterrupted tool result before promoting.
1879
+ delivery.timer = setTimeout(() => {
1880
+ delete delivery.timer;
1881
+ void send().finally(() => foregroundDeliveries.delete(toolCallId));
1882
+ }, 0);
1883
+ };
1762
1884
  const phaseBridge = (store, metadata, lifecycle) => {
1763
1885
  let cursor = 0;
1764
1886
  return async (phase) => {
@@ -1811,10 +1933,25 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1811
1933
  const replayed = await store.replay(path);
1812
1934
  if (replayed)
1813
1935
  return readShellResult(replayed.value);
1814
- const cwd = identity.worktreeOwner ? (await persistWorktree(store, metadata, identity.worktreeOwner)).cwd : store.cwd;
1815
- const result = await executeShellCommand(command, options, signal, cwd);
1816
- await store.complete(path, result);
1817
- return result;
1936
+ const started = await persistRunState(store, metadata, (current) => ({ ...current, activeShells: (current.activeShells ?? 0) + 1 }));
1937
+ runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(started)));
1938
+ try {
1939
+ const cwd = identity.worktreeOwner ? (await persistWorktree(store, metadata, identity.worktreeOwner)).cwd : store.cwd;
1940
+ const result = await executeShellCommand(command, options, signal, cwd);
1941
+ await store.complete(path, result);
1942
+ return result;
1943
+ }
1944
+ finally {
1945
+ const stopped = await persistRunState(store, metadata, (current) => {
1946
+ const activeShells = Math.max(0, (current.activeShells ?? 0) - 1);
1947
+ if (activeShells > 0)
1948
+ return { ...current, activeShells };
1949
+ const next = { ...current };
1950
+ delete next.activeShells;
1951
+ return next;
1952
+ });
1953
+ runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(stopped)));
1954
+ }
1818
1955
  }
1819
1956
  finally {
1820
1957
  await lifecycle.leave();
@@ -1825,8 +1962,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1825
1962
  budget.transition(next);
1826
1963
  const persisted = await persistRunState(store, metadata, (current) => {
1827
1964
  const nextRun = { ...current, state: next, ...budget.snapshot() };
1828
- if (next === "running" || next === "completed")
1965
+ if (next === "running" || next === "completed") {
1829
1966
  delete nextRun.error;
1967
+ delete nextRun.failedAt;
1968
+ }
1830
1969
  return nextRun;
1831
1970
  });
1832
1971
  await eventPublisher.runState(store, metadata, previous, next, reason);
@@ -1841,28 +1980,31 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1841
1980
  const onProgress = async (progress) => {
1842
1981
  let runState;
1843
1982
  if (progress.persist) {
1844
- runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity } : agent) } : current);
1983
+ runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) } : current);
1845
1984
  }
1846
1985
  else {
1847
1986
  const loaded = await run.store.load();
1848
1987
  if (!loaded.run.agents.some((agent) => agent.id === id))
1849
1988
  return;
1850
- runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity } : agent) };
1989
+ runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) };
1851
1990
  }
1852
1991
  if (!runState.agents.some((agent) => agent.id === id))
1853
1992
  return;
1854
1993
  setLiveActivity(runId, id, progress.activity);
1994
+ setLiveEventTime(runId, id, progress.lastEventAt);
1855
1995
  run.update?.(workflowToolUpdate(withLiveActivities(runState)));
1856
1996
  };
1857
1997
  const onAttempt = async (attempt) => {
1858
1998
  await scheduler.flush();
1859
1999
  scheduler.attemptStarted(id);
2000
+ const lastEventAt = Date.now();
2001
+ setLiveEventTime(runId, id, lastEventAt);
1860
2002
  await scheduler.flush();
1861
2003
  const before = (await run.store.load()).run;
1862
2004
  await persistActiveAgentAttempt(run.store, id, attempt);
1863
2005
  const active = (await run.store.load()).run;
1864
2006
  await eventPublisher.agentStates(run.store, run.metadata, before.agents, active.agents);
1865
- const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
2007
+ const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? { ...agent, lastEventAt } : agent) }));
1866
2008
  run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
1867
2009
  };
1868
2010
  const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, onProgress, onAttempt, budget, ...(run.providerErrorRecovery ? { providerErrorRecovery: run.providerErrorRecovery } : {}), ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.thinking ? { thinking: options.thinking } : {}), ...(options.role ? { role: options.role } : {}), ...(options.role ? {} : { tools: options.tools }), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}) }, signal, scheduler.toolsFor(id, (role, tools, model, inheritedTools, thinking) => run.executor.resolve({ label: "child", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(tools !== undefined ? { tools } : {}) }, inheritedTools).tools), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
@@ -1908,7 +2050,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1908
2050
  effective = previous ? { model: previous.model, ...(previous.requestedModel ? { requestedModel: previous.requestedModel } : {}), tools: previous.tools } : { model: node.options.model ? modelSpec(node.options.model, run.model) : { ...run.model, ...(node.options.thinking ? { thinking: node.options.thinking } : {}) }, ...(node.options.model ? { requestedModel: node.options.model } : {}), tools: node.options.tools };
1909
2051
  }
1910
2052
  const resultPath = !node.parentId && node.options.agentIdentity ? agentIdentityPath(node.options.agentIdentity) : undefined;
1911
- return { id: node.id, name: node.label, ...(node.options.requestedLabel ? { label: node.options.requestedLabel } : {}), path: node.id, state: node.state, ...(node.parentId ? { parentId: node.parentId } : {}), structuralPath: [...(node.options.agentIdentity?.structuralPath ?? [])], ...(resultPath ? { resultPath } : {}), ...(node.options.parentBreadcrumb ? { parentBreadcrumb: node.options.parentBreadcrumb } : {}), ...(node.options.worktreeOwner ? { worktreeOwner: node.options.worktreeOwner } : {}), ...(node.options.role ? { role: node.options.role } : {}), ...(effective.requestedModel ? { requestedModel: effective.requestedModel } : {}), model: effective.model, tools: effective.tools, attempts: previous?.attempts ?? 0, ...(previous?.attemptDetails ? { attemptDetails: previous.attemptDetails } : {}), ...(previous?.accounting ? { accounting: previous.accounting } : {}), ...(previous?.toolCalls ? { toolCalls: previous.toolCalls } : {}), ...(previous?.activity ? { activity: previous.activity } : {}) };
2053
+ const lastEventAt = node.state === "running" ? previous?.state === "running" && previous.lastEventAt !== undefined ? previous.lastEventAt : Date.now() : previous?.lastEventAt;
2054
+ return { id: node.id, name: node.label, ...(node.options.requestedLabel ? { label: node.options.requestedLabel } : {}), path: node.id, state: node.state, ...(node.parentId ? { parentId: node.parentId } : {}), structuralPath: [...(node.options.agentIdentity?.structuralPath ?? [])], ...(resultPath ? { resultPath } : {}), ...(node.options.parentBreadcrumb ? { parentBreadcrumb: node.options.parentBreadcrumb } : {}), ...(node.options.worktreeOwner ? { worktreeOwner: node.options.worktreeOwner } : {}), ...(node.options.role ? { role: node.options.role } : {}), ...(effective.requestedModel ? { requestedModel: effective.requestedModel } : {}), model: effective.model, tools: effective.tools, attempts: previous?.attempts ?? 0, ...(previous?.attemptDetails ? { attemptDetails: previous.attemptDetails } : {}), ...(previous?.accounting ? { accounting: previous.accounting } : {}), ...(previous?.toolCalls ? { toolCalls: previous.toolCalls } : {}), ...(previous?.activity ? { activity: previous.activity } : {}), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
1912
2055
  });
1913
2056
  return { ...current, agents };
1914
2057
  });
@@ -1927,6 +2070,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1927
2070
  terminalRunStates.set(runId, run.lifecycle.state);
1928
2071
  run.checkpointResolvers.clear();
1929
2072
  liveActivities.delete(runId);
2073
+ liveEventTimes.delete(runId);
1930
2074
  eventPublisher.removeRun(runId);
1931
2075
  runs.delete(runId);
1932
2076
  };
@@ -1967,7 +2111,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1967
2111
  run.budget.recordEvent({ type, budgetVersion: request.budgetVersion, dimensions: [], usage: structuredClone(request.consumed), limits: structuredClone(request.proposed), at: Date.now(), proposalId: request.proposalId, previous: structuredClone(request.previous), proposed: structuredClone(request.proposed) });
1968
2112
  await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
1969
2113
  };
1970
- const answerBudgetDecision = async (runId, proposalId, approved, silent = false, context, signal) => {
2114
+ const answerBudgetDecision = async (runId, proposalId, approved, silent = false, context, signal, waitForCompletion = true) => {
1971
2115
  const run = runs.get(runId);
1972
2116
  if (!run)
1973
2117
  return undefined;
@@ -1975,7 +2119,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1975
2119
  if (!request)
1976
2120
  return undefined;
1977
2121
  await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
1978
- const result = await applyBudgetDecision(request, approved, context, signal);
2122
+ const result = await applyBudgetDecision(request, approved, context, signal, waitForCompletion);
1979
2123
  if (!silent)
1980
2124
  deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
1981
2125
  return result;
@@ -2105,7 +2249,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2105
2249
  });
2106
2250
  catalogRegistered = true;
2107
2251
  };
2108
- const createAgentExecutor = (root) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, agentSetupHooks: registry.agentSetupHooks() }, createSession);
2252
+ const createAgentExecutor = (root) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, ...(additionalSkillPaths.length ? { additionalSkillPaths } : {}), agentSetupHooks: registry.agentSetupHooks() }, createSession);
2109
2253
  const activeSnapshotTools = (tools, active) => active === "session"
2110
2254
  ? new Set(tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog"))
2111
2255
  : new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
@@ -2135,7 +2279,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2135
2279
  const snapshot = createLaunchSnapshot({ ...input.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
2136
2280
  return { active, settingsPath, resolution, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script };
2137
2281
  };
2138
- const workflowAgentHandler = (store, metadata, lifecycle, executor, cwd, runId) => async (prompt, options, agentSignal, identity) => {
2282
+ const workflowAgentHandler = (store, metadata, lifecycle, executor, cwd, runId, captureRole) => async (prompt, options, agentSignal, identity) => {
2139
2283
  await lifecycle.enter();
2140
2284
  try {
2141
2285
  const path = agentIdentityPath(identity);
@@ -2150,6 +2294,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2150
2294
  const thinking = parseThinking(options.thinking);
2151
2295
  const requestedLabel = typeof options.label === "string" ? options.label : undefined;
2152
2296
  const resolved = executor.resolve({ label: requestedLabel ?? role ?? "agent", workflowName: metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(Array.isArray(options.tools) ? { tools: options.tools } : {}) });
2297
+ if (role)
2298
+ await captureRole?.(role, resolved.model);
2153
2299
  const label = displayAgentName(requestedLabel, role, resolved.model);
2154
2300
  const tools = resolved.tools;
2155
2301
  const schema = object(options.outputSchema) ? options.outputSchema : undefined;
@@ -2182,8 +2328,21 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2182
2328
  if (drift.length)
2183
2329
  await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
2184
2330
  };
2185
- const coldResumeRun = async (run, hasUI, ui, trustedProject, context) => {
2331
+ const recoveryUi = (context) => {
2332
+ const host = object(context) ? context : undefined;
2333
+ const ui = host && object(host.ui) ? host.ui : {};
2334
+ return { hasUI: host?.hasUI === true, ui };
2335
+ };
2336
+ const coldResumeRun = async (run, hasUI, ui, trustedProject, context, modeOverride, waitForCompletion = true) => {
2186
2337
  const loaded = await run.store.load();
2338
+ const foreground = modeOverride ?? loaded.snapshot.launchMode === "foreground";
2339
+ if (loaded.run.activeShells !== undefined) {
2340
+ await persistRunState(run.store, run.metadata, (current) => {
2341
+ const next = { ...current };
2342
+ delete next.activeShells;
2343
+ return next;
2344
+ });
2345
+ }
2187
2346
  await run.store.validateRetrySource();
2188
2347
  await run.store.validateBorrowedWorktrees();
2189
2348
  if (loaded.snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION)
@@ -2206,7 +2365,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2206
2365
  const { settingsPath, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script } = await resumeLaunchPrologue({ snapshot: loaded.snapshot, cwd: run.store.cwd, trustedProject, rootModel, ...(context?.modelRegistry ? { modelRegistry: context.modelRegistry } : {}), signal: controller.signal, ...(context?.resolvedAliases ? { resolvedAliases: context.resolvedAliases } : {}), ...(context?.blockedAliases ? { blockedAliases: context.blockedAliases } : {}), ...(context?.blockedAliasTargets ? { blockedAliasTargets: context.blockedAliasTargets } : {}), withPreflight: true });
2207
2366
  if (!script)
2208
2367
  throw new WorkflowError("INTERNAL_ERROR", "Resume preflight did not produce a launch script");
2209
- await run.store.saveSnapshot(snapshot);
2368
+ const persistedSnapshot = modeOverride === undefined ? snapshot : createLaunchSnapshot({ ...snapshot, launchMode: foreground ? "foreground" : "background" });
2369
+ await run.store.saveSnapshot(persistedSnapshot);
2210
2370
  scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
2211
2371
  run.executor = createAgentExecutor({ cwd: run.store.cwd, model: rootModel, tools: activeSnapshotTools(snapshot.tools, "session"), availableModels, knownModels, modelAliases: currentAliases, blockedAliases, blockedAliasTargets, settingsPath, agentDefinitions: snapshot.roles ?? {}, runStore: run.store, providerPause: async () => { deliver(pi, `Workflow ${snapshot.metadata.name} paused: provider limit.`); await run.lifecycle.providerPause(); }, agentResourcePolicy: frozenResourcePolicy(currentPolicy) });
2212
2372
  const drift = aliasDrift(previousAliases, currentAliases);
@@ -2222,7 +2382,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2222
2382
  const typed = asWorkflowError(error);
2223
2383
  if (!HARD_TERMINAL_RUN_STATES.has(run.lifecycle.state)) {
2224
2384
  await run.lifecycle.terminal("failed", typed.code).catch(() => undefined);
2225
- const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, error: { code: typed.code, message: typed.message } }));
2385
+ const persisted = await persistRunState(run.store, run.metadata, (current) => persistedFailure({ ...current }, typed));
2226
2386
  await eventPublisher.runFailed(run.store, run.metadata, typed, run.lifecycle.state === "interrupted" ? "interrupted" : "failed");
2227
2387
  run.update?.(workflowToolUpdate(persisted));
2228
2388
  if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
@@ -2233,7 +2393,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2233
2393
  }
2234
2394
  await scheduler.cancelRun(run.store.runId);
2235
2395
  await run.lifecycle.resume();
2236
- const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, false, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.lifecycle, run.metadata.name) }, run.store, runContext, variables, registry), controller.signal);
2396
+ const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(run.store, run.metadata, run.lifecycle, command, options, signal, identity), agent: workflowAgentHandler(run.store, run.metadata, run.lifecycle, run.executor, run.store.cwd, run.store.runId), worktree: async (owner) => resolveWorktree(run.store, run.metadata, owner), checkpoint: checkpointBridge(run.store.runId, run.store, run.metadata, foreground, hasUI ? ui : undefined), phase: phaseBridge(run.store, run.metadata, run.lifecycle), log: logBridge(run.lifecycle, run.metadata.name) }, run.store, runContext, variables, registry), controller.signal);
2237
2397
  run.execution = execution;
2238
2398
  const completion = execution.result.then(async (value) => {
2239
2399
  await scheduler.flush();
@@ -2248,19 +2408,34 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2248
2408
  const typed = error instanceof WorkflowError ? error : new WorkflowError(errorCode(error) ?? "INTERNAL_ERROR", errorText(error));
2249
2409
  if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
2250
2410
  await run.lifecycle.terminal(typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
2251
- const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot(), error: { code: typed.code, message: typed.message } }));
2411
+ const persisted = await persistRunState(run.store, run.metadata, (current) => persistedFailure({ ...current, ...run.budget.snapshot() }, typed));
2252
2412
  const state = run.lifecycle.state === "stopped" || run.lifecycle.state === "interrupted" || run.lifecycle.state === "budget_exhausted" ? run.lifecycle.state : "failed";
2253
2413
  if (state === "failed")
2254
2414
  retryReservations.delete(persisted.retry?.lineageRootRunId ?? run.store.runId);
2255
2415
  await eventPublisher.runFailed(run.store, run.metadata, typed, state);
2256
2416
  run.update?.(workflowToolUpdate(persisted));
2257
- if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
2258
- await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted).then((diagnostic) => { deliverFailure(pi, diagnostic); }).catch(() => undefined);
2417
+ if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) {
2418
+ const diagnostic = await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted);
2419
+ Object.defineProperty(typed, WORKFLOW_FAILURE_DIAGNOSTICS, { value: diagnostic });
2420
+ }
2421
+ throw typed;
2259
2422
  }).finally(() => cleanupTerminalRun(run.store.runId));
2260
2423
  run.completion = completion;
2261
- void completion;
2424
+ if (!foreground || !waitForCompletion) {
2425
+ void completion.then(async ({ value, resultPath }) => {
2426
+ deliver(pi, completionDelivery(run.metadata.name, value, resultPath, await run.store.changedWorktrees()));
2427
+ }, (error) => {
2428
+ const diagnostic = failureDiagnosticsFrom(error);
2429
+ if (diagnostic)
2430
+ deliverFailure(pi, diagnostic);
2431
+ else
2432
+ deliver(pi, formatWorkflowFailureDeliveryFallback(run.metadata.name, run.store.runId, run.store.directory, error));
2433
+ });
2434
+ return undefined;
2435
+ }
2436
+ return completion;
2262
2437
  };
2263
- const applyBudgetDecision = async (request, approved, context, signal) => {
2438
+ const applyBudgetDecision = async (request, approved, context, signal, waitForCompletion = true) => {
2264
2439
  const run = runs.get(request.runId);
2265
2440
  if (!run)
2266
2441
  throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${request.runId}`);
@@ -2274,10 +2449,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2274
2449
  next.budget = nextBudget;
2275
2450
  else
2276
2451
  delete next.budget; return next; });
2277
- await coldResumeRun(run, false, {}, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) });
2452
+ const { hasUI, ui } = recoveryUi(context);
2453
+ const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, undefined, waitForCompletion);
2454
+ if (completed)
2455
+ return { state: "completed", approved: true, value: completed.value, run: (await run.store.load()).run };
2278
2456
  return { state: "running", approved: true };
2279
2457
  };
2280
- const resumeWorkflowRun = async (runId, rawPatch, context, signal) => {
2458
+ const resumeWorkflowRun = async (runId, rawPatch, context, signal, modeOverride, waitForCompletion = true) => {
2281
2459
  const run = runs.get(runId);
2282
2460
  if (!run) {
2283
2461
  const host = object(context) ? context : {};
@@ -2323,11 +2501,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2323
2501
  else
2324
2502
  delete next.budget; return next; });
2325
2503
  }
2326
- await coldResumeRun(run, false, {}, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) });
2504
+ const { hasUI, ui } = recoveryUi(context);
2505
+ const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, modeOverride, waitForCompletion);
2506
+ if (completed)
2507
+ return { state: "completed", runId, value: completed.value, run: (await run.store.load()).run };
2327
2508
  return { state: "running" };
2328
2509
  };
2329
2510
  const retryReservations = new Set();
2330
- const retryWorkflowRun = async (runId, context, signal) => {
2511
+ const retryWorkflowRun = async (runId, context, signal, modeOverride) => {
2331
2512
  if (typeof runId !== "string" || !runId.trim())
2332
2513
  throw new WorkflowError("RESUME_INCOMPATIBLE", "workflow_retry requires an explicit run ID");
2333
2514
  const host = object(context) ? context : {};
@@ -2410,12 +2591,19 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2410
2591
  runs.set(childRunId, childRun);
2411
2592
  scheduler.addRun(childRunId, loaded.snapshot.settings.concurrency, () => { childBudget.checkAgentLaunch(); });
2412
2593
  await eventPublisher.runStarted(childStore, loaded.snapshot.metadata);
2413
- await coldResumeRun(childRun, false, {}, trustedProject, { model: hostModel, modelRegistry, resolvedAliases: currentAliases, blockedAliases, blockedAliasTargets, ...(signal ? { signal } : {}) });
2594
+ const { hasUI, ui } = recoveryUi(context);
2595
+ const completed = await coldResumeRun(childRun, hasUI, ui, trustedProject, { model: hostModel, modelRegistry, resolvedAliases: currentAliases, blockedAliases, blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
2414
2596
  const completion = runs.get(childRunId)?.completion;
2415
2597
  if (completion) {
2416
2598
  childStarted = true;
2417
2599
  void completion.then(() => { retryReservations.delete(lineageRootRunId); }, () => { retryReservations.delete(lineageRootRunId); });
2418
2600
  }
2601
+ else if (completed) {
2602
+ childStarted = true;
2603
+ retryReservations.delete(lineageRootRunId);
2604
+ }
2605
+ if (completed)
2606
+ return { runId: childRunId, parentRunId: loaded.run.id, state: "completed", value: completed.value, run: (await childStore.load()).run };
2419
2607
  return { runId: childRunId, parentRunId: loaded.run.id, state: "running" };
2420
2608
  }
2421
2609
  finally {
@@ -2430,7 +2618,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2430
2618
  parameters: WORKFLOW_RETRY_PARAMETERS,
2431
2619
  async execute(_id, params, signal, _onUpdate, ctx) {
2432
2620
  try {
2433
- const result = await retryWorkflowRun(params.runId, ctx, signal);
2621
+ const result = await retryWorkflowRun(params.runId, ctx, signal, params.foreground);
2434
2622
  return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
2435
2623
  }
2436
2624
  catch (error) {
@@ -2444,10 +2632,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2444
2632
  name: "workflow_resume",
2445
2633
  label: "Workflow Resume",
2446
2634
  description: "Resume an exhausted workflow with unchanged or patched aggregate budgets",
2447
- parameters: Type.Object({ runId: Type.String(), budget: Type.Optional(Type.Unknown()) }, { additionalProperties: false }),
2635
+ parameters: Type.Object({ runId: Type.String(), budget: Type.Optional(Type.Unknown()), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) }, { additionalProperties: false }),
2448
2636
  async execute(_id, params, signal, _onUpdate, ctx) {
2449
2637
  try {
2450
- const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal);
2638
+ const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal, params.foreground);
2451
2639
  return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
2452
2640
  }
2453
2641
  catch (error) {
@@ -2484,11 +2672,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2484
2672
  }
2485
2673
  if (loaded.run.state !== "interrupted" && loaded.run.state !== "budget_exhausted") {
2486
2674
  const previousState = loaded.run.state;
2487
- await store.updateState((current) => ["completed", "failed", "stopped", "interrupted", "budget_exhausted"].includes(current.state) ? current : { ...current, state: "interrupted" });
2675
+ await store.updateState((current) => {
2676
+ if (["completed", "failed", "stopped", "interrupted", "budget_exhausted"].includes(current.state))
2677
+ return current;
2678
+ const next = { ...current, state: "interrupted" };
2679
+ delete next.activeShells;
2680
+ return next;
2681
+ });
2488
2682
  loaded = { ...loaded, run: (await store.load()).run };
2489
2683
  await eventPublisher.runState(store, loaded.snapshot.metadata, previousState, "interrupted", "session_shutdown");
2490
2684
  loaded = { ...loaded, run: (await store.load()).run };
2491
2685
  }
2686
+ else if (loaded.run.activeShells !== undefined) {
2687
+ await store.updateState((current) => {
2688
+ if (["completed", "failed", "stopped"].includes(current.state))
2689
+ return current;
2690
+ const next = { ...current };
2691
+ delete next.activeShells;
2692
+ return next;
2693
+ });
2694
+ loaded = { ...loaded, run: (await store.load()).run };
2695
+ }
2492
2696
  const model = modelSpec(loaded.snapshot.models[0] ?? "", { provider: ctx.model?.provider ?? "", model: ctx.model?.id ?? "", thinking: pi.getThinkingLevel() });
2493
2697
  const budget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
2494
2698
  eventPublisher.seedBudget(runId, loaded.run.budgetEvents);
@@ -2516,7 +2720,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2516
2720
  const toResume = choice === "Resume all" ? interrupted : interrupted.filter((_, i) => labels[i] === choice);
2517
2721
  for (const run of toResume) {
2518
2722
  try {
2519
- await coldResumeRun(run, true, ctx.ui, projectTrusted(ctx), ctx);
2723
+ await coldResumeRun(run, true, ctx.ui, projectTrusted(ctx), ctx, undefined, false);
2520
2724
  ctx.ui.notify(`Resumed workflow ${run.metadata.name}.`, "info");
2521
2725
  }
2522
2726
  catch (err) {
@@ -2588,10 +2792,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2588
2792
  const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
2589
2793
  const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, modelAliases, knownModels, settingsPath)] : []; });
2590
2794
  const snapshotModels = [...new Set([rootModelName, ...checked.referenced.models, ...roleModels])];
2591
- const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, settings, settingsPath, settingsSources: { ...launch.resolution.sources, concurrency: params.concurrency === undefined ? launch.resolution.sources.concurrency : "per-run options" }, ...(functionName ? { launchKind: "function", functionName } : {}), ...(Object.keys(modelAliases).length ? { modelAliases } : {}), ...(budget ? { budget } : {}), ...(checked.referenced.phases.length ? { phases: checked.referenced.phases } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
2795
+ const snapshot = createLaunchSnapshot({ script, args, metadata: checked.metadata, launchMode: params.foreground ? "foreground" : "background", settings, settingsPath, settingsSources: { ...launch.resolution.sources, concurrency: params.concurrency === undefined ? launch.resolution.sources.concurrency : "per-run options" }, ...(functionName ? { launchKind: "function", functionName } : {}), ...(Object.keys(modelAliases).length ? { modelAliases } : {}), ...(budget ? { budget } : {}), ...(checked.referenced.phases.length ? { phases: checked.referenced.phases } : {}), models: snapshotModels, tools: rootTools, agentTypes: checked.referenced.agentTypes, roles, projectRoles, schemas: checked.schemas });
2796
+ let persistedSnapshot = snapshot;
2797
+ const captureFunctionRole = functionName ? async (role, model) => {
2798
+ const definition = agentDefinitions[role];
2799
+ if (!definition)
2800
+ return;
2801
+ const modelName = `${model.provider}/${model.model}`;
2802
+ const hasProjectRole = projectAgentDefinitions[role] !== undefined;
2803
+ if (persistedSnapshot.roles?.[role] !== undefined && (!hasProjectRole || persistedSnapshot.projectRoles?.includes(role)) && persistedSnapshot.models.includes(modelName))
2804
+ return;
2805
+ const roles = { ...(persistedSnapshot.roles ?? {}), [role]: definition };
2806
+ const projectRoles = hasProjectRole ? [...new Set([...(persistedSnapshot.projectRoles ?? []), role])] : persistedSnapshot.projectRoles ?? [];
2807
+ const models = [...new Set([...persistedSnapshot.models, modelName])];
2808
+ persistedSnapshot = createLaunchSnapshot({ ...persistedSnapshot, models, roles, projectRoles });
2809
+ await store.saveSnapshot(persistedSnapshot);
2810
+ } : undefined;
2592
2811
  const budgetRuntime = new WorkflowBudgetRuntime(budget);
2593
2812
  const initialBudget = budgetRuntime.snapshot();
2594
- await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], nativeSessions: [], ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
2813
+ await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], nativeSessions: [], delivery: params.foreground ? { mode: "foreground", state: "attached", toolCallId } : { mode: "background", state: "pending" }, ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
2814
+ if (params.foreground)
2815
+ foregroundDeliveries.set(toolCallId, { store, inline: false });
2595
2816
  const lifecycle = lifecycleFor(store, "running", budgetRuntime, checked.metadata);
2596
2817
  const background = !params.foreground;
2597
2818
  const providerPause = async () => { if (background)
@@ -2602,7 +2823,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2602
2823
  if (params.foreground && onUpdate)
2603
2824
  onUpdate(workflowToolUpdate((await store.load()).run));
2604
2825
  scheduler.addRun(runId, settings.concurrency, () => runs.get(runId)?.budget.checkAgentLaunch());
2605
- const execution = runWorkflow(script, args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: workflowAgentHandler(store, checked.metadata, lifecycle, executor, ctx.cwd, runId), worktree: async (owner) => resolveWorktree(store, checked.metadata, owner), checkpoint: checkpointBridge(runId, store, checked.metadata, Boolean(params.foreground), params.foreground && ctx.hasUI ? ctx.ui : undefined, headless), phase: phaseBridge(store, checked.metadata, lifecycle), log: logBridge(lifecycle, checked.metadata.name) }, store, runContext, variables, registry), runController.signal);
2826
+ const execution = runWorkflow(script, args, withWorkflowFunctions({ shell: (command, options, signal, identity) => shellForRun(store, checked.metadata, lifecycle, command, options, signal, identity), agent: workflowAgentHandler(store, checked.metadata, lifecycle, executor, ctx.cwd, runId, captureFunctionRole), worktree: async (owner) => resolveWorktree(store, checked.metadata, owner), checkpoint: checkpointBridge(runId, store, checked.metadata, Boolean(params.foreground), params.foreground && ctx.hasUI ? ctx.ui : undefined, headless), phase: phaseBridge(store, checked.metadata, lifecycle), log: logBridge(lifecycle, checked.metadata.name) }, store, runContext, variables, registry), runController.signal);
2606
2827
  runs.get(runId).execution = execution;
2607
2828
  await eventPublisher.runStarted(store, checked.metadata);
2608
2829
  const finish = execution.result.then(async (value) => {
@@ -2618,7 +2839,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2618
2839
  const typed = error instanceof WorkflowError ? error : new WorkflowError("INTERNAL_ERROR", String(error));
2619
2840
  if (!["stopped", "interrupted", "budget_exhausted"].includes(lifecycle.state))
2620
2841
  await lifecycle.terminal(typed.code === "CANCELLED" ? "stopped" : typed.code === "BUDGET_EXHAUSTED" ? "budget_exhausted" : "failed", typed.code);
2621
- const persisted = await persistRunState(store, checked.metadata, (current) => ({ ...current, ...budgetRuntime.snapshot(), error: { code: typed.code, message: typed.message } }));
2842
+ const persisted = await persistRunState(store, checked.metadata, (current) => persistedFailure({ ...current, ...budgetRuntime.snapshot() }, typed));
2622
2843
  const state = lifecycle.state === "stopped" || lifecycle.state === "interrupted" || lifecycle.state === "budget_exhausted" ? lifecycle.state : "failed";
2623
2844
  await eventPublisher.runFailed(store, checked.metadata, typed, state);
2624
2845
  const diagnostic = await createWorkflowFailureDiagnostics(store, checked.metadata, typed, persisted);
@@ -2629,18 +2850,41 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2629
2850
  });
2630
2851
  const completion = finish.finally(() => cleanupTerminalRun(runId));
2631
2852
  runs.get(runId).completion = completion;
2853
+ const deliverFailureContent = (error) => {
2854
+ const diagnostic = failureDiagnosticsFrom(error);
2855
+ return diagnostic ? formatWorkflowFailureDelivery(diagnostic) : formatWorkflowFailureDeliveryFallback(checked.metadata.name, runId, store.directory, error);
2856
+ };
2857
+ const queueForegroundDelivery = async (content) => {
2858
+ const delivery = foregroundDeliveries.get(toolCallId);
2859
+ if (!delivery)
2860
+ return;
2861
+ await store.updateState((current) => {
2862
+ if (!current.delivery || current.delivery.state === "delivered")
2863
+ return current;
2864
+ return { ...current, delivery: { ...current.delivery, mode: "background", state: "pending" } };
2865
+ });
2866
+ if (delivery.inline)
2867
+ return;
2868
+ scheduleForegroundDelivery(toolCallId, async () => {
2869
+ if (delivery.inline)
2870
+ return;
2871
+ pendingFailureDiagnostics.delete(toolCallId);
2872
+ await deliverTerminal(store, content);
2873
+ });
2874
+ };
2632
2875
  if (background) {
2633
2876
  void completion.then(async ({ value, resultPath }) => {
2634
- deliver(pi, completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
2635
- }, (error) => {
2636
- const diagnostic = failureDiagnosticsFrom(error);
2637
- if (diagnostic)
2638
- deliverFailure(pi, diagnostic);
2639
- else
2640
- deliver(pi, `Workflow ${checked.metadata.name} failed: ${formatWorkflowFailure(error)}`);
2877
+ await deliverTerminal(store, completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
2878
+ }, async (error) => {
2879
+ await deliverTerminal(store, deliverFailureContent(error));
2641
2880
  });
2642
2881
  return { content: [{ type: "text", text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
2643
2882
  }
2883
+ void completion.then(async ({ value, resultPath }) => {
2884
+ await queueForegroundDelivery(completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
2885
+ }, async (error) => {
2886
+ await queueForegroundDelivery(deliverFailureContent(error));
2887
+ });
2644
2888
  const { value } = await completion;
2645
2889
  const run = (await store.load()).run;
2646
2890
  return { content: [{ type: "text", text: JSON.stringify(value) }, { type: "text", text: `Workflow run ID: ${runId}` }], details: { runId, value, run } };
@@ -2687,7 +2931,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2687
2931
  const entries = await Promise.all((await listRunIds(ctx.cwd, ctx.sessionManager.getSessionId(), home)).map(async (runId) => {
2688
2932
  const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
2689
2933
  try {
2690
- return { store, loaded: await store.load() };
2934
+ const loaded = await store.load();
2935
+ return { store, loaded: { ...loaded, run: withLiveActivities(loaded.run) } };
2691
2936
  }
2692
2937
  catch {
2693
2938
  if (!await store.isComplete())
@@ -2715,7 +2960,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2715
2960
  return keepContext ? "dashboard" : "done";
2716
2961
  }
2717
2962
  if ((action === "budget-approve" || action === "budget-reject") && runId && rest[0]) {
2718
- const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx);
2963
+ const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx, undefined, false);
2719
2964
  ctx.ui.notify(result ? `Budget adjustment ${rest[0]} ${result.approved ? "approved" : "rejected"}.` : "Budget proposal is not pending.", result ? "info" : "warning");
2720
2965
  return keepContext ? "dashboard" : "done";
2721
2966
  }
@@ -2740,12 +2985,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2740
2985
  if (action === "resume" && run) {
2741
2986
  if (run.lifecycle.state === "budget_exhausted") {
2742
2987
  const patch = rest.length ? JSON.parse(rest.join(" ")) : undefined;
2743
- const result = await resumeWorkflowRun(run.store.runId, patch, ctx);
2744
- ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "running" ? "info" : "warning");
2988
+ const result = await resumeWorkflowRun(run.store.runId, patch, ctx, undefined, undefined, false);
2989
+ ctx.ui.notify(result.state === "completed" ? `Workflow ${run.store.runId} completed.` : result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "awaiting_approval" ? "warning" : "info");
2745
2990
  }
2746
2991
  else {
2747
2992
  if (run.lifecycle.state === "interrupted")
2748
- await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx);
2993
+ await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx, undefined, false);
2749
2994
  else {
2750
2995
  if (run.lifecycle.state === "paused")
2751
2996
  await refreshPausedRunAliases(run, { ...resumeHostContext(ctx), projectTrusted: projectTrusted(ctx) });
@@ -2759,8 +3004,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2759
3004
  const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}");
2760
3005
  if (input === undefined)
2761
3006
  return keepContext ? "dashboard" : "done";
2762
- const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx);
2763
- ctx.ui.notify(result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "running" ? "info" : "warning");
3007
+ const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx, undefined, undefined, false);
3008
+ ctx.ui.notify(result.state === "completed" ? `Workflow ${run.store.runId} completed.` : result.state === "running" ? `Resumed workflow ${run.store.runId}.` : `Budget adjustment for ${run.store.runId} is awaiting approval.`, result.state === "awaiting_approval" ? "warning" : "info");
2764
3009
  return keepContext ? "dashboard" : "done";
2765
3010
  }
2766
3011
  if (action === "stop" && run) {
@@ -2981,11 +3226,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2981
3226
  };
2982
3227
  const loadDashboard = async () => {
2983
3228
  const loaded = await store.load();
3229
+ const liveRun = withLiveActivities(loaded.run);
2984
3230
  const checkpoints = await store.awaitingCheckpoints();
2985
3231
  const worktrees = await store.worktrees();
2986
3232
  const completedOperations = ctx.mode === "tui" ? await store.replayableOperations().catch(() => []) : [];
2987
3233
  const agentResults = new Map();
2988
- for (const agent of loaded.run.agents) {
3234
+ for (const agent of liveRun.agents) {
2989
3235
  if (agent.state !== "completed" || agent.parentId || !agent.resultPath)
2990
3236
  continue;
2991
3237
  const operation = completedOperations.find((candidate) => candidate.path === agent.resultPath);
@@ -2997,11 +3243,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2997
3243
  const reviews = new Map();
2998
3244
  const add = (label, value) => { actions.set(label, `${value} ${store.runId}`); };
2999
3245
  const addCopy = (label, value, artifact) => { actions.set(label, "copy"); copies.set(label, { value, artifact }); };
3000
- if (loaded.run.state === "running")
3246
+ if (liveRun.state === "running")
3001
3247
  add("Pause", "pause");
3002
- if (["paused", "interrupted"].includes(loaded.run.state))
3248
+ if (["paused", "interrupted"].includes(liveRun.state))
3003
3249
  add("Resume", "resume");
3004
- if (loaded.run.state === "budget_exhausted") {
3250
+ if (liveRun.state === "budget_exhausted") {
3005
3251
  actions.set("Resume unchanged", `resume ${store.runId}`);
3006
3252
  actions.set("Adjust budget", `adjust ${store.runId}`);
3007
3253
  }
@@ -3010,7 +3256,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3010
3256
  actions.set(`Approve budget ${id}`, `budget-approve ${store.runId} ${decision.proposalId}`);
3011
3257
  actions.set(`Reject budget ${id}`, `budget-reject ${store.runId} ${decision.proposalId}`);
3012
3258
  }
3013
- if (!terminalStates.has(loaded.run.state))
3259
+ if (!terminalStates.has(liveRun.state))
3014
3260
  add("Stop", "stop");
3015
3261
  for (const cp of checkpoints) {
3016
3262
  if (ctx.mode === "tui") {
@@ -3027,15 +3273,15 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3027
3273
  actions.set("Refresh", "refresh");
3028
3274
  else
3029
3275
  actions.set("Open script in editor", "open-script");
3030
- if (ctx.mode !== "tui" && loaded.run.agents.length)
3276
+ if (ctx.mode !== "tui" && liveRun.agents.length)
3031
3277
  actions.set("Agents...", "agents");
3032
- if (terminalStates.has(loaded.run.state))
3278
+ if (terminalStates.has(liveRun.state))
3033
3279
  add("Delete", "delete");
3034
3280
  if (ctx.mode === "tui") {
3035
3281
  addCopy("Copy run path", store.directory, "run path");
3036
3282
  addCopy("Copy run ID", store.runId, "run ID");
3037
3283
  }
3038
- return { dashboard: formatWorkflowPhaseDashboard(loaded.run, loaded.snapshot, process.stdout.columns || 80).join("\n"), phaseModel: buildWorkflowPhaseModel(loaded.run, loaded.snapshot), run: loaded.run, snapshot: loaded.snapshot, actions, copies, reviews, agentResults, agents: loaded.run.agents, worktrees, cwd: loaded.run.cwd };
3284
+ return { dashboard: formatWorkflowPhaseDashboard(liveRun, loaded.snapshot, process.stdout.columns || 80).join("\n"), phaseModel: buildWorkflowPhaseModel(liveRun, loaded.snapshot), run: liveRun, snapshot: loaded.snapshot, actions, copies, reviews, agentResults, agents: liveRun.agents, worktrees, cwd: liveRun.cwd };
3039
3285
  };
3040
3286
  const agentWorktreeFor = (dashboard, agent) => agent.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === agent.worktreeOwner) : undefined;
3041
3287
  const agentActionLabels = (dashboard, agent) => {
@@ -3123,12 +3369,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3123
3369
  let tree = buildWorkflowPhaseTree(view.phaseModel);
3124
3370
  let selectedNodeId = initialSelection.nodeId ?? tree.nodes[0]?.id;
3125
3371
  let expandedNodeIds = new Set(initialSelection.expandedNodeIds ?? workflowPhaseTreeInitialExpanded(tree));
3126
- const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
3372
+ const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_PANEL_FOOTER_ROWS);
3127
3373
  const keyLabels = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
3128
- const keyLabel = (binding, fallback) => {
3129
- const keys = keybindingKeys(keybindings, binding);
3130
- return keys?.length ? keys.map((key) => keyLabels[key] ?? key).join("/") : fallback;
3131
- };
3374
+ const keyLabel = (binding, fallback) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
3132
3375
  const selectedAgentRecord = () => {
3133
3376
  const node = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
3134
3377
  return node?.kind === "agent" && node.agentId ? view.agents.find((agent) => agent.id === node.agentId) : undefined;
@@ -3159,7 +3402,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3159
3402
  }
3160
3403
  finally {
3161
3404
  editorRunning = false;
3162
- tui.requestRender(true);
3163
3405
  }
3164
3406
  };
3165
3407
  const updateDashboard = async () => {
@@ -3213,7 +3455,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3213
3455
  void updateDashboard().catch(() => undefined).finally(() => { refreshing = false; });
3214
3456
  }, 1000);
3215
3457
  timer.unref();
3216
- return borderWorkflowOverlay({
3458
+ return {
3217
3459
  render(width) {
3218
3460
  renderedWidth = width;
3219
3461
  const narrow = width < 80;
@@ -3249,7 +3491,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3249
3491
  selectionNeedsScroll = false;
3250
3492
  }
3251
3493
  dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
3252
- const hint = truncateToVisualLines(theme.fg("dim", actionMode ? `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.select.confirm", "enter")} run · ${keyLabel("tui.select.cancel", "esc")} tree` : `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} tree · ${keyLabel("tui.editor.cursorLeft", "←")}/${keyLabel("tui.editor.cursorRight", "→")} collapse/expand · ${keyLabel("tui.select.confirm", "enter")} inspect · a actions · ${keyLabel("tui.select.cancel", "esc")} ${narrow && detailsMode ? "tree" : "close"}${content.length > viewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
3494
+ const hint = truncateToVisualLines(theme.fg("dim", actionMode ? `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.select.confirm", "enter")} run · ${keyLabel("tui.select.cancel", "esc")} tree` : `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} tree · ${keyLabel("tui.editor.cursorLeft", "←")}/${keyLabel("tui.editor.cursorRight", "→")} collapse/expand · ${keyLabel("tui.select.confirm", "enter")} inspect · a actions · ${keyLabel("tui.select.cancel", "esc")} ${narrow && detailsMode ? "tree" : "back"}${content.length > viewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
3253
3495
  return [...content.slice(dashboardOffset, dashboardOffset + viewport), ...(hintRows ? [hint] : [])];
3254
3496
  },
3255
3497
  invalidate() { },
@@ -3266,21 +3508,21 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3266
3508
  }
3267
3509
  if (actionMode) {
3268
3510
  const options = actionOptions();
3269
- if (keybindings.matches(data, "tui.select.cancel")) {
3511
+ if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) {
3270
3512
  actionMode = false;
3271
3513
  dashboardOffset = 0;
3272
3514
  tui.requestRender();
3273
3515
  return;
3274
3516
  }
3275
- if (keybindings.matches(data, "tui.select.up"))
3517
+ if (workflowKeyMatches(keybindings, data, "tui.select.up"))
3276
3518
  actionIndex = (actionIndex + options.length - 1) % options.length;
3277
- else if (keybindings.matches(data, "tui.select.down"))
3519
+ else if (workflowKeyMatches(keybindings, data, "tui.select.down"))
3278
3520
  actionIndex = (actionIndex + 1) % options.length;
3279
- else if (keybindings.matches(data, "tui.select.pageUp"))
3521
+ else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
3280
3522
  dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
3281
- else if (keybindings.matches(data, "tui.select.pageDown"))
3523
+ else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
3282
3524
  dashboardOffset += Math.max(1, terminalRows() - 1);
3283
- else if (keybindings.matches(data, "tui.select.confirm")) {
3525
+ else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
3284
3526
  const action = options[actionIndex];
3285
3527
  const agent = selectedAgentRecord();
3286
3528
  if (!action || action === "Back") {
@@ -3314,20 +3556,20 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3314
3556
  return;
3315
3557
  }
3316
3558
  const current = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
3317
- if (keybindings.matches(data, "tui.select.cancel")) {
3559
+ if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) {
3318
3560
  if (narrow && detailsMode) {
3319
3561
  detailsMode = false;
3320
3562
  selectionNeedsScroll = true;
3321
3563
  }
3322
3564
  else
3323
- done(undefined);
3565
+ done("Back");
3324
3566
  }
3325
3567
  else if (narrow && detailsMode) {
3326
- if (keybindings.matches(data, "tui.select.pageUp"))
3568
+ if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
3327
3569
  dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
3328
- else if (keybindings.matches(data, "tui.select.pageDown"))
3570
+ else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
3329
3571
  dashboardOffset += Math.max(1, terminalRows() - 1);
3330
- else if (keybindings.matches(data, "tui.select.confirm")) {
3572
+ else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
3331
3573
  if (current?.kind === "agent" && current.agentId) {
3332
3574
  actionMode = true;
3333
3575
  actionIndex = 0;
@@ -3340,33 +3582,33 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3340
3582
  }
3341
3583
  }
3342
3584
  }
3343
- else if (keybindings.matches(data, "tui.editor.cursorLeft")) {
3585
+ else if (workflowKeyMatches(keybindings, data, "tui.editor.cursorLeft")) {
3344
3586
  const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "left");
3345
3587
  selectedNodeId = next.nodeId;
3346
3588
  expandedNodeIds = new Set(next.expandedNodeIds);
3347
3589
  selectionNeedsScroll = true;
3348
3590
  }
3349
- else if (keybindings.matches(data, "tui.editor.cursorRight")) {
3591
+ else if (workflowKeyMatches(keybindings, data, "tui.editor.cursorRight")) {
3350
3592
  const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "right");
3351
3593
  selectedNodeId = next.nodeId;
3352
3594
  expandedNodeIds = new Set(next.expandedNodeIds);
3353
3595
  selectionNeedsScroll = true;
3354
3596
  }
3355
- else if (keybindings.matches(data, "tui.select.up")) {
3597
+ else if (workflowKeyMatches(keybindings, data, "tui.select.up")) {
3356
3598
  const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "up");
3357
3599
  selectedNodeId = next.nodeId;
3358
3600
  selectionNeedsScroll = true;
3359
3601
  }
3360
- else if (keybindings.matches(data, "tui.select.down")) {
3602
+ else if (workflowKeyMatches(keybindings, data, "tui.select.down")) {
3361
3603
  const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "down");
3362
3604
  selectedNodeId = next.nodeId;
3363
3605
  selectionNeedsScroll = true;
3364
3606
  }
3365
- else if (keybindings.matches(data, "tui.select.pageUp"))
3607
+ else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
3366
3608
  dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
3367
- else if (keybindings.matches(data, "tui.select.pageDown"))
3609
+ else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
3368
3610
  dashboardOffset += Math.max(1, terminalRows() - 1);
3369
- else if (keybindings.matches(data, "tui.select.confirm")) {
3611
+ else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
3370
3612
  if (narrow)
3371
3613
  detailsMode = true;
3372
3614
  else if (current?.kind === "agent" && current.agentId) {
@@ -3383,11 +3625,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3383
3625
  tui.requestRender();
3384
3626
  },
3385
3627
  dispose() { disposed = true; clearInterval(timer); setWorkflowStatus(undefined); },
3386
- }, theme);
3387
- }, { overlay: true, overlayOptions: WORKFLOW_OVERLAY_OPTIONS })
3388
- : await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Close"]);
3389
- if (!actionChoice || actionChoice === "Close")
3390
- return;
3628
+ };
3629
+ })
3630
+ : await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Back"]);
3631
+ if (!actionChoice || actionChoice === "Back") {
3632
+ stores = await loadStores();
3633
+ break;
3634
+ }
3391
3635
  if (actionChoice === "Agents...") {
3392
3636
  await selectAgent(view);
3393
3637
  continue;
@@ -3439,7 +3683,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3439
3683
  const currentLayout = layout();
3440
3684
  const maxOffset = Math.max(0, renderedLines.length - currentLayout.contentViewport);
3441
3685
  offset = Math.min(offset, maxOffset);
3442
- const hint = truncateToVisualLines(theme.fg("dim", "↑↓/pgup/pgdn scroll · enter select · esc cancel"), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
3686
+ const keyLabels = { up: "", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
3687
+ const keyLabel = (binding, fallback) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
3688
+ const hint = truncateToVisualLines(theme.fg("dim", `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")}/pgup/pgdn scroll · enter select · esc cancel`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
3443
3689
  const controls = currentLayout.compactControls
3444
3690
  ? [options.map((option, index) => `${index === selectedIndex ? "[" : " "}${option}${index === selectedIndex ? "]" : " "}`).join(" ")]
3445
3691
  : options.map((option, index) => `${index === selectedIndex ? "→ " : " "}${option}`);
@@ -3453,17 +3699,17 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3453
3699
  },
3454
3700
  invalidate() { },
3455
3701
  handleInput(data) {
3456
- if (keybindings.matches(data, "tui.select.up"))
3702
+ if (workflowKeyMatches(keybindings, data, "tui.select.up"))
3457
3703
  selectedIndex = (selectedIndex + options.length - 1) % options.length;
3458
- else if (keybindings.matches(data, "tui.select.down"))
3704
+ else if (workflowKeyMatches(keybindings, data, "tui.select.down"))
3459
3705
  selectedIndex = (selectedIndex + 1) % options.length;
3460
- else if (keybindings.matches(data, "tui.select.pageUp"))
3706
+ else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
3461
3707
  move(-layout().contentViewport);
3462
- else if (keybindings.matches(data, "tui.select.pageDown"))
3708
+ else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
3463
3709
  move(layout().contentViewport);
3464
- else if (keybindings.matches(data, "tui.select.confirm"))
3710
+ else if (workflowKeyMatches(keybindings, data, "tui.select.confirm"))
3465
3711
  done(options[selectedIndex] === "Cancel" ? undefined : options[selectedIndex]);
3466
- else if (keybindings.matches(data, "tui.select.cancel"))
3712
+ else if (workflowKeyMatches(keybindings, data, "tui.select.cancel"))
3467
3713
  done(undefined);
3468
3714
  tui.requestRender();
3469
3715
  },