pi-extensible-workflows 3.2.0 → 3.4.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.
Files changed (47) hide show
  1. package/README.md +2 -31
  2. package/dist/src/agent-execution.d.ts +67 -9
  3. package/dist/src/agent-execution.js +385 -141
  4. package/dist/src/bundles.d.ts +53 -0
  5. package/dist/src/bundles.js +457 -0
  6. package/dist/src/cli.d.ts +3 -1
  7. package/dist/src/cli.js +156 -22
  8. package/dist/src/doctor-cleanup.js +68 -31
  9. package/dist/src/eval-capture-extension.js +16 -1
  10. package/dist/src/execution.d.ts +1 -1
  11. package/dist/src/execution.js +29 -13
  12. package/dist/src/host.d.ts +12 -9
  13. package/dist/src/host.js +525 -195
  14. package/dist/src/index.d.ts +5 -4
  15. package/dist/src/index.js +3 -2
  16. package/dist/src/persistence.d.ts +43 -8
  17. package/dist/src/persistence.js +54 -0
  18. package/dist/src/registry.d.ts +3 -2
  19. package/dist/src/registry.js +16 -4
  20. package/dist/src/session-inspector.d.ts +12 -2
  21. package/dist/src/session-inspector.js +50 -24
  22. package/dist/src/types.d.ts +141 -60
  23. package/dist/src/types.js +1 -0
  24. package/dist/src/validation.js +28 -7
  25. package/dist/src/workflow-artifacts.d.ts +1 -0
  26. package/dist/src/workflow-artifacts.js +1 -0
  27. package/dist/src/workflow-evals.d.ts +7 -1
  28. package/dist/src/workflow-evals.js +23 -3
  29. package/evals/cases/recovery-completed-worktree.yaml +17 -0
  30. package/evals/cases/recovery-failed-run.yaml +14 -0
  31. package/package.json +1 -1
  32. package/skills/pi-extensible-workflows/SKILL.md +13 -5
  33. package/src/agent-execution.ts +302 -107
  34. package/src/bundles.ts +471 -0
  35. package/src/cli.ts +130 -22
  36. package/src/doctor-cleanup.ts +38 -4
  37. package/src/eval-capture-extension.ts +15 -1
  38. package/src/execution.ts +27 -15
  39. package/src/host.ts +454 -175
  40. package/src/index.ts +5 -4
  41. package/src/persistence.ts +58 -4
  42. package/src/registry.ts +14 -5
  43. package/src/session-inspector.ts +49 -26
  44. package/src/types.ts +55 -30
  45. package/src/validation.ts +19 -6
  46. package/src/workflow-artifacts.ts +1 -0
  47. package/src/workflow-evals.ts +24 -3
package/dist/src/host.js CHANGED
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
7
7
  import { Type } from "@earendil-works/pi-ai";
8
8
  import { Value } from "typebox/value";
9
9
  import { copyToClipboard, getAgentDir, SettingsManager, truncateToVisualLines } from "@earendil-works/pi-coding-agent";
10
- import { createNativeAgentSession, FairAgentScheduler, WorkflowAgentExecutor } from "./agent-execution.js";
10
+ import { FairAgentScheduler, WorkflowAgentExecutor, localAgentTransport } from "./agent-execution.js";
11
11
  import { herdrPaneId, openHerdrPane } from "./herdr.js";
12
12
  import { acquireSessionLease, listRunIds, RunStore, structuralPath as operationPath } from "./persistence.js";
13
13
  import { budgetRelaxed, budgetUsage, mergeBudget, resumeBudgetAllowed, validateBudget, validateBudgetPatch, WorkflowBudgetRuntime } from "./budget.js";
@@ -15,8 +15,8 @@ import { asWorkflowError, aliasDrift, createLaunchSnapshot, deepFreeze, errorCod
15
15
  import { launchScriptForSnapshot, loadAgentDefinitions, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, saveModelAliases, validateAgentOptions, validateCheckpoint, validateModelAliasAvailability, validateShellOptions, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowPrompt, workflowSettingsPath } from "./validation.js";
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
- 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";
18
+ import { openWorkflowArtifact, workflowPromptArtifact, workflowResultArtifact, workflowScriptArtifact } from "./workflow-artifacts.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;
@@ -163,14 +165,14 @@ export class RunLifecycle {
163
165
  export function formatWorkflowPreview(args) {
164
166
  const explicitName = typeof args.name === "string" && args.name.trim() ? args.name.trim() : undefined;
165
167
  const registeredName = typeof args.workflow === "string" && args.workflow.trim() ? args.workflow.trim() : undefined;
166
- const name = registeredName ?? explicitName ?? "workflow";
168
+ const name = explicitName ?? registeredName ?? "workflow";
167
169
  if (typeof args.script !== "string" || !args.script.trim())
168
- return `workflow ${name}${registeredName ? "\nRegistered function" : ""}`;
170
+ return `workflow ${name}${registeredName ? `\nRegistered function${explicitName ? `: ${registeredName}` : ""}` : ""}`;
169
171
  return [`workflow ${name}`, typeof args.description === "string" && args.description.trim() ? args.description.trim() : ""].filter(Boolean).join("\n");
170
172
  }
171
173
  export const WORKFLOW_TOOL_LABEL = "Workflow";
172
- 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.";
174
+ export const WORKFLOW_TOOL_DESCRIPTION = "Run a deterministic JavaScript workflow with a named inline or file-backed parallel-to-summary path by default";
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 and file-backed launches require a non-empty name; registered function launches may use name as an optional run label and otherwise 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")
@@ -194,17 +196,18 @@ function workflowRecoveryGuidance(action, state) {
194
196
  return `Only failed workflow runs can be retried; source is ${state}`;
195
197
  }
196
198
  export const WORKFLOW_TOOL_PARAMETERS = Type.Object({
197
- name: Type.Optional(Type.String({ description: "Required non-empty name for the default inline workflow path; invalid for registered function launches" })),
199
+ name: Type.Optional(Type.String({ description: "Optional run label; required and non-empty for inline or file-backed launches, defaults to the registered function name when omitted" })),
198
200
  description: Type.Optional(Type.String({ description: "Optional human-readable workflow description" })),
199
201
  script: Type.Optional(Type.String({ description: "Immutable inline workflow source; default to a named script that fans out with parallel(...) and awaits results before passing them to a summarizing agent(...)" })),
200
- workflow: Type.Optional(Type.String({ description: "Advanced: registered reusable function as an unqualified name" })),
202
+ scriptPath: Type.Optional(Type.String({ description: "Path to a JavaScript workflow file, read once at launch and persisted as the inline source" })),
203
+ workflow: Type.Optional(Type.String({ description: "Advanced: registered reusable function as an unqualified name; name may optionally label the run" })),
201
204
  args: Type.Optional(Type.Unknown({ description: "JSON-compatible workflow arguments" })),
202
205
  foreground: Type.Optional(Type.Boolean({ description: "Wait for completion instead of the default background launch" })),
203
206
  concurrency: Type.Optional(Type.Integer({ minimum: 1, maximum: 16, description: "Advanced: optional per-run active-agent limit" })),
204
207
  budget: Type.Optional(Type.Unknown({ description: "Advanced: optional aggregate soft and hard run budgets" })),
205
208
  parentRunId: Type.Optional(Type.String({ description: "Advanced: terminal run whose named worktrees may be reused" })),
206
209
  });
207
- export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }) });
210
+ 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
211
  function phaseNames(source) {
209
212
  const phases = source === undefined ? [] : Array.isArray(source) ? source : source.phases ?? [];
210
213
  return phases.filter((phase) => typeof phase === "string" && phase.trim() !== "").map((phase) => phase.trim());
@@ -547,19 +550,36 @@ function styleForState(map, state, styles) {
547
550
  function progressStyleForState(state, styles) { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
548
551
  function workflowIconStyle(state, styles) { return styleForState(WORKFLOW_ICON_STYLE, state, styles); }
549
552
  function phaseStyleForState(state, styles) { return styleForState(PHASE_STATE_STYLE, state, styles); }
550
- export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
553
+ function formatWorkflowRuntime(durationMs) {
554
+ const seconds = Math.max(0, Math.floor(durationMs / 1000));
555
+ if (seconds < 60)
556
+ return `${String(seconds)}s`;
557
+ const minutes = Math.floor(seconds / 60);
558
+ const remainingSeconds = seconds % 60;
559
+ if (minutes < 60)
560
+ return `${String(minutes)}m${remainingSeconds ? ` ${String(remainingSeconds)}s` : ""}`;
561
+ const hours = Math.floor(minutes / 60);
562
+ const remainingMinutes = minutes % 60;
563
+ return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
564
+ }
565
+ export function formatWorkflowProgress(run, spinner = "◇", styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
551
566
  const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
552
567
  const workflowIcon = runStateGlyph(run.state, spinner);
553
568
  const iconStyle = workflowIconStyle(run.state, styles);
554
569
  const header = styles.bold(styles.accent(`Workflow: ${run.workflowName} (${String(done)}/${String(run.agents.length)} done)`));
555
- const lines = [`${iconStyle(workflowIcon)} ${header}`];
570
+ const state = progressStyleForState(run.state, styles)(`[${run.state}]`);
571
+ const runtime = run.usage ? ` runtime=${formatWorkflowRuntime(run.usage.durationMs)}` : "";
572
+ const lines = [`${iconStyle(workflowIcon)} ${header} ${state}${runtime}`];
556
573
  const budgetWarning = run.state === "budget_exhausted" || (run.budgetEvents ?? []).some((event) => event.type === "hard_exhausted");
557
574
  lines.push(...formatCompactBudgetStatus(run).map((line) => ` ${budgetWarning ? styles.warning(line) : line}`));
575
+ const activeShells = run.activeShells ?? 0;
576
+ if (activeShells > 0)
577
+ lines.push(` ${styles.accent(spinner)} shell ${styles.accent("[running]")} ${styles.dim(`(${String(activeShells)} active)`)}`);
558
578
  const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
559
579
  const renderAgents = (agents, offset, nested) => renderGroupedAgents(agents, ({ agent, index, depth }, grouped) => {
560
580
  const icon = agentStateGlyph(agent.state, spinner);
561
581
  const indent = " ".repeat((grouped ? 2 : 1) + depth);
562
- const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles);
582
+ const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles, now);
563
583
  const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
564
584
  const state = progressStyleForState(agent.state, styles);
565
585
  return `${indent}#${String(offset + index + 1)} ${state(icon)} ${name} ${state(`[${agent.state}]`)}${activity ? ` ${activity}` : ""}`;
@@ -581,6 +601,7 @@ function workflowToolUpdate(run) {
581
601
  return { content: [{ type: "text", text: formatWorkflowProgress(run) }], details: { runId: run.id, run } };
582
602
  }
583
603
  const workflowSpinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
604
+ const WORKFLOW_PROGRESS_REFRESH_MS = 1_000;
584
605
  function textBlock(text) {
585
606
  return {
586
607
  render(width) {
@@ -623,7 +644,7 @@ function controlState(state, theme) {
623
644
  return theme.fg(color, state);
624
645
  }
625
646
  function controlAction(action, theme) {
626
- const color = /approved|stopped|started|resumed/.test(action) ? "success" : /rejected|failed/.test(action) ? "error" : "warning";
647
+ const color = /approved|completed|stopped|started|resumed/.test(action) ? "success" : /rejected|failed/.test(action) ? "error" : "warning";
627
648
  return theme.fg(color, action);
628
649
  }
629
650
  function budgetPatchEntries(value) {
@@ -681,14 +702,15 @@ function workflowControlResult(name, args, result, expanded, theme, isError) {
681
702
  if (name === "workflow_retry") {
682
703
  const childRunId = controlString(value.runId) ?? "(unknown)";
683
704
  const state = controlString(value.state) ?? "unknown";
705
+ const action = state === "completed" ? "completed" : "started";
684
706
  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");
707
+ return [title, `Source ${theme.fg("accent", runId)}`, `Child ${theme.fg("accent", childRunId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`].join("\n");
708
+ 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
709
  }
688
710
  if (name === "workflow_resume") {
689
711
  const state = controlString(value.state) ?? "unknown";
690
712
  const proposalId = controlString(value.proposalId);
691
- const action = state === "awaiting_approval" ? "approval required" : state === "running" ? "resumed" : "no change";
713
+ const action = state === "awaiting_approval" ? "approval required" : state === "running" ? "resumed" : state === "completed" ? "completed" : "no change";
692
714
  if (!expanded)
693
715
  return [title, `Run ${theme.fg("accent", runId)} · ${controlState(state, theme)} · ${controlAction(action, theme)}`, ...(proposalId ? [`Proposal ${theme.fg("accent", proposalId)}`] : [])].join("\n");
694
716
  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");
@@ -809,14 +831,41 @@ function themeWorkflowProgressStyles(theme) {
809
831
  bold: (text) => typeof theme.bold === "function" ? theme.bold(text) : text,
810
832
  };
811
833
  }
812
- function workflowProgressBlock(run, theme) {
834
+ function workflowProgressBlock(run, theme, progress, refresh, invalidate) {
813
835
  const styles = themeWorkflowProgressStyles(theme);
836
+ const currentRun = () => {
837
+ const displayed = progress?.run ?? run;
838
+ if (!progress || displayed.state !== "running")
839
+ return displayed;
840
+ const durationMs = Math.max(displayed.usage?.durationMs ?? 0, progress.runtimeBaseMs + Date.now() - progress.runtimeStartedAt);
841
+ return { ...displayed, usage: { ...budgetUsage(displayed.usage), durationMs } };
842
+ };
814
843
  return {
815
844
  render(width) {
816
845
  const frame = workflowSpinner[Math.floor(Date.now() / 80) % workflowSpinner.length] ?? "◇";
817
- return truncateWorkflowProgress(formatWorkflowProgress(run, frame, styles), width);
846
+ return truncateWorkflowProgress(formatWorkflowProgress(currentRun(), frame, styles), width);
847
+ },
848
+ invalidate() {
849
+ const displayed = currentRun();
850
+ if (!progress || !refresh || displayed.state !== "running" || !displayed.agents.some((agent) => agent.state === "running"))
851
+ return;
852
+ const now = Date.now();
853
+ if (progress.refresh || now - progress.lastRefreshAt < WORKFLOW_PROGRESS_REFRESH_MS)
854
+ return;
855
+ progress.lastRefreshAt = now;
856
+ const inputRun = progress.inputRun;
857
+ const pending = refresh().then((next) => {
858
+ if (next && progress.inputRun === inputRun) {
859
+ progress.run = next;
860
+ invalidate?.();
861
+ }
862
+ }).catch(() => undefined);
863
+ progress.refresh = pending;
864
+ void pending.finally(() => {
865
+ if (progress.refresh === pending)
866
+ delete progress.refresh;
867
+ });
818
868
  },
819
- invalidate() { },
820
869
  };
821
870
  }
822
871
  export function formatBudgetStatus(run) {
@@ -888,15 +937,41 @@ function styledAgentBreadcrumb(agent, byId, styles) {
888
937
  return parts[0] ?? "";
889
938
  return `${styles.muted(parts.slice(0, -1).join(" > "))} > ${styles.bold(parts[parts.length - 1] ?? "")}`;
890
939
  }
891
- function formatAgentActivity(agent, spinner, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
940
+ export function formatStalledDuration(durationMs) {
941
+ const minutes = Math.max(0, Math.floor(durationMs / 60_000));
942
+ if (minutes < 60)
943
+ return `${String(minutes)}m`;
944
+ const hours = Math.floor(minutes / 60);
945
+ const remainingMinutes = minutes % 60;
946
+ return `${String(hours)}h${remainingMinutes ? ` ${String(remainingMinutes)}m` : ""}`;
947
+ }
948
+ function stalledDuration(agent, now) {
949
+ if (agent.state !== "running" || agent.lastEventAt === undefined || !Number.isFinite(agent.lastEventAt))
950
+ return undefined;
951
+ const duration = now - agent.lastEventAt;
952
+ return duration >= WORKFLOW_AGENT_STALL_THRESHOLD_MS ? duration : undefined;
953
+ }
954
+ function formatAgentActivity(agent, spinner, styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
892
955
  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)}` : "";
956
+ const activity = label ? `${styles.accent(spinner)} ${styles.dim(label)}` : "";
957
+ const stalled = stalledDuration(agent, now);
958
+ if (stalled === undefined)
959
+ return activity;
960
+ const warning = `stalled? ${formatStalledDuration(stalled)}`;
961
+ return activity ? `${activity} ${styles.warning(`- ${warning}`)}` : styles.warning(warning);
962
+ }
963
+ function formatAccountingValue(value) {
964
+ return new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(value).toLowerCase();
894
965
  }
895
966
  function formatAccounting(accounting) {
896
967
  const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
897
- return `${new Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(total).toLowerCase()} tok`;
968
+ return `${formatAccountingValue(total)} tok`;
969
+ }
970
+ function formatAgentAccounting(accounting) {
971
+ const total = accounting.input + accounting.output + accounting.cacheRead + accounting.cacheWrite;
972
+ 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
973
  }
899
- export function formatNavigatorDashboard(run, checkpoints, worktrees) {
974
+ export function formatNavigatorDashboard(run, checkpoints, worktrees, now = Date.now()) {
900
975
  void worktrees;
901
976
  const done = run.agents.filter((a) => SETTLED_AGENT_STATES.has(a.state)).length;
902
977
  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 +997,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
922
997
  if (last?.error)
923
998
  result.push(`${indent} error: ${last.error.code}: ${last.error.message}`);
924
999
  }
925
- const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦") : "";
1000
+ const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
926
1001
  if (activity)
927
1002
  result.push(`${indent} ${activity}`);
928
1003
  return result.join("\n");
@@ -935,7 +1010,7 @@ export function formatNavigatorDashboard(run, checkpoints, worktrees) {
935
1010
  }
936
1011
  return lines.join("\n");
937
1012
  }
938
- export function formatNavigatorRun(loaded, checkpoints, worktrees) {
1013
+ export function formatNavigatorRun(loaded, checkpoints, worktrees, now = Date.now()) {
939
1014
  const { run, snapshot } = loaded;
940
1015
  const lines = [
941
1016
  `Workflow: ${run.workflowName}`,
@@ -971,6 +1046,9 @@ export function formatNavigatorRun(loaded, checkpoints, worktrees) {
971
1046
  result.push(`${indent} attempt ${String(attempt.attempt)}${attempt.error ? ` error=${attempt.error.code}: ${attempt.error.message}` : ""}`);
972
1047
  for (const call of agent.toolCalls ?? [])
973
1048
  result.push(`${indent} tool ${call.name} state=${call.state}`);
1049
+ const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
1050
+ if (activity)
1051
+ result.push(`${indent} ${activity}`);
974
1052
  return result.join("\n");
975
1053
  }));
976
1054
  lines.push("Checkpoints:");
@@ -979,10 +1057,10 @@ export function formatNavigatorRun(loaded, checkpoints, worktrees) {
979
1057
  for (const checkpoint of checkpoints)
980
1058
  lines.push(` ${checkpoint.name}: ${checkpoint.prompt} context=${JSON.stringify(checkpoint.context)}`);
981
1059
  lines.push(`Worktrees: ${String(worktrees.length)}`);
982
- lines.push(`Native Pi transcripts: ${String(run.nativeSessions.length)}`);
1060
+ lines.push(`Agent sessions: ${String(run.agentSessions.length)}`);
983
1061
  return lines.join("\n");
984
1062
  }
985
- export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {}, styles = PLAIN_WORKFLOW_PROGRESS_STYLES) {
1063
+ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {}, styles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now()) {
986
1064
  const safeWidth = Math.max(1, width);
987
1065
  const model = buildWorkflowPhaseModel(run, snapshot);
988
1066
  const tree = buildWorkflowPhaseTree(model);
@@ -1022,7 +1100,8 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
1022
1100
  const treeLine = (node) => {
1023
1101
  const selected = node.id === selectedNode?.id;
1024
1102
  const state = progressStyleForState(node.state, styles);
1025
- return `${selected ? "→" : " "} ${" ".repeat(node.depth)}${nodeIcon(node)} ${node.label} · ${state(node.state)}`;
1103
+ const activity = node.agent && !SETTLED_AGENT_STATES.has(node.agent.state) ? formatAgentActivity(node.agent, "⠦", styles, now) : "";
1104
+ return `${selected ? "→" : " "} ${" ".repeat(node.depth)}${nodeIcon(node)} ${node.label} · ${state(node.state)}${activity ? ` ${activity}` : ""}`;
1026
1105
  };
1027
1106
  const details = (node) => {
1028
1107
  if (!node)
@@ -1041,12 +1120,15 @@ export function formatWorkflowPhaseDashboard(run, snapshot, width, selection = {
1041
1120
  if (!agent)
1042
1121
  return [styles.muted("Agent details are unavailable")];
1043
1122
  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")])];
1123
+ 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)"}`, ...(agent.systemPrompt === undefined ? [] : [`System prompt: ${agent.systemPrompt}`]), `Attempts: ${String(agent.attempts)}`, ...(agent.accounting ? formatAgentAccounting(agent.accounting) : []), ...(selection.actions ? [] : [styles.muted("enter for agent actions")])];
1045
1124
  const error = agent.attemptDetails?.at(-1)?.error;
1046
1125
  if (error)
1047
1126
  result.push(styles.error(`Error: ${error.code}: ${error.message}`));
1048
1127
  if (agent.activity)
1049
1128
  result.push(`Activity: ${agent.activity.text}`);
1129
+ const stalled = stalledDuration(agent, now);
1130
+ if (stalled !== undefined)
1131
+ result.push(styles.warning(`stalled? ${formatStalledDuration(stalled)}`));
1050
1132
  return result;
1051
1133
  };
1052
1134
  const stateNames = ["not started", "running", "completed", "failed", "cancelled", "interrupted", "budget_exhausted"];
@@ -1130,8 +1212,8 @@ function boundedWorkflowFailureDiagnostics(value) {
1130
1212
  ...(value.failedAgent.role ? { role: utf8Prefix(value.failedAgent.role, 128) } : {}),
1131
1213
  structuralPath: value.failedAgent.structuralPath.slice(0, 8).map((part) => utf8Prefix(part, 128)),
1132
1214
  attempt: value.failedAgent.attempt,
1133
- ...(value.failedAgent.sessionId ? { sessionId: utf8Prefix(value.failedAgent.sessionId, 256) } : {}),
1134
- ...(value.failedAgent.sessionFile ? { sessionFile: utf8Prefix(value.failedAgent.sessionFile, 1024) } : {}),
1215
+ ...(value.failedAgent.transport ? { transport: utf8Prefix(value.failedAgent.transport, 128) } : {}),
1216
+ ...(value.failedAgent.session ? { session: structuredClone(value.failedAgent.session) } : {}),
1135
1217
  } } : {}),
1136
1218
  completedSiblingAgents: (value.completedSiblingAgents ?? []).slice(0, 16).map((agent) => ({
1137
1219
  id: utf8Prefix(agent.id, 128),
@@ -1160,18 +1242,6 @@ function boundedWorkflowFailureDiagnostics(value) {
1160
1242
  bounded = { ...bounded, retry };
1161
1243
  continue;
1162
1244
  }
1163
- if (bounded.failedAgent?.sessionFile) {
1164
- const failedAgent = { ...bounded.failedAgent };
1165
- delete failedAgent.sessionFile;
1166
- bounded = { ...bounded, failedAgent };
1167
- continue;
1168
- }
1169
- if (bounded.failedAgent?.sessionId) {
1170
- const failedAgent = { ...bounded.failedAgent };
1171
- delete failedAgent.sessionId;
1172
- bounded = { ...bounded, failedAgent };
1173
- continue;
1174
- }
1175
1245
  if (Buffer.byteLength(bounded.artifacts.runDirectory) > 256) {
1176
1246
  bounded = { ...bounded, artifacts: { ...bounded.artifacts, runDirectory: utf8Prefix(bounded.artifacts.runDirectory, 256) } };
1177
1247
  continue;
@@ -1244,8 +1314,8 @@ async function createWorkflowFailureDiagnostics(store, metadata, error, run) {
1244
1314
  ...(failedAgentRecord.role ? { role: failedAgentRecord.role } : {}),
1245
1315
  structuralPath: [...(failedAgentRecord.structuralPath ?? [])],
1246
1316
  attempt: Math.max(1, failedAttempt?.attempt ?? failedAgentRecord.attempts),
1247
- ...(failedAttempt?.sessionId ? { sessionId: failedAttempt.sessionId } : {}),
1248
- ...(failedAttempt?.sessionFile ? { sessionFile: failedAttempt.sessionFile } : {}),
1317
+ ...(failedAttempt?.transport ? { transport: failedAttempt.transport } : {}),
1318
+ ...(failedAttempt?.session ? { session: failedAttempt.session } : {}),
1249
1319
  } : undefined;
1250
1320
  const completedSiblingAgents = run.agents.filter((agent) => {
1251
1321
  if (agent.state !== "completed" || agent.id === failedAgentRecord?.id)
@@ -1282,21 +1352,41 @@ async function createWorkflowFailureDiagnostics(store, metadata, error, run) {
1282
1352
  });
1283
1353
  }
1284
1354
  export function formatWorkflowFailureDiagnostics(diagnostic) {
1285
- const failedAgent = diagnostic.failedAgent ? `${diagnostic.failedAgent.label ?? diagnostic.failedAgent.id}${diagnostic.failedAgent.role ? ` role=${diagnostic.failedAgent.role}` : ""} attempt=${String(diagnostic.failedAgent.attempt)} path=${diagnostic.failedAgent.structuralPath.join(" > ") || "(root)"}${diagnostic.failedAgent.sessionFile ? ` session=${diagnostic.failedAgent.sessionFile}` : ""}` : "(not persisted)";
1355
+ const failedAgent = diagnostic.failedAgent ? `${diagnostic.failedAgent.label ?? diagnostic.failedAgent.id}${diagnostic.failedAgent.role ? ` role=${diagnostic.failedAgent.role}` : ""} attempt=${String(diagnostic.failedAgent.attempt)} path=${diagnostic.failedAgent.structuralPath.join(" > ") || "(root)"}${diagnostic.failedAgent.session ? ` session=${diagnostic.failedAgent.session.transport}/${diagnostic.failedAgent.session.sessionId}` : ""}` : "(not persisted)";
1286
1356
  const siblingAgents = diagnostic.completedSiblingAgents;
1287
1357
  const siblings = siblingAgents ? siblingAgents.map((agent) => `${agent.label ?? agent.id}${agent.role ? ` role=${agent.role}` : ""} path=${agent.structuralPath.join(" > ") || "(root)"}`).join(", ") || "(none)" : diagnostic.completedSiblingPaths.map((path) => path.join(" > ") || "(root)").join(", ") || "(none)";
1288
1358
  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
1359
  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
1360
  }
1361
+ function deliveryPart(value, maxBytes) { return utf8Prefix(value.replace(/\s+/g, " ").trim(), maxBytes) || "(unknown)"; }
1362
+ export function formatWorkflowFailureDelivery(diagnostic) {
1363
+ const name = deliveryPart(diagnostic.workflowName, 128);
1364
+ const runId = deliveryPart(diagnostic.runId, 128);
1365
+ const error = `${diagnostic.error.code}: ${deliveryPart(diagnostic.error.message, 768)}`;
1366
+ const failedPath = diagnostic.failedAt ? `; failed path=${deliveryPart(diagnostic.failedAt, 512)}` : "";
1367
+ const nextAction = diagnostic.retry ? `; next action: ${deliveryPart(diagnostic.retry.action, 256)}` : "";
1368
+ const artifacts = `; artifacts: runDirectory=${deliveryPart(diagnostic.artifacts.runDirectory, 512)} statePath=${deliveryPart(diagnostic.artifacts.statePath, 512)} journalPath=${deliveryPart(diagnostic.artifacts.journalPath, 512)}`;
1369
+ const line = `Workflow ${name} failed (runId=${runId}): error=${error}${failedPath}${nextAction}${artifacts}`;
1370
+ return Buffer.byteLength(line) <= DELIVERY_LIMIT_BYTES ? line : utf8Prefix(line, DELIVERY_LIMIT_BYTES);
1371
+ }
1372
+ function formatWorkflowFailureDeliveryFallback(workflowName, runId, runDirectory, error) {
1373
+ const code = errorCode(error) ?? "INTERNAL_ERROR";
1374
+ const failedPath = workflowFailedAt(error);
1375
+ const nextAction = code === "BUDGET_EXHAUSTED" || code === "CANCELLED" ? "" : `; next action: workflow_retry({ runId: ${JSON.stringify(runId)} })`;
1376
+ 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)}`;
1377
+ return Buffer.byteLength(line) <= DELIVERY_LIMIT_BYTES ? line : utf8Prefix(line, DELIVERY_LIMIT_BYTES);
1378
+ }
1291
1379
  function serializeWorkflowFailureDiagnostics(diagnostic) { return JSON.stringify(diagnostic); }
1292
1380
  function isWorkflowFailureDiagnostics(value) {
1293
1381
  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
1382
  }
1295
1383
  function deliver(pi, content) {
1384
+ if (typeof pi.sendMessage !== "function")
1385
+ return;
1296
1386
  pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
1297
1387
  }
1298
1388
  function deliverFailure(pi, diagnostic) {
1299
- deliver(pi, `Workflow ${utf8Prefix(diagnostic.workflowName, 128)} failure diagnostics: ${serializeWorkflowFailureDiagnostics(diagnostic)}`);
1389
+ deliver(pi, formatWorkflowFailureDelivery(diagnostic));
1300
1390
  }
1301
1391
  function safeEventError(error) {
1302
1392
  const code = errorCode(error) ?? "INTERNAL_ERROR";
@@ -1633,6 +1723,7 @@ function tuiRows(tui) {
1633
1723
  const rows = object(tui) && object(tui.terminal) ? tui.terminal.rows : undefined;
1634
1724
  return typeof rows === "number" && Number.isFinite(rows) ? rows : 24;
1635
1725
  }
1726
+ const WORKFLOW_PANEL_FOOTER_ROWS = 2;
1636
1727
  const WORKFLOW_OVERLAY_BORDER_ROWS = 2;
1637
1728
  const WORKFLOW_OVERLAY_TOP_MARGIN = 1;
1638
1729
  const WORKFLOW_OVERLAY_OPTIONS = { anchor: "top-left", width: "100%", maxHeight: "100%", margin: { top: WORKFLOW_OVERLAY_TOP_MARGIN } };
@@ -1649,7 +1740,15 @@ function keybindingKeys(keybindings, name) {
1649
1740
  const getKeys = object(keybindings) ? asFn(keybindings.getKeys) : undefined;
1650
1741
  return getKeys ? getKeys.call(keybindings, name) : undefined;
1651
1742
  }
1652
- export default function workflowExtension(pi, home, clipboard = copyToClipboard, createSession = createNativeAgentSession, agentDir) {
1743
+ const WORKFLOW_VIM_KEYS = { "tui.select.up": "k", "tui.select.down": "j", "tui.editor.cursorLeft": "h", "tui.editor.cursorRight": "l" };
1744
+ function workflowKeyMatches(keybindings, data, binding) { return keybindings.matches(data, binding) || WORKFLOW_VIM_KEYS[binding] === data; }
1745
+ function workflowKeyLabel(keybindings, binding, fallback, labels) {
1746
+ const keys = keybindingKeys(keybindings, binding);
1747
+ const configured = keys?.length ? keys.map((key) => labels[key] ?? key) : [fallback];
1748
+ const vim = WORKFLOW_VIM_KEYS[binding];
1749
+ return [...new Set(vim ? [...configured, vim] : configured)].join("/");
1750
+ }
1751
+ export default function workflowExtension(pi, home, clipboard = copyToClipboard, transport = localAgentTransport, agentDir, additionalSkillPaths = []) {
1653
1752
  beginWorkflowExtensionLoading();
1654
1753
  const registry = loadingRegistry();
1655
1754
  const extensionAgentDir = agentDir ?? getAgentDir();
@@ -1702,16 +1801,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1702
1801
  });
1703
1802
  };
1704
1803
  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
- });
1804
+ const foregroundDeliveries = new Map();
1714
1805
  const liveActivities = new Map();
1806
+ const liveEventTimes = new Map();
1807
+ const liveAgentSessions = new Map();
1808
+ const setLiveAgentSession = (runId, agentId, session) => { const key = `${runId}:${agentId}`; if (session)
1809
+ liveAgentSessions.set(key, session);
1810
+ else
1811
+ liveAgentSessions.delete(key); };
1715
1812
  const setLiveActivity = (runId, agentId, activity) => {
1716
1813
  const activities = liveActivities.get(runId);
1717
1814
  if (activity) {
@@ -1726,12 +1823,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1726
1823
  liveActivities.delete(runId);
1727
1824
  }
1728
1825
  };
1826
+ const setLiveEventTime = (runId, agentId, timestamp) => {
1827
+ if (timestamp === undefined)
1828
+ return;
1829
+ const timestamps = liveEventTimes.get(runId);
1830
+ if (timestamps)
1831
+ timestamps.set(agentId, timestamp);
1832
+ else
1833
+ liveEventTimes.set(runId, new Map([[agentId, timestamp]]));
1834
+ };
1729
1835
  const withLiveActivities = (run) => {
1730
1836
  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;
1837
+ const timestamps = liveEventTimes.get(run.id);
1838
+ if (!activities?.size && !timestamps?.size)
1839
+ return run;
1840
+ return { ...run, agents: run.agents.map((agent) => {
1841
+ const activity = activities?.get(agent.id);
1842
+ const lastEventAt = timestamps?.get(agent.id);
1843
+ if (activity === undefined && lastEventAt === undefined)
1844
+ return agent;
1845
+ return { ...agent, ...(activity === undefined ? {} : { activity }), ...(lastEventAt === undefined ? {} : { lastEventAt }) };
1846
+ }) };
1735
1847
  };
1736
1848
  const terminalRunStates = new Map();
1737
1849
  let sessionLease;
@@ -1759,6 +1871,52 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1759
1871
  await eventPublisher.budget(store, metadata, persisted);
1760
1872
  return persisted;
1761
1873
  };
1874
+ pi.on("tool_result", async (event) => {
1875
+ const delivery = event.toolName === "workflow" ? foregroundDeliveries.get(event.toolCallId) : undefined;
1876
+ if (delivery) {
1877
+ if (delivery.timer)
1878
+ clearTimeout(delivery.timer);
1879
+ delivery.inline = true;
1880
+ await delivery.store.updateState((current) => {
1881
+ if (current.delivery?.toolCallId !== event.toolCallId || current.delivery.state === "delivered")
1882
+ return current;
1883
+ return { ...current, delivery: { ...current.delivery, state: "delivered" } };
1884
+ });
1885
+ foregroundDeliveries.delete(event.toolCallId);
1886
+ }
1887
+ if (event.toolName !== "workflow" || !event.isError)
1888
+ return;
1889
+ const diagnostic = pendingFailureDiagnostics.get(event.toolCallId);
1890
+ if (!diagnostic)
1891
+ return;
1892
+ pendingFailureDiagnostics.delete(event.toolCallId);
1893
+ return { content: [{ type: "text", text: serializeWorkflowFailureDiagnostics(diagnostic) }], details: diagnostic, isError: true };
1894
+ });
1895
+ const deliverTerminal = async (store, content) => {
1896
+ let claimed;
1897
+ await store.updateState((current) => {
1898
+ if (current.delivery?.state === "delivered")
1899
+ return current;
1900
+ if (!current.delivery) {
1901
+ claimed = true;
1902
+ return current;
1903
+ }
1904
+ claimed = true;
1905
+ return { ...current, delivery: { ...current.delivery, mode: "background", state: "delivered" } };
1906
+ });
1907
+ if (claimed === true)
1908
+ deliver(pi, content);
1909
+ };
1910
+ const scheduleForegroundDelivery = (toolCallId, send) => {
1911
+ const delivery = foregroundDeliveries.get(toolCallId);
1912
+ if (!delivery || delivery.inline || typeof pi.sendMessage !== "function")
1913
+ return;
1914
+ //NOTE: Give Pi one event-loop turn to deliver an uninterrupted tool result before promoting.
1915
+ delivery.timer = setTimeout(() => {
1916
+ delete delivery.timer;
1917
+ void send().finally(() => foregroundDeliveries.delete(toolCallId));
1918
+ }, 0);
1919
+ };
1762
1920
  const phaseBridge = (store, metadata, lifecycle) => {
1763
1921
  let cursor = 0;
1764
1922
  return async (phase) => {
@@ -1811,10 +1969,25 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1811
1969
  const replayed = await store.replay(path);
1812
1970
  if (replayed)
1813
1971
  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;
1972
+ const started = await persistRunState(store, metadata, (current) => ({ ...current, activeShells: (current.activeShells ?? 0) + 1 }));
1973
+ runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(started)));
1974
+ try {
1975
+ const cwd = identity.worktreeOwner ? (await persistWorktree(store, metadata, identity.worktreeOwner)).cwd : store.cwd;
1976
+ const result = await executeShellCommand(command, options, signal, cwd);
1977
+ await store.complete(path, result);
1978
+ return result;
1979
+ }
1980
+ finally {
1981
+ const stopped = await persistRunState(store, metadata, (current) => {
1982
+ const activeShells = Math.max(0, (current.activeShells ?? 0) - 1);
1983
+ if (activeShells > 0)
1984
+ return { ...current, activeShells };
1985
+ const next = { ...current };
1986
+ delete next.activeShells;
1987
+ return next;
1988
+ });
1989
+ runs.get(store.runId)?.update?.(workflowToolUpdate(withLiveActivities(stopped)));
1990
+ }
1818
1991
  }
1819
1992
  finally {
1820
1993
  await lifecycle.leave();
@@ -1825,8 +1998,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1825
1998
  budget.transition(next);
1826
1999
  const persisted = await persistRunState(store, metadata, (current) => {
1827
2000
  const nextRun = { ...current, state: next, ...budget.snapshot() };
1828
- if (next === "running" || next === "completed")
2001
+ if (next === "running" || next === "completed") {
1829
2002
  delete nextRun.error;
2003
+ delete nextRun.failedAt;
2004
+ }
1830
2005
  return nextRun;
1831
2006
  });
1832
2007
  await eventPublisher.runState(store, metadata, previous, next, reason);
@@ -1841,28 +2016,32 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1841
2016
  const onProgress = async (progress) => {
1842
2017
  let runState;
1843
2018
  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);
2019
+ 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, ...(progress.state ? { model: progress.state.model, tools: progress.state.tools, ...(progress.state.systemPrompt === undefined ? {} : { systemPrompt: progress.state.systemPrompt }) } : {}), activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) } : current);
1845
2020
  }
1846
2021
  else {
1847
2022
  const loaded = await run.store.load();
1848
2023
  if (!loaded.run.agents.some((agent) => agent.id === id))
1849
2024
  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) };
2025
+ runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls, ...(progress.state ? { model: progress.state.model, tools: progress.state.tools, ...(progress.state.systemPrompt === undefined ? {} : { systemPrompt: progress.state.systemPrompt }) } : {}), activity: progress.activity, ...(progress.lastEventAt === undefined ? {} : { lastEventAt: progress.lastEventAt }) } : agent) };
1851
2026
  }
1852
2027
  if (!runState.agents.some((agent) => agent.id === id))
1853
2028
  return;
1854
2029
  setLiveActivity(runId, id, progress.activity);
2030
+ setLiveEventTime(runId, id, progress.lastEventAt);
1855
2031
  run.update?.(workflowToolUpdate(withLiveActivities(runState)));
1856
2032
  };
1857
2033
  const onAttempt = async (attempt) => {
2034
+ setLiveAgentSession(runId, id, attempt.liveSession);
1858
2035
  await scheduler.flush();
1859
2036
  scheduler.attemptStarted(id);
2037
+ const lastEventAt = Date.now();
2038
+ setLiveEventTime(runId, id, lastEventAt);
1860
2039
  await scheduler.flush();
1861
2040
  const before = (await run.store.load()).run;
1862
2041
  await persistActiveAgentAttempt(run.store, id, attempt);
1863
2042
  const active = (await run.store.load()).run;
1864
2043
  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() }));
2044
+ 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
2045
  run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
1867
2046
  };
1868
2047
  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); });
@@ -1872,10 +2051,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1872
2051
  await eventPublisher.agentStates(run.store, run.metadata, before.agents, completed.agents);
1873
2052
  const persisted = await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
1874
2053
  setLiveActivity(runId, id);
2054
+ setLiveAgentSession(runId, id);
1875
2055
  run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
1876
2056
  return result.value;
1877
2057
  }
1878
2058
  catch (error) {
2059
+ setLiveAgentSession(runId, id);
1879
2060
  const attempts = error.attempts;
1880
2061
  if (attempts?.length) {
1881
2062
  const before = (await run.store.load()).run;
@@ -1908,7 +2089,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1908
2089
  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
2090
  }
1910
2091
  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 } : {}) };
2092
+ const lastEventAt = node.state === "running" ? previous?.state === "running" && previous.lastEventAt !== undefined ? previous.lastEventAt : Date.now() : previous?.lastEventAt;
2093
+ return { ...(previous?.systemPrompt === undefined ? {} : { systemPrompt: previous.systemPrompt }), ...(node.prompt !== undefined ? { prompt: node.prompt } : previous?.prompt !== undefined ? { prompt: previous.prompt } : {}), 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
2094
  });
1913
2095
  return { ...current, agents };
1914
2096
  });
@@ -1927,6 +2109,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1927
2109
  terminalRunStates.set(runId, run.lifecycle.state);
1928
2110
  run.checkpointResolvers.clear();
1929
2111
  liveActivities.delete(runId);
2112
+ liveEventTimes.delete(runId);
2113
+ for (const key of liveAgentSessions.keys())
2114
+ if (key.startsWith(`${runId}:`))
2115
+ liveAgentSessions.delete(key);
1930
2116
  eventPublisher.removeRun(runId);
1931
2117
  runs.delete(runId);
1932
2118
  };
@@ -1967,7 +2153,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1967
2153
  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
2154
  await persistRunState(run.store, run.metadata, (current) => ({ ...current, ...run.budget.snapshot() }));
1969
2155
  };
1970
- const answerBudgetDecision = async (runId, proposalId, approved, silent = false, context, signal) => {
2156
+ const answerBudgetDecision = async (runId, proposalId, approved, silent = false, context, signal, waitForCompletion = true) => {
1971
2157
  const run = runs.get(runId);
1972
2158
  if (!run)
1973
2159
  return undefined;
@@ -1975,7 +2161,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
1975
2161
  if (!request)
1976
2162
  return undefined;
1977
2163
  await appendBudgetDecisionEvent(run, request, approved ? "adjustment_approved" : "adjustment_rejected");
1978
- const result = await applyBudgetDecision(request, approved, context, signal);
2164
+ const result = await applyBudgetDecision(request, approved, context, signal, waitForCompletion);
1979
2165
  if (!silent)
1980
2166
  deliver(pi, `Workflow ${run.metadata.name} budget adjustment ${proposalId}: ${approved ? "Approved" : "Rejected"}.`);
1981
2167
  return result;
@@ -2088,7 +2274,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2088
2274
  pi.registerTool({
2089
2275
  name: "workflow_catalog",
2090
2276
  label: "Workflow Catalog",
2091
- description: "List reusable workflow functions, variables, and model aliases, or load one entry in full",
2277
+ description: "List reusable workflow functions, variables, and model aliases; pass `name` to load one entry in full",
2092
2278
  parameters: Type.Object({ name: Type.Optional(Type.String({ description: "Registered function, variable, or model alias name for full detail" })) }, { additionalProperties: false }),
2093
2279
  async execute(_id, params = {}) {
2094
2280
  const context = { cwd, projectTrusted: trustedProject, globalSettingsPath: workflowSettingsPath(extensionAgentDir) };
@@ -2105,7 +2291,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2105
2291
  });
2106
2292
  catalogRegistered = true;
2107
2293
  };
2108
- const createAgentExecutor = (root) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, agentSetupHooks: registry.agentSetupHooks() }, createSession);
2294
+ const createAgentExecutor = (root) => new WorkflowAgentExecutor({ ...root, agentDir: extensionAgentDir, ...(additionalSkillPaths.length ? { additionalSkillPaths } : {}), agentSetupHooks: registry.agentSetupHooks() }, transport);
2109
2295
  const activeSnapshotTools = (tools, active) => active === "session"
2110
2296
  ? new Set(tools.filter((tool) => pi.getActiveTools().includes(tool) && tool !== "workflow_catalog"))
2111
2297
  : new Set(tools.filter((tool) => active.has(tool) || tool === "workflow_catalog"));
@@ -2135,7 +2321,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2135
2321
  const snapshot = createLaunchSnapshot({ ...input.snapshot, settingsPath, ...refreshed, modelAliases: currentAliases });
2136
2322
  return { active, settingsPath, resolution, currentPolicy, previousAliases, knownModels, availableModels, currentAliases, blockedAliases, blockedAliasTargets, snapshot, script };
2137
2323
  };
2138
- const workflowAgentHandler = (store, metadata, lifecycle, executor, cwd, runId) => async (prompt, options, agentSignal, identity) => {
2324
+ const workflowAgentHandler = (store, metadata, lifecycle, executor, cwd, runId, captureRole) => async (prompt, options, agentSignal, identity) => {
2139
2325
  await lifecycle.enter();
2140
2326
  try {
2141
2327
  const path = agentIdentityPath(identity);
@@ -2150,6 +2336,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2150
2336
  const thinking = parseThinking(options.thinking);
2151
2337
  const requestedLabel = typeof options.label === "string" ? options.label : undefined;
2152
2338
  const resolved = executor.resolve({ label: requestedLabel ?? role ?? "agent", workflowName: metadata.name, ...(model ? { model } : {}), ...(thinking ? { thinking } : {}), ...(role ? { role } : {}), ...(Array.isArray(options.tools) ? { tools: options.tools } : {}) });
2339
+ if (role)
2340
+ await captureRole?.(role, resolved.model);
2153
2341
  const label = displayAgentName(requestedLabel, role, resolved.model);
2154
2342
  const tools = resolved.tools;
2155
2343
  const schema = object(options.outputSchema) ? options.outputSchema : undefined;
@@ -2182,8 +2370,21 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2182
2370
  if (drift.length)
2183
2371
  await run.store.appendEvent({ type: "warning", message: `Model alias mappings changed on resume: ${drift.join("; ")}` });
2184
2372
  };
2185
- const coldResumeRun = async (run, hasUI, ui, trustedProject, context) => {
2373
+ const recoveryUi = (context) => {
2374
+ const host = object(context) ? context : undefined;
2375
+ const ui = host && object(host.ui) ? host.ui : {};
2376
+ return { hasUI: host?.hasUI === true, ui };
2377
+ };
2378
+ const coldResumeRun = async (run, hasUI, ui, trustedProject, context, modeOverride, waitForCompletion = true) => {
2186
2379
  const loaded = await run.store.load();
2380
+ const foreground = modeOverride ?? loaded.snapshot.launchMode === "foreground";
2381
+ if (loaded.run.activeShells !== undefined) {
2382
+ await persistRunState(run.store, run.metadata, (current) => {
2383
+ const next = { ...current };
2384
+ delete next.activeShells;
2385
+ return next;
2386
+ });
2387
+ }
2187
2388
  await run.store.validateRetrySource();
2188
2389
  await run.store.validateBorrowedWorktrees();
2189
2390
  if (loaded.snapshot.identityVersion !== LAUNCH_SNAPSHOT_IDENTITY_VERSION)
@@ -2206,7 +2407,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2206
2407
  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
2408
  if (!script)
2208
2409
  throw new WorkflowError("INTERNAL_ERROR", "Resume preflight did not produce a launch script");
2209
- await run.store.saveSnapshot(snapshot);
2410
+ const persistedSnapshot = modeOverride === undefined ? snapshot : createLaunchSnapshot({ ...snapshot, launchMode: foreground ? "foreground" : "background" });
2411
+ await run.store.saveSnapshot(persistedSnapshot);
2210
2412
  scheduler.updateRunLimit(run.store.runId, snapshot.settings.concurrency);
2211
2413
  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
2414
  const drift = aliasDrift(previousAliases, currentAliases);
@@ -2222,7 +2424,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2222
2424
  const typed = asWorkflowError(error);
2223
2425
  if (!HARD_TERMINAL_RUN_STATES.has(run.lifecycle.state)) {
2224
2426
  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 } }));
2427
+ const persisted = await persistRunState(run.store, run.metadata, (current) => persistedFailure({ ...current }, typed));
2226
2428
  await eventPublisher.runFailed(run.store, run.metadata, typed, run.lifecycle.state === "interrupted" ? "interrupted" : "failed");
2227
2429
  run.update?.(workflowToolUpdate(persisted));
2228
2430
  if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
@@ -2233,7 +2435,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2233
2435
  }
2234
2436
  await scheduler.cancelRun(run.store.runId);
2235
2437
  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);
2438
+ 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
2439
  run.execution = execution;
2238
2440
  const completion = execution.result.then(async (value) => {
2239
2441
  await scheduler.flush();
@@ -2248,19 +2450,34 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2248
2450
  const typed = error instanceof WorkflowError ? error : new WorkflowError(errorCode(error) ?? "INTERNAL_ERROR", errorText(error));
2249
2451
  if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state))
2250
2452
  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 } }));
2453
+ const persisted = await persistRunState(run.store, run.metadata, (current) => persistedFailure({ ...current, ...run.budget.snapshot() }, typed));
2252
2454
  const state = run.lifecycle.state === "stopped" || run.lifecycle.state === "interrupted" || run.lifecycle.state === "budget_exhausted" ? run.lifecycle.state : "failed";
2253
2455
  if (state === "failed")
2254
2456
  retryReservations.delete(persisted.retry?.lineageRootRunId ?? run.store.runId);
2255
2457
  await eventPublisher.runFailed(run.store, run.metadata, typed, state);
2256
2458
  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);
2459
+ if (!["stopped", "interrupted", "budget_exhausted"].includes(run.lifecycle.state)) {
2460
+ const diagnostic = await createWorkflowFailureDiagnostics(run.store, run.metadata, typed, persisted);
2461
+ Object.defineProperty(typed, WORKFLOW_FAILURE_DIAGNOSTICS, { value: diagnostic });
2462
+ }
2463
+ throw typed;
2259
2464
  }).finally(() => cleanupTerminalRun(run.store.runId));
2260
2465
  run.completion = completion;
2261
- void completion;
2466
+ if (!foreground || !waitForCompletion) {
2467
+ void completion.then(async ({ value, resultPath }) => {
2468
+ deliver(pi, completionDelivery(run.metadata.name, value, resultPath, await run.store.changedWorktrees()));
2469
+ }, (error) => {
2470
+ const diagnostic = failureDiagnosticsFrom(error);
2471
+ if (diagnostic)
2472
+ deliverFailure(pi, diagnostic);
2473
+ else
2474
+ deliver(pi, formatWorkflowFailureDeliveryFallback(run.metadata.name, run.store.runId, run.store.directory, error));
2475
+ });
2476
+ return undefined;
2477
+ }
2478
+ return completion;
2262
2479
  };
2263
- const applyBudgetDecision = async (request, approved, context, signal) => {
2480
+ const applyBudgetDecision = async (request, approved, context, signal, waitForCompletion = true) => {
2264
2481
  const run = runs.get(request.runId);
2265
2482
  if (!run)
2266
2483
  throw new WorkflowError("RESUME_INCOMPATIBLE", `Unknown workflow run: ${request.runId}`);
@@ -2274,10 +2491,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2274
2491
  next.budget = nextBudget;
2275
2492
  else
2276
2493
  delete next.budget; return next; });
2277
- await coldResumeRun(run, false, {}, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) });
2494
+ const { hasUI, ui } = recoveryUi(context);
2495
+ const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, undefined, waitForCompletion);
2496
+ if (completed)
2497
+ return { state: "completed", approved: true, value: completed.value, run: (await run.store.load()).run };
2278
2498
  return { state: "running", approved: true };
2279
2499
  };
2280
- const resumeWorkflowRun = async (runId, rawPatch, context, signal) => {
2500
+ const resumeWorkflowRun = async (runId, rawPatch, context, signal, modeOverride, waitForCompletion = true) => {
2281
2501
  const run = runs.get(runId);
2282
2502
  if (!run) {
2283
2503
  const host = object(context) ? context : {};
@@ -2323,11 +2543,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2323
2543
  else
2324
2544
  delete next.budget; return next; });
2325
2545
  }
2326
- await coldResumeRun(run, false, {}, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) });
2546
+ const { hasUI, ui } = recoveryUi(context);
2547
+ const completed = await coldResumeRun(run, hasUI, ui, projectTrusted(context), { ...resumeHostContext(context), ...(signal ? { signal } : {}) }, modeOverride, waitForCompletion);
2548
+ if (completed)
2549
+ return { state: "completed", runId, value: completed.value, run: (await run.store.load()).run };
2327
2550
  return { state: "running" };
2328
2551
  };
2329
2552
  const retryReservations = new Set();
2330
- const retryWorkflowRun = async (runId, context, signal) => {
2553
+ const retryWorkflowRun = async (runId, context, signal, modeOverride) => {
2331
2554
  if (typeof runId !== "string" || !runId.trim())
2332
2555
  throw new WorkflowError("RESUME_INCOMPATIBLE", "workflow_retry requires an explicit run ID");
2333
2556
  const host = object(context) ? context : {};
@@ -2399,7 +2622,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2399
2622
  const childBudget = new WorkflowBudgetRuntime(budget, loaded.run.budgetVersion ?? 1, loaded.run.usage, loaded.run.budgetEvents);
2400
2623
  const childInitialBudget = childBudget.snapshot();
2401
2624
  const retry = { sourceRunId: loaded.run.id, lineageRootRunId, completedPaths, incompletePaths, namedWorktrees };
2402
- await childStore.create({ id: childRunId, workflowName: loaded.snapshot.metadata.name, cwd, sessionId, state: "interrupted", parentRunId: loaded.run.id, retry, agents: [], nativeSessions: [], ...(budget ? { budget } : {}), budgetVersion: loaded.run.budgetVersion ?? 1, ...childInitialBudget }, childSnapshot);
2625
+ await childStore.create({ id: childRunId, workflowName: loaded.snapshot.metadata.name, cwd, sessionId, state: "interrupted", parentRunId: loaded.run.id, retry, agents: [], agentSessions: [], ...(budget ? { budget } : {}), budgetVersion: loaded.run.budgetVersion ?? 1, ...childInitialBudget }, childSnapshot);
2403
2626
  const fallbackModel = { provider: hostModel.provider, model: hostModel.id, thinking: pi.getThinkingLevel() };
2404
2627
  const model = modelSpec(loaded.snapshot.models[0] ?? "", fallbackModel);
2405
2628
  const lifecycle = lifecycleFor(childStore, "interrupted", childBudget, loaded.snapshot.metadata);
@@ -2410,12 +2633,19 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2410
2633
  runs.set(childRunId, childRun);
2411
2634
  scheduler.addRun(childRunId, loaded.snapshot.settings.concurrency, () => { childBudget.checkAgentLaunch(); });
2412
2635
  await eventPublisher.runStarted(childStore, loaded.snapshot.metadata);
2413
- await coldResumeRun(childRun, false, {}, trustedProject, { model: hostModel, modelRegistry, resolvedAliases: currentAliases, blockedAliases, blockedAliasTargets, ...(signal ? { signal } : {}) });
2636
+ const { hasUI, ui } = recoveryUi(context);
2637
+ const completed = await coldResumeRun(childRun, hasUI, ui, trustedProject, { model: hostModel, modelRegistry, resolvedAliases: currentAliases, blockedAliases, blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
2414
2638
  const completion = runs.get(childRunId)?.completion;
2415
2639
  if (completion) {
2416
2640
  childStarted = true;
2417
2641
  void completion.then(() => { retryReservations.delete(lineageRootRunId); }, () => { retryReservations.delete(lineageRootRunId); });
2418
2642
  }
2643
+ else if (completed) {
2644
+ childStarted = true;
2645
+ retryReservations.delete(lineageRootRunId);
2646
+ }
2647
+ if (completed)
2648
+ return { runId: childRunId, parentRunId: loaded.run.id, state: "completed", value: completed.value, run: (await childStore.load()).run };
2419
2649
  return { runId: childRunId, parentRunId: loaded.run.id, state: "running" };
2420
2650
  }
2421
2651
  finally {
@@ -2430,7 +2660,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2430
2660
  parameters: WORKFLOW_RETRY_PARAMETERS,
2431
2661
  async execute(_id, params, signal, _onUpdate, ctx) {
2432
2662
  try {
2433
- const result = await retryWorkflowRun(params.runId, ctx, signal);
2663
+ const result = await retryWorkflowRun(params.runId, ctx, signal, params.foreground);
2434
2664
  return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
2435
2665
  }
2436
2666
  catch (error) {
@@ -2444,10 +2674,10 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2444
2674
  name: "workflow_resume",
2445
2675
  label: "Workflow Resume",
2446
2676
  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 }),
2677
+ 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
2678
  async execute(_id, params, signal, _onUpdate, ctx) {
2449
2679
  try {
2450
- const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal);
2680
+ const result = await resumeWorkflowRun(params.runId, params.budget, ctx, signal, params.foreground);
2451
2681
  return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
2452
2682
  }
2453
2683
  catch (error) {
@@ -2484,11 +2714,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2484
2714
  }
2485
2715
  if (loaded.run.state !== "interrupted" && loaded.run.state !== "budget_exhausted") {
2486
2716
  const previousState = loaded.run.state;
2487
- await store.updateState((current) => ["completed", "failed", "stopped", "interrupted", "budget_exhausted"].includes(current.state) ? current : { ...current, state: "interrupted" });
2717
+ await store.updateState((current) => {
2718
+ if (["completed", "failed", "stopped", "interrupted", "budget_exhausted"].includes(current.state))
2719
+ return current;
2720
+ const next = { ...current, state: "interrupted" };
2721
+ delete next.activeShells;
2722
+ return next;
2723
+ });
2488
2724
  loaded = { ...loaded, run: (await store.load()).run };
2489
2725
  await eventPublisher.runState(store, loaded.snapshot.metadata, previousState, "interrupted", "session_shutdown");
2490
2726
  loaded = { ...loaded, run: (await store.load()).run };
2491
2727
  }
2728
+ else if (loaded.run.activeShells !== undefined) {
2729
+ await store.updateState((current) => {
2730
+ if (["completed", "failed", "stopped"].includes(current.state))
2731
+ return current;
2732
+ const next = { ...current };
2733
+ delete next.activeShells;
2734
+ return next;
2735
+ });
2736
+ loaded = { ...loaded, run: (await store.load()).run };
2737
+ }
2492
2738
  const model = modelSpec(loaded.snapshot.models[0] ?? "", { provider: ctx.model?.provider ?? "", model: ctx.model?.id ?? "", thinking: pi.getThinkingLevel() });
2493
2739
  const budget = validateBudget(loaded.run.budget ?? loaded.snapshot.budget);
2494
2740
  eventPublisher.seedBudget(runId, loaded.run.budgetEvents);
@@ -2516,7 +2762,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2516
2762
  const toResume = choice === "Resume all" ? interrupted : interrupted.filter((_, i) => labels[i] === choice);
2517
2763
  for (const run of toResume) {
2518
2764
  try {
2519
- await coldResumeRun(run, true, ctx.ui, projectTrusted(ctx), ctx);
2765
+ await coldResumeRun(run, true, ctx.ui, projectTrusted(ctx), ctx, undefined, false);
2520
2766
  ctx.ui.notify(`Resumed workflow ${run.metadata.name}.`, "info");
2521
2767
  }
2522
2768
  catch (err) {
@@ -2588,10 +2834,27 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2588
2834
  const projectRoles = roleNames.filter((role) => projectAgentDefinitions[role] !== undefined);
2589
2835
  const roleModels = roleNames.flatMap((role) => { const model = agentDefinitions[role]?.model; return model ? [modelCapability(model, modelAliases, knownModels, settingsPath)] : []; });
2590
2836
  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 });
2837
+ 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 });
2838
+ let persistedSnapshot = snapshot;
2839
+ const captureFunctionRole = functionName ? async (role, model) => {
2840
+ const definition = agentDefinitions[role];
2841
+ if (!definition)
2842
+ return;
2843
+ const modelName = `${model.provider}/${model.model}`;
2844
+ const hasProjectRole = projectAgentDefinitions[role] !== undefined;
2845
+ if (persistedSnapshot.roles?.[role] !== undefined && (!hasProjectRole || persistedSnapshot.projectRoles?.includes(role)) && persistedSnapshot.models.includes(modelName))
2846
+ return;
2847
+ const roles = { ...(persistedSnapshot.roles ?? {}), [role]: definition };
2848
+ const projectRoles = hasProjectRole ? [...new Set([...(persistedSnapshot.projectRoles ?? []), role])] : persistedSnapshot.projectRoles ?? [];
2849
+ const models = [...new Set([...persistedSnapshot.models, modelName])];
2850
+ persistedSnapshot = createLaunchSnapshot({ ...persistedSnapshot, models, roles, projectRoles });
2851
+ await store.saveSnapshot(persistedSnapshot);
2852
+ } : undefined;
2592
2853
  const budgetRuntime = new WorkflowBudgetRuntime(budget);
2593
2854
  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);
2855
+ await store.create({ id: runId, workflowName: checked.metadata.name, cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId(), state: "running", ...(parentRunId !== undefined ? { parentRunId } : {}), agents: [], agentSessions: [], delivery: params.foreground ? { mode: "foreground", state: "attached", toolCallId } : { mode: "background", state: "pending" }, ...(budget ? { budget } : {}), budgetVersion: 1, ...initialBudget }, snapshot);
2856
+ if (params.foreground)
2857
+ foregroundDeliveries.set(toolCallId, { store, inline: false });
2595
2858
  const lifecycle = lifecycleFor(store, "running", budgetRuntime, checked.metadata);
2596
2859
  const background = !params.foreground;
2597
2860
  const providerPause = async () => { if (background)
@@ -2602,7 +2865,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2602
2865
  if (params.foreground && onUpdate)
2603
2866
  onUpdate(workflowToolUpdate((await store.load()).run));
2604
2867
  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);
2868
+ 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
2869
  runs.get(runId).execution = execution;
2607
2870
  await eventPublisher.runStarted(store, checked.metadata);
2608
2871
  const finish = execution.result.then(async (value) => {
@@ -2618,7 +2881,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2618
2881
  const typed = error instanceof WorkflowError ? error : new WorkflowError("INTERNAL_ERROR", String(error));
2619
2882
  if (!["stopped", "interrupted", "budget_exhausted"].includes(lifecycle.state))
2620
2883
  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 } }));
2884
+ const persisted = await persistRunState(store, checked.metadata, (current) => persistedFailure({ ...current, ...budgetRuntime.snapshot() }, typed));
2622
2885
  const state = lifecycle.state === "stopped" || lifecycle.state === "interrupted" || lifecycle.state === "budget_exhausted" ? lifecycle.state : "failed";
2623
2886
  await eventPublisher.runFailed(store, checked.metadata, typed, state);
2624
2887
  const diagnostic = await createWorkflowFailureDiagnostics(store, checked.metadata, typed, persisted);
@@ -2629,18 +2892,41 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2629
2892
  });
2630
2893
  const completion = finish.finally(() => cleanupTerminalRun(runId));
2631
2894
  runs.get(runId).completion = completion;
2895
+ const deliverFailureContent = (error) => {
2896
+ const diagnostic = failureDiagnosticsFrom(error);
2897
+ return diagnostic ? formatWorkflowFailureDelivery(diagnostic) : formatWorkflowFailureDeliveryFallback(checked.metadata.name, runId, store.directory, error);
2898
+ };
2899
+ const queueForegroundDelivery = async (content) => {
2900
+ const delivery = foregroundDeliveries.get(toolCallId);
2901
+ if (!delivery)
2902
+ return;
2903
+ await store.updateState((current) => {
2904
+ if (!current.delivery || current.delivery.state === "delivered")
2905
+ return current;
2906
+ return { ...current, delivery: { ...current.delivery, mode: "background", state: "pending" } };
2907
+ });
2908
+ if (delivery.inline)
2909
+ return;
2910
+ scheduleForegroundDelivery(toolCallId, async () => {
2911
+ if (delivery.inline)
2912
+ return;
2913
+ pendingFailureDiagnostics.delete(toolCallId);
2914
+ await deliverTerminal(store, content);
2915
+ });
2916
+ };
2632
2917
  if (background) {
2633
2918
  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)}`);
2919
+ await deliverTerminal(store, completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
2920
+ }, async (error) => {
2921
+ await deliverTerminal(store, deliverFailureContent(error));
2641
2922
  });
2642
2923
  return { content: [{ type: "text", text: JSON.stringify({ runId, state: "running" }) }], details: { runId, preview: `Started workflow ${runId}.` } };
2643
2924
  }
2925
+ void completion.then(async ({ value, resultPath }) => {
2926
+ await queueForegroundDelivery(completionDelivery(checked.metadata.name, value, resultPath, await store.changedWorktrees()));
2927
+ }, async (error) => {
2928
+ await queueForegroundDelivery(deliverFailureContent(error));
2929
+ });
2644
2930
  const { value } = await completion;
2645
2931
  const run = (await store.load()).run;
2646
2932
  return { content: [{ type: "text", text: JSON.stringify(value) }, { type: "text", text: `Workflow run ID: ${runId}` }], details: { runId, value, run } };
@@ -2666,8 +2952,33 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2666
2952
  clearInterval(state.workflowSpinner);
2667
2953
  delete state.workflowSpinner;
2668
2954
  }
2669
- if (runDetails?.run)
2670
- return workflowProgressBlock(runDetails.run, theme);
2955
+ if (runDetails?.run) {
2956
+ const incoming = runDetails.run;
2957
+ let progress = state.workflowProgress;
2958
+ if (!isPartial || !progress || progress.runId !== incoming.id) {
2959
+ progress = undefined;
2960
+ delete state.workflowProgress;
2961
+ if (isPartial) {
2962
+ progress = { runId: incoming.id, inputRun: incoming, run: incoming, lastRefreshAt: 0, runtimeStartedAt: Date.now(), runtimeBaseMs: incoming.usage?.durationMs ?? 0 };
2963
+ state.workflowProgress = progress;
2964
+ }
2965
+ }
2966
+ else if (progress.inputRun !== incoming) {
2967
+ if (progress.run.state !== "running" && incoming.state === "running") {
2968
+ progress.runtimeBaseMs = incoming.usage?.durationMs ?? 0;
2969
+ progress.runtimeStartedAt = Date.now();
2970
+ }
2971
+ progress.inputRun = incoming;
2972
+ progress.run = incoming;
2973
+ }
2974
+ return workflowProgressBlock(progress?.run ?? incoming, theme, progress, async () => {
2975
+ const active = runs.get(incoming.id);
2976
+ const store = active?.store ?? new RunStore(incoming.cwd, incoming.sessionId, incoming.id, home);
2977
+ const loaded = await store.load();
2978
+ return withLiveActivities(loaded.run);
2979
+ }, () => { if (state.workflowProgress === progress)
2980
+ context.invalidate(); });
2981
+ }
2671
2982
  const content = result.content[0];
2672
2983
  return textBlock(isPartial ? "Workflow starting..." : runDetails?.preview ?? (content?.type === "text" ? content.text : "Workflow finished"));
2673
2984
  },
@@ -2687,7 +2998,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2687
2998
  const entries = await Promise.all((await listRunIds(ctx.cwd, ctx.sessionManager.getSessionId(), home)).map(async (runId) => {
2688
2999
  const store = new RunStore(ctx.cwd, ctx.sessionManager.getSessionId(), runId, home);
2689
3000
  try {
2690
- return { store, loaded: await store.load() };
3001
+ const loaded = await store.load();
3002
+ return { store, loaded: { ...loaded, run: withLiveActivities(loaded.run) } };
2691
3003
  }
2692
3004
  catch {
2693
3005
  if (!await store.isComplete())
@@ -2715,7 +3027,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2715
3027
  return keepContext ? "dashboard" : "done";
2716
3028
  }
2717
3029
  if ((action === "budget-approve" || action === "budget-reject") && runId && rest[0]) {
2718
- const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx);
3030
+ const result = await answerBudgetDecision(runId, rest[0], action === "budget-approve", true, ctx, undefined, false);
2719
3031
  ctx.ui.notify(result ? `Budget adjustment ${rest[0]} ${result.approved ? "approved" : "rejected"}.` : "Budget proposal is not pending.", result ? "info" : "warning");
2720
3032
  return keepContext ? "dashboard" : "done";
2721
3033
  }
@@ -2740,12 +3052,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2740
3052
  if (action === "resume" && run) {
2741
3053
  if (run.lifecycle.state === "budget_exhausted") {
2742
3054
  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");
3055
+ const result = await resumeWorkflowRun(run.store.runId, patch, ctx, undefined, undefined, false);
3056
+ 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
3057
  }
2746
3058
  else {
2747
3059
  if (run.lifecycle.state === "interrupted")
2748
- await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx);
3060
+ await coldResumeRun(run, ctx.hasUI, ctx.ui, projectTrusted(ctx), ctx, undefined, false);
2749
3061
  else {
2750
3062
  if (run.lifecycle.state === "paused")
2751
3063
  await refreshPausedRunAliases(run, { ...resumeHostContext(ctx), projectTrusted: projectTrusted(ctx) });
@@ -2759,8 +3071,8 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2759
3071
  const input = await uiHostCapabilities(ctx.ui)?.input?.call(ctx.ui, "Budget patch (JSON)", "{\"tokens\":{\"hard\":null}}");
2760
3072
  if (input === undefined)
2761
3073
  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");
3074
+ const result = await resumeWorkflowRun(run.store.runId, JSON.parse(input), ctx, undefined, undefined, false);
3075
+ 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
3076
  return keepContext ? "dashboard" : "done";
2765
3077
  }
2766
3078
  if (action === "stop" && run) {
@@ -2981,11 +3293,12 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2981
3293
  };
2982
3294
  const loadDashboard = async () => {
2983
3295
  const loaded = await store.load();
3296
+ const liveRun = withLiveActivities(loaded.run);
2984
3297
  const checkpoints = await store.awaitingCheckpoints();
2985
3298
  const worktrees = await store.worktrees();
2986
3299
  const completedOperations = ctx.mode === "tui" ? await store.replayableOperations().catch(() => []) : [];
2987
3300
  const agentResults = new Map();
2988
- for (const agent of loaded.run.agents) {
3301
+ for (const agent of liveRun.agents) {
2989
3302
  if (agent.state !== "completed" || agent.parentId || !agent.resultPath)
2990
3303
  continue;
2991
3304
  const operation = completedOperations.find((candidate) => candidate.path === agent.resultPath);
@@ -2997,11 +3310,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
2997
3310
  const reviews = new Map();
2998
3311
  const add = (label, value) => { actions.set(label, `${value} ${store.runId}`); };
2999
3312
  const addCopy = (label, value, artifact) => { actions.set(label, "copy"); copies.set(label, { value, artifact }); };
3000
- if (loaded.run.state === "running")
3313
+ if (liveRun.state === "running")
3001
3314
  add("Pause", "pause");
3002
- if (["paused", "interrupted"].includes(loaded.run.state))
3315
+ if (["paused", "interrupted"].includes(liveRun.state))
3003
3316
  add("Resume", "resume");
3004
- if (loaded.run.state === "budget_exhausted") {
3317
+ if (liveRun.state === "budget_exhausted") {
3005
3318
  actions.set("Resume unchanged", `resume ${store.runId}`);
3006
3319
  actions.set("Adjust budget", `adjust ${store.runId}`);
3007
3320
  }
@@ -3010,7 +3323,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3010
3323
  actions.set(`Approve budget ${id}`, `budget-approve ${store.runId} ${decision.proposalId}`);
3011
3324
  actions.set(`Reject budget ${id}`, `budget-reject ${store.runId} ${decision.proposalId}`);
3012
3325
  }
3013
- if (!terminalStates.has(loaded.run.state))
3326
+ if (!terminalStates.has(liveRun.state))
3014
3327
  add("Stop", "stop");
3015
3328
  for (const cp of checkpoints) {
3016
3329
  if (ctx.mode === "tui") {
@@ -3027,47 +3340,50 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3027
3340
  actions.set("Refresh", "refresh");
3028
3341
  else
3029
3342
  actions.set("Open script in editor", "open-script");
3030
- if (ctx.mode !== "tui" && loaded.run.agents.length)
3343
+ if (ctx.mode !== "tui" && liveRun.agents.length)
3031
3344
  actions.set("Agents...", "agents");
3032
- if (terminalStates.has(loaded.run.state))
3345
+ if (terminalStates.has(liveRun.state))
3033
3346
  add("Delete", "delete");
3034
3347
  if (ctx.mode === "tui") {
3035
3348
  addCopy("Copy run path", store.directory, "run path");
3036
3349
  addCopy("Copy run ID", store.runId, "run ID");
3037
3350
  }
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 };
3351
+ 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
3352
  };
3040
3353
  const agentWorktreeFor = (dashboard, agent) => agent.worktreeOwner ? dashboard.worktrees.find((candidate) => candidate.owner === agent.worktreeOwner) : undefined;
3354
+ const agentAttemptActionContext = (dashboard, agent) => {
3355
+ const attempt = (agent.attemptDetails ?? []).reduce((latest, candidate) => !latest || candidate.attempt > latest.attempt ? candidate : latest, undefined);
3356
+ if (!attempt)
3357
+ return undefined;
3358
+ const liveCandidate = liveAgentSessions.get(`${dashboard.run.id}:${agent.id}`);
3359
+ const live = liveCandidate && attempt.session && liveCandidate.reference.transport === attempt.session.transport && liveCandidate.reference.sessionId === attempt.session.sessionId ? liveCandidate : undefined;
3360
+ const run = runs.get(dashboard.run.id);
3361
+ const ui = { notify: (message, level = "info") => { ctx.ui.notify(message, level); }, confirm: (title, message) => ctx.ui.confirm(title, message), select: (title, options) => { return ctx.ui.select(title, [...options]); }, input: (title, placeholder) => ctx.ui.input(title, placeholder) };
3362
+ const attemptSnapshot = deepFreeze(structuredClone(attempt));
3363
+ return { run: deepFreeze(structuredClone(dashboard.run)), agent: deepFreeze(structuredClone(agent)), attempt: attemptSnapshot, ...(attemptSnapshot.session ? { session: attemptSnapshot.session } : {}), ...(live ? { liveSession: live } : {}), signal: run?.abortController.signal ?? new AbortController().signal, ui: Object.freeze(ui) };
3364
+ };
3365
+ const visibleAgentAttemptActions = (dashboard, agent) => {
3366
+ const context = agentAttemptActionContext(dashboard, agent);
3367
+ if (!context)
3368
+ return [];
3369
+ return Object.entries(registry.agentAttemptActions()).filter(([, action]) => { try {
3370
+ return action.visible(context);
3371
+ }
3372
+ catch {
3373
+ return false;
3374
+ } });
3375
+ };
3041
3376
  const agentActionLabels = (dashboard, agent) => {
3042
3377
  const worktree = agentWorktreeFor(dashboard, agent);
3043
3378
  return [
3044
- ...(agent.attemptDetails?.length && herdrPaneId() ? ["Fork as Pi session in pane"] : []),
3379
+ ...visibleAgentAttemptActions(dashboard, agent).map(([, action]) => action.label),
3045
3380
  ...(worktree ? ["Copy branch", "Copy worktree path"] : []),
3381
+ ...(ctx.mode === "tui" && agent.prompt !== undefined ? ["Open prompt in editor"] : []),
3046
3382
  ...(ctx.mode === "tui" && dashboard.agentResults.has(agent.id) ? ["Open result in editor"] : []),
3047
3383
  "Copy agent ID",
3048
3384
  "Back",
3049
3385
  ];
3050
3386
  };
3051
- const forkAgentSession = async (dashboard, agent) => {
3052
- const attempts = [...(agent.attemptDetails ?? [])].sort((left, right) => left.attempt - right.attempt);
3053
- const worktree = agentWorktreeFor(dashboard, agent);
3054
- const choices = attempts.map((attempt) => `Attempt ${String(attempt.attempt)}`);
3055
- const choice = choices.length === 1 ? choices[0] : await ctx.ui.select("Fork attempts", [...choices, "Back"]);
3056
- const index = choice ? choices.indexOf(choice) : -1;
3057
- const attempt = index >= 0 ? attempts[index] : undefined;
3058
- if (!attempt)
3059
- return;
3060
- const running = !SETTLED_AGENT_STATES.has(agent.state) && attempt.attempt === attempts.at(-1)?.attempt && !attempt.error;
3061
- if (running && !await ctx.ui.confirm("Fork running attempt?", "This attempt is still running. The snapshot may end mid-turn and will not receive later updates. It opens read-only to avoid concurrent changes to the workflow agent's working directory. Continue?"))
3062
- return;
3063
- try {
3064
- await openHerdrPane({ action: "fork", cwd: worktree?.cwd ?? attempt.setup?.cwd ?? dashboard.cwd, original: attempt.sessionFile, ...(running ? { readOnly: true } : {}) });
3065
- ctx.ui.notify("Forked Pi session in pane.", "info");
3066
- }
3067
- catch (error) {
3068
- ctx.ui.notify(`Cannot fork Pi session in pane: ${error instanceof Error ? error.message : String(error)}`, "warning");
3069
- }
3070
- };
3071
3387
  const selectAgent = async (dashboard, requestedAgentId) => {
3072
3388
  const byId = new Map(dashboard.agents.map((agent) => [agent.id, agent]));
3073
3389
  const title = (agent) => agentBreadcrumb(agent, byId, true);
@@ -3088,6 +3404,19 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3088
3404
  const action = await ctx.ui.select(title(selected), actions);
3089
3405
  if (!action || action === "Back")
3090
3406
  return;
3407
+ const extensionAction = visibleAgentAttemptActions(dashboard, selected).find(([, candidate]) => candidate.label === action);
3408
+ if (extensionAction) {
3409
+ const context = agentAttemptActionContext(dashboard, selected);
3410
+ if (context) {
3411
+ try {
3412
+ await extensionAction[1].run(context);
3413
+ }
3414
+ catch (error) {
3415
+ ctx.ui.notify(`Agent attempt action failed: ${error instanceof Error ? error.message : String(error)}`, "error");
3416
+ }
3417
+ }
3418
+ return;
3419
+ }
3091
3420
  if (action === "Copy agent ID") {
3092
3421
  await copyArtifact(selected.id, "agent ID");
3093
3422
  continue;
@@ -3100,8 +3429,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3100
3429
  await copyArtifact(worktree.path, "worktree path");
3101
3430
  continue;
3102
3431
  }
3103
- if (action === "Fork as Pi session in pane")
3104
- await forkAgentSession(dashboard, selected);
3105
3432
  }
3106
3433
  };
3107
3434
  for (;;) {
@@ -3123,12 +3450,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3123
3450
  let tree = buildWorkflowPhaseTree(view.phaseModel);
3124
3451
  let selectedNodeId = initialSelection.nodeId ?? tree.nodes[0]?.id;
3125
3452
  let expandedNodeIds = new Set(initialSelection.expandedNodeIds ?? workflowPhaseTreeInitialExpanded(tree));
3126
- const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_OVERLAY_BORDER_ROWS);
3453
+ const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_PANEL_FOOTER_ROWS);
3127
3454
  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
- };
3455
+ const keyLabel = (binding, fallback) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
3132
3456
  const selectedAgentRecord = () => {
3133
3457
  const node = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
3134
3458
  return node?.kind === "agent" && node.agentId ? view.agents.find((agent) => agent.id === node.agentId) : undefined;
@@ -3159,7 +3483,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3159
3483
  }
3160
3484
  finally {
3161
3485
  editorRunning = false;
3162
- tui.requestRender(true);
3163
3486
  }
3164
3487
  };
3165
3488
  const updateDashboard = async () => {
@@ -3213,7 +3536,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3213
3536
  void updateDashboard().catch(() => undefined).finally(() => { refreshing = false; });
3214
3537
  }, 1000);
3215
3538
  timer.unref();
3216
- return borderWorkflowOverlay({
3539
+ return {
3217
3540
  render(width) {
3218
3541
  renderedWidth = width;
3219
3542
  const narrow = width < 80;
@@ -3249,7 +3572,7 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3249
3572
  selectionNeedsScroll = false;
3250
3573
  }
3251
3574
  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] ?? "";
3575
+ const hint = truncateToVisualLines(theme.fg("dim", actionMode ? `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.select.confirm", "enter")} run · ${keyLabel("tui.editor.cursorLeft", "←")} tree · ${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
3576
  return [...content.slice(dashboardOffset, dashboardOffset + viewport), ...(hintRows ? [hint] : [])];
3254
3577
  },
3255
3578
  invalidate() { },
@@ -3266,21 +3589,21 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3266
3589
  }
3267
3590
  if (actionMode) {
3268
3591
  const options = actionOptions();
3269
- if (keybindings.matches(data, "tui.select.cancel")) {
3592
+ if (workflowKeyMatches(keybindings, data, "tui.select.cancel") || workflowKeyMatches(keybindings, data, "tui.editor.cursorLeft")) {
3270
3593
  actionMode = false;
3271
3594
  dashboardOffset = 0;
3272
3595
  tui.requestRender();
3273
3596
  return;
3274
3597
  }
3275
- if (keybindings.matches(data, "tui.select.up"))
3598
+ if (workflowKeyMatches(keybindings, data, "tui.select.up"))
3276
3599
  actionIndex = (actionIndex + options.length - 1) % options.length;
3277
- else if (keybindings.matches(data, "tui.select.down"))
3600
+ else if (workflowKeyMatches(keybindings, data, "tui.select.down"))
3278
3601
  actionIndex = (actionIndex + 1) % options.length;
3279
- else if (keybindings.matches(data, "tui.select.pageUp"))
3602
+ else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
3280
3603
  dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
3281
- else if (keybindings.matches(data, "tui.select.pageDown"))
3604
+ else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
3282
3605
  dashboardOffset += Math.max(1, terminalRows() - 1);
3283
- else if (keybindings.matches(data, "tui.select.confirm")) {
3606
+ else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
3284
3607
  const action = options[actionIndex];
3285
3608
  const agent = selectedAgentRecord();
3286
3609
  if (!action || action === "Back") {
@@ -3289,7 +3612,11 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3289
3612
  }
3290
3613
  else if (agent) {
3291
3614
  const worktree = agentWorktreeFor(view, agent);
3292
- if (action === "Open result in editor") {
3615
+ if (action === "Open prompt in editor") {
3616
+ if (agent.prompt !== undefined)
3617
+ void openArtifact(Promise.resolve(workflowPromptArtifact(agent.prompt)), "agent prompt");
3618
+ }
3619
+ else if (action === "Open result in editor") {
3293
3620
  const result = view.agentResults.get(agent.id);
3294
3621
  if (result !== undefined)
3295
3622
  void openArtifact(Promise.resolve(workflowResultArtifact(result)), "agent result");
@@ -3300,8 +3627,14 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3300
3627
  void copyArtifact(worktree.branch, "branch");
3301
3628
  else if (action === "Copy worktree path" && worktree)
3302
3629
  void copyArtifact(worktree.path, "worktree path");
3303
- else
3304
- done(`__workflow_fork__:${agent.id}`);
3630
+ else {
3631
+ const extensionAction = visibleAgentAttemptActions(view, agent).find(([, candidate]) => candidate.label === action);
3632
+ const actionContext = extensionAction ? agentAttemptActionContext(view, agent) : undefined;
3633
+ if (extensionAction && actionContext) {
3634
+ actionMode = false;
3635
+ void Promise.resolve(extensionAction[1].run(actionContext)).catch((error) => { ctx.ui.notify(`Agent attempt action failed: ${error instanceof Error ? error.message : String(error)}`, "error"); }).finally(() => { void updateDashboard(); });
3636
+ }
3637
+ }
3305
3638
  }
3306
3639
  else if (action === "Open script in editor")
3307
3640
  void openArtifact(readFile(join(store.directory, "workflow.js"), "utf8").then(workflowScriptArtifact), "workflow script");
@@ -3314,20 +3647,20 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3314
3647
  return;
3315
3648
  }
3316
3649
  const current = selectedNodeId ? tree.byId.get(selectedNodeId) : tree.nodes[0];
3317
- if (keybindings.matches(data, "tui.select.cancel")) {
3650
+ if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) {
3318
3651
  if (narrow && detailsMode) {
3319
3652
  detailsMode = false;
3320
3653
  selectionNeedsScroll = true;
3321
3654
  }
3322
3655
  else
3323
- done(undefined);
3656
+ done("Back");
3324
3657
  }
3325
3658
  else if (narrow && detailsMode) {
3326
- if (keybindings.matches(data, "tui.select.pageUp"))
3659
+ if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
3327
3660
  dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
3328
- else if (keybindings.matches(data, "tui.select.pageDown"))
3661
+ else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
3329
3662
  dashboardOffset += Math.max(1, terminalRows() - 1);
3330
- else if (keybindings.matches(data, "tui.select.confirm")) {
3663
+ else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
3331
3664
  if (current?.kind === "agent" && current.agentId) {
3332
3665
  actionMode = true;
3333
3666
  actionIndex = 0;
@@ -3340,33 +3673,33 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3340
3673
  }
3341
3674
  }
3342
3675
  }
3343
- else if (keybindings.matches(data, "tui.editor.cursorLeft")) {
3676
+ else if (workflowKeyMatches(keybindings, data, "tui.editor.cursorLeft")) {
3344
3677
  const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "left");
3345
3678
  selectedNodeId = next.nodeId;
3346
3679
  expandedNodeIds = new Set(next.expandedNodeIds);
3347
3680
  selectionNeedsScroll = true;
3348
3681
  }
3349
- else if (keybindings.matches(data, "tui.editor.cursorRight")) {
3682
+ else if (workflowKeyMatches(keybindings, data, "tui.editor.cursorRight")) {
3350
3683
  const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "right");
3351
3684
  selectedNodeId = next.nodeId;
3352
3685
  expandedNodeIds = new Set(next.expandedNodeIds);
3353
3686
  selectionNeedsScroll = true;
3354
3687
  }
3355
- else if (keybindings.matches(data, "tui.select.up")) {
3688
+ else if (workflowKeyMatches(keybindings, data, "tui.select.up")) {
3356
3689
  const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "up");
3357
3690
  selectedNodeId = next.nodeId;
3358
3691
  selectionNeedsScroll = true;
3359
3692
  }
3360
- else if (keybindings.matches(data, "tui.select.down")) {
3693
+ else if (workflowKeyMatches(keybindings, data, "tui.select.down")) {
3361
3694
  const next = navigateWorkflowPhaseTree(tree, selectedNodeId, expandedNodeIds, "down");
3362
3695
  selectedNodeId = next.nodeId;
3363
3696
  selectionNeedsScroll = true;
3364
3697
  }
3365
- else if (keybindings.matches(data, "tui.select.pageUp"))
3698
+ else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
3366
3699
  dashboardOffset = Math.max(0, dashboardOffset - Math.max(1, terminalRows() - 1));
3367
- else if (keybindings.matches(data, "tui.select.pageDown"))
3700
+ else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
3368
3701
  dashboardOffset += Math.max(1, terminalRows() - 1);
3369
- else if (keybindings.matches(data, "tui.select.confirm")) {
3702
+ else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) {
3370
3703
  if (narrow)
3371
3704
  detailsMode = true;
3372
3705
  else if (current?.kind === "agent" && current.agentId) {
@@ -3383,11 +3716,13 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3383
3716
  tui.requestRender();
3384
3717
  },
3385
3718
  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;
3719
+ };
3720
+ })
3721
+ : await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Back"]);
3722
+ if (!actionChoice || actionChoice === "Back") {
3723
+ stores = await loadStores();
3724
+ break;
3725
+ }
3391
3726
  if (actionChoice === "Agents...") {
3392
3727
  await selectAgent(view);
3393
3728
  continue;
@@ -3396,13 +3731,6 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3396
3731
  await selectAgent(view, actionChoice.slice("__workflow_agent__:".length));
3397
3732
  continue;
3398
3733
  }
3399
- if (actionChoice.startsWith("__workflow_fork__:")) {
3400
- const agentId = actionChoice.slice("__workflow_fork__:".length);
3401
- const agent = view.agents.find((candidate) => candidate.id === agentId);
3402
- if (agent)
3403
- await forkAgentSession(view, agent);
3404
- continue;
3405
- }
3406
3734
  if (actionChoice === "Refresh")
3407
3735
  continue;
3408
3736
  const copy = view.copies.get(actionChoice);
@@ -3439,7 +3767,9 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3439
3767
  const currentLayout = layout();
3440
3768
  const maxOffset = Math.max(0, renderedLines.length - currentLayout.contentViewport);
3441
3769
  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] ?? "";
3770
+ const keyLabels = { up: "", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
3771
+ const keyLabel = (binding, fallback) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
3772
+ 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
3773
  const controls = currentLayout.compactControls
3444
3774
  ? [options.map((option, index) => `${index === selectedIndex ? "[" : " "}${option}${index === selectedIndex ? "]" : " "}`).join(" ")]
3445
3775
  : options.map((option, index) => `${index === selectedIndex ? "→ " : " "}${option}`);
@@ -3453,17 +3783,17 @@ export default function workflowExtension(pi, home, clipboard = copyToClipboard,
3453
3783
  },
3454
3784
  invalidate() { },
3455
3785
  handleInput(data) {
3456
- if (keybindings.matches(data, "tui.select.up"))
3786
+ if (workflowKeyMatches(keybindings, data, "tui.select.up"))
3457
3787
  selectedIndex = (selectedIndex + options.length - 1) % options.length;
3458
- else if (keybindings.matches(data, "tui.select.down"))
3788
+ else if (workflowKeyMatches(keybindings, data, "tui.select.down"))
3459
3789
  selectedIndex = (selectedIndex + 1) % options.length;
3460
- else if (keybindings.matches(data, "tui.select.pageUp"))
3790
+ else if (workflowKeyMatches(keybindings, data, "tui.select.pageUp"))
3461
3791
  move(-layout().contentViewport);
3462
- else if (keybindings.matches(data, "tui.select.pageDown"))
3792
+ else if (workflowKeyMatches(keybindings, data, "tui.select.pageDown"))
3463
3793
  move(layout().contentViewport);
3464
- else if (keybindings.matches(data, "tui.select.confirm"))
3794
+ else if (workflowKeyMatches(keybindings, data, "tui.select.confirm"))
3465
3795
  done(options[selectedIndex] === "Cancel" ? undefined : options[selectedIndex]);
3466
- else if (keybindings.matches(data, "tui.select.cancel"))
3796
+ else if (workflowKeyMatches(keybindings, data, "tui.select.cancel"))
3467
3797
  done(undefined);
3468
3798
  tui.requestRender();
3469
3799
  },