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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/dist/src/agent-execution.js +114 -87
  3. package/dist/src/background-widget.d.ts +61 -0
  4. package/dist/src/background-widget.js +23 -16
  5. package/dist/src/decoders.js +28 -1
  6. package/dist/src/execution.d.ts +4 -2
  7. package/dist/src/execution.js +43 -2
  8. package/dist/src/host-delivery.js +1 -0
  9. package/dist/src/host-navigator.d.ts +18 -0
  10. package/dist/src/host-navigator.js +71 -13
  11. package/dist/src/host-recovery.d.ts +5 -2
  12. package/dist/src/host-recovery.js +3 -3
  13. package/dist/src/host-view.d.ts +21 -1
  14. package/dist/src/host-view.js +138 -47
  15. package/dist/src/host.d.ts +2 -2
  16. package/dist/src/host.js +246 -60
  17. package/dist/src/index.d.ts +3 -1
  18. package/dist/src/index.js +1346 -622
  19. package/dist/src/index.js.map +3 -3
  20. package/dist/src/paths.d.ts +2 -0
  21. package/dist/src/paths.js +2 -0
  22. package/dist/src/registry.js +3 -2
  23. package/dist/src/roles.d.ts +70 -0
  24. package/dist/src/roles.js +326 -0
  25. package/dist/src/script-tools.d.ts +27 -0
  26. package/dist/src/script-tools.js +87 -0
  27. package/dist/src/settings.d.ts +26 -0
  28. package/dist/src/settings.js +277 -0
  29. package/dist/src/trajectory.d.ts +39 -4
  30. package/dist/src/trajectory.js +82 -5
  31. package/dist/src/types.d.ts +30 -3
  32. package/dist/src/types.js +1 -1
  33. package/dist/src/utils.d.ts +21 -1
  34. package/dist/src/utils.js +40 -0
  35. package/dist/src/validation.d.ts +10 -17
  36. package/dist/src/validation.js +29 -458
  37. package/dist/starter/index.js +0 -2
  38. package/dist/starter/index.js.map +1 -1
  39. package/dist/starter/review-loop.d.ts +0 -13
  40. package/dist/starter/review-loop.js +0 -2
  41. package/dist/subagents/index.js +1344 -798
  42. package/dist/subagents/index.js.map +3 -3
  43. package/dist/subagents/src/contracts.d.ts +49 -29
  44. package/dist/subagents/src/contracts.js +18 -7
  45. package/dist/subagents/src/index.d.ts +1 -1
  46. package/dist/subagents/src/index.js +15 -7
  47. package/dist/subagents/src/manager.js +32 -13
  48. package/dist/subagents/src/navigator.js +396 -247
  49. package/dist/subagents/src/view.d.ts +24 -2
  50. package/dist/subagents/src/view.js +107 -80
  51. package/dist/trajectory/assets/index.html +370 -232
  52. package/dist/trajectory/assets/prism.min.js +13 -0
  53. package/dist/trajectory/index.js +445 -277
  54. package/dist/trajectory/index.js.map +3 -3
  55. package/dist/trajectory/src/assets/index.html +370 -232
  56. package/dist/trajectory/src/assets/prism.min.js +13 -0
  57. package/dist/trajectory/src/export.js +4 -2
  58. package/dist/trajectory/src/index.d.ts +2 -0
  59. package/dist/trajectory/src/index.js +151 -60
  60. package/dist/trajectory/src/server.js +138 -11
  61. package/dist/trajectory/src/server.js.map +2 -2
  62. package/package.json +7 -6
  63. package/skills/pi-extensible-workflows/SKILL.md +2 -0
  64. package/src/agent-execution.ts +110 -80
  65. package/src/background-widget.ts +26 -19
  66. package/src/decoders.ts +22 -1
  67. package/src/execution.ts +41 -4
  68. package/src/host-delivery.ts +1 -0
  69. package/src/host-navigator.ts +58 -14
  70. package/src/host-recovery.ts +8 -7
  71. package/src/host-view.ts +116 -42
  72. package/src/host.ts +223 -55
  73. package/src/index.ts +3 -1
  74. package/src/paths.ts +3 -0
  75. package/src/registry.ts +3 -2
  76. package/src/roles.ts +327 -0
  77. package/src/script-tools.ts +76 -0
  78. package/src/settings.ts +224 -0
  79. package/src/trajectory.ts +97 -9
  80. package/src/types.ts +12 -5
  81. package/src/utils.ts +36 -1
  82. package/src/validation.ts +30 -356
  83. package/starter/review-loop.ts +0 -2
  84. package/subagents/README.md +3 -3
  85. package/subagents/src/contracts.ts +22 -7
  86. package/subagents/src/index.ts +18 -7
  87. package/subagents/src/manager.ts +31 -13
  88. package/subagents/src/navigator.ts +336 -180
  89. package/subagents/src/view.ts +108 -76
  90. package/trajectory/src/assets/index.html +370 -232
  91. package/trajectory/src/assets/prism.min.js +13 -0
  92. package/trajectory/src/export.ts +4 -2
  93. package/trajectory/src/index.ts +124 -47
  94. package/trajectory/src/server.ts +114 -14
@@ -25,7 +25,7 @@ const ENTRY_TYPE = "piewf-run-receipt";
25
25
  * Repaint interval. Fast enough that the spinner reads as motion rather than a
26
26
  * stutter; the underlying data is only re-read when an event says it changed.
27
27
  */
28
- const REPAINT_MS = 125;
28
+ export const REPAINT_MS = 125;
29
29
  /**
30
30
  * Filesystem rescan interval. Shell-only activity has no event, but it does not
31
31
  * need to be polled at the animation cadence.
@@ -51,7 +51,7 @@ const MAX_ROWS = 10;
51
51
  * of thinking and far shorter than the ten minutes it takes to be declared
52
52
  * stalled.
53
53
  */
54
- const QUIET_MS = 3 * 60 * 1000;
54
+ export const QUIET_MS = 3 * 60 * 1000;
55
55
  /** The core's own threshold for calling an agent stalled outright. */
56
56
  const STALL_MS = WORKFLOW_AGENT_STALL_THRESHOLD_MS;
57
57
  /**
@@ -130,7 +130,7 @@ function spinner(now) {
130
130
  return SPINNER[Math.floor(now / REPAINT_MS) % SPINNER.length] ?? SPINNER[0];
131
131
  }
132
132
  /** Status mark for a finished-or-running unit of work. */
133
- function mark(state, now, paint, runState) {
133
+ export function mark(state, now, paint, runState) {
134
134
  if (state === "running" && (runState === undefined || runState === "running"))
135
135
  return paint(ROLE.live, spinner(now));
136
136
  if (state === "completed")
@@ -139,7 +139,7 @@ function mark(state, now, paint, runState) {
139
139
  return paint(ROLE.failed, "✗");
140
140
  return paint(ROLE.quiet, "·");
141
141
  }
142
- function formatElapsed(ms) {
142
+ export function formatElapsed(ms) {
143
143
  const total = Math.max(0, Math.floor(ms / 1000));
144
144
  const minutes = Math.floor(total / 60);
145
145
  const seconds = total % 60;
@@ -576,13 +576,6 @@ function renderFrame(runs, now, width, offset = 0, theme, scrolling = false, siz
576
576
  const title = live.length === 1 ? "Workflow" : `Workflows · ${String(live.length)} runs`;
577
577
  sizeOut.rows = rows.length;
578
578
  const body = collapse(rows, MAX_ROWS - 2, paint, offset, scrolling);
579
- // Width budget: the rule takes a column and a space on each side, so the
580
- // text has four fewer than the widget is given. Every piece is measured
581
- // against `inner` so the right-hand rule lands in one column on every row.
582
- const inner = Math.max(20, width - 4);
583
- // Side rules below the lid, and no floor: the box opens downward into the
584
- // editor rather than closing on itself, which is one more line kept for the
585
- // run.
586
579
  // A scrollbar on the right rule, drawn only when there is more tree than
587
580
  // frame. Without it the window's position is a guess: three rows of a
588
581
  // twenty-row tree look the same near the top as near the bottom.
@@ -610,6 +603,25 @@ function renderFrame(runs, now, width, offset = 0, theme, scrolling = false, siz
610
603
  const top = limit === 0 ? 0 : start >= limit ? shown - size : Math.floor((start / limit) * (shown - size));
611
604
  return index >= top && index < top + size ? "█" : "░";
612
605
  };
606
+ // Only advertise navigation when it can reveal hidden rows: a hint for keys
607
+ // that do nothing is worse than no hint.
608
+ const hint = !__navigationForTests.enabled || maxOffset(total, MAX_ROWS - 2) === 0
609
+ ? ""
610
+ : scrolling ? "↑/↓ to scroll · Esc to exit" : "Alt+O to scroll";
611
+ return drawFrame(title, hint, body, width, paint, accent, scrollbar);
612
+ }
613
+ /**
614
+ * The box around the live rows, shared with the subagent widget so both sit
615
+ * under the editor in one style.
616
+ */
617
+ export function drawFrame(title, hint, body, width, paint, accent, scrollbar = () => undefined) {
618
+ // Width budget: the rule takes a column and a space on each side, so the
619
+ // text has four fewer than the widget is given. Every piece is measured
620
+ // against `inner` so the right-hand rule lands in one column on every row.
621
+ const inner = Math.max(20, width - 4);
622
+ // Side rules below the lid, and no floor: the box opens downward into the
623
+ // editor rather than closing on itself, which is one more line kept for the
624
+ // run.
613
625
  const lines = body.map((row, index) => {
614
626
  // The numbers hold the right edge; the name gives way when the two would
615
627
  // meet. A truncated workflow name is still recognisable, where a truncated
@@ -630,11 +642,6 @@ function renderFrame(runs, now, width, offset = 0, theme, scrolling = false, siz
630
642
  // rather than sitting under a separate run of dashes. In ten rows a line
631
643
  // spent on decoration is a line not spent on the run, and the box still
632
644
  // reads as closed at the top.
633
- // Only advertise navigation when it can reveal hidden rows: a hint for keys
634
- // that do nothing is worse than no hint.
635
- const hint = !__navigationForTests.enabled || maxOffset(total, MAX_ROWS - 2) === 0
636
- ? ""
637
- : scrolling ? "↑/↓ to scroll · Esc to exit" : "Alt+O to scroll";
638
645
  const label = ` ${title} `;
639
646
  const tail = hint === "" ? "" : ` ${hint} `;
640
647
  const spare = inner + 2 - visibleLength(label) - visibleLength(tail);
@@ -109,9 +109,10 @@ export function decodeWorkflowExtensions(value) {
109
109
  continue;
110
110
  }
111
111
  if (namespace === "trajectory") {
112
+ // Snapshots persisted before the themes setting was removed still carry a boolean `themes`; it is accepted and dropped.
112
113
  if (!object(raw) || Object.keys(raw).some((key) => key !== "port" && key !== "themes") || raw.port !== undefined && (typeof raw.port !== "number" || !Number.isSafeInteger(raw.port) || raw.port < 1 || raw.port > 65535) || raw.themes !== undefined && typeof raw.themes !== "boolean")
113
114
  return undefined;
114
- normalized.trajectory = { ...(raw.port === undefined ? {} : { port: raw.port }), ...(raw.themes === undefined ? {} : { themes: raw.themes }) };
115
+ normalized.trajectory = { ...(raw.port === undefined ? {} : { port: raw.port }) };
115
116
  continue;
116
117
  }
117
118
  if (!jsonValue(raw))
@@ -433,6 +434,28 @@ function decodeRunEvent(value) {
433
434
  return undefined;
434
435
  return { type: value.type, message: value.message, ...(timestamp === undefined ? {} : { timestamp }) };
435
436
  }
437
+ function decodeScriptCall(value) {
438
+ if (!object(value) || value.kind !== "shell" && value.kind !== "tool" || typeof value.name !== "string" || typeof value.path !== "string" || !finiteNumber(value.startedAt))
439
+ return undefined;
440
+ const shell = value.kind === "shell";
441
+ if (shell && !jsonValue(value.input))
442
+ return undefined;
443
+ const inputBytes = shell ? optionalNumber(value.inputBytes) : undefined;
444
+ const phase = optionalString(value.phase);
445
+ const finishedAt = optionalNumber(value.finishedAt);
446
+ let error;
447
+ if (value.error !== undefined) {
448
+ if (shell)
449
+ error = decodeWorkflowError(value.error);
450
+ else if (object(value.error) && typeof value.error.code === "string")
451
+ error = { code: isWorkflowErrorCode(value.error.code) ? value.error.code : "INTERNAL_ERROR" };
452
+ if (!error)
453
+ return undefined;
454
+ }
455
+ if (inputBytes === INVALID_PERSISTED_VALUE || phase === INVALID_PERSISTED_VALUE || finishedAt === INVALID_PERSISTED_VALUE)
456
+ return undefined;
457
+ return { kind: value.kind, name: value.name, path: value.path, ...(shell && jsonValue(value.input) ? { input: value.input } : {}), ...(inputBytes === undefined ? {} : { inputBytes }), ...(phase === undefined ? {} : { phase }), startedAt: value.startedAt, ...(finishedAt === undefined ? {} : { finishedAt }), ...(error === undefined ? {} : { error }) };
458
+ }
436
459
  function decodeDelivery(value) {
437
460
  if (!object(value))
438
461
  return undefined;
@@ -466,6 +489,9 @@ export function decodePersistedRun(value, allowLegacyAgentSessions = false) {
466
489
  const budgetEvents = value.budgetEvents === undefined ? undefined : decodeArray(value.budgetEvents, decodeBudgetEvent);
467
490
  const events = value.events === undefined ? undefined : decodeArray(value.events, decodeRunEvent);
468
491
  const delivery = value.delivery === undefined ? undefined : decodeDelivery(value.delivery);
492
+ const scriptCalls = value.scriptCalls === undefined ? undefined : decodeArray(value.scriptCalls, decodeScriptCall);
493
+ if (value.scriptCalls !== undefined && !scriptCalls)
494
+ return undefined;
469
495
  if (!agentSessions || !agents || parentRunId === INVALID_PERSISTED_VALUE || value.retry !== undefined && !retry || phase === INVALID_PERSISTED_VALUE || value.phaseHistory !== undefined && !phaseHistory || phaseHistoryIndex === INVALID_PERSISTED_VALUE || activeShells === INVALID_PERSISTED_VALUE || activeShellStartedAt === INVALID_PERSISTED_VALUE || value.activeShellsByPhase !== undefined && !activeShellsByPhase || value.error !== undefined && !error || failedAt === INVALID_PERSISTED_VALUE || value.budget !== undefined && !budget || budgetVersion === INVALID_PERSISTED_VALUE || value.budgetVersion !== undefined && !integer(budgetVersion) || value.usage !== undefined && !usage || value.budgetEvents !== undefined && !budgetEvents || value.events !== undefined && !events || value.delivery !== undefined && !delivery)
470
496
  return undefined;
471
497
  if (value.agentSessions === undefined && !allowLegacyAgentSessions)
@@ -486,6 +512,7 @@ export function decodePersistedRun(value, allowLegacyAgentSessions = false) {
486
512
  ...(activeShells === undefined ? {} : { activeShells }),
487
513
  ...(activeShellStartedAt === undefined ? {} : { activeShellStartedAt }),
488
514
  ...(activeShellsByPhase === undefined ? {} : { activeShellsByPhase }),
515
+ ...(scriptCalls === undefined ? {} : { scriptCalls }),
489
516
  ...(error === undefined ? {} : { error }),
490
517
  ...(failedAt === undefined ? {} : { failedAt }),
491
518
  ...(budget === undefined ? {} : { budget }),
@@ -1,10 +1,12 @@
1
1
  import { RunStore } from "./persistence.js";
2
2
  import type { AgentAttempt } from "./agent-execution.js";
3
- import type { AgentIdentity, JsonValue, ShellIdentity, ShellOptions, ShellResult, WorkflowBridge, WorkflowExecution } from "./types.js";
3
+ import type { AgentIdentity, JsonValue, ShellIdentity, ShellOptions, ShellResult, ToolIdentity, WorkflowBridge, WorkflowExecution } from "./types.js";
4
4
  export declare const RPC_LIMIT_BYTES: number;
5
5
  export declare const HEARTBEAT_TIMEOUT_MS = 5000;
6
6
  export declare function encoded(value: JsonValue): string;
7
7
  export declare function encoded(value: unknown): asserts value is JsonValue;
8
+ /** The journal path of a script tool call; the arguments' digest keeps calls with different arguments apart whatever order they start in. */
9
+ export declare function toolIdentityPath(identifier: string, args: JsonValue, identity: ToolIdentity): string;
8
10
  export declare function agentHandleTurnPath(handle: string, turn: number): string;
9
11
  export declare function agentIdentityPath(identity: AgentIdentity): string;
10
12
  export declare function shellIdentityPath(identity: ShellIdentity): string;
@@ -16,4 +18,4 @@ export declare function executeShellCommand(command: string, options: ShellOptio
16
18
  export declare function runWorkflow(script: string, args?: JsonValue, bridge?: WorkflowBridge, signal?: AbortSignal): WorkflowExecution;
17
19
  export declare function persistActiveAgentAttempt(store: RunStore, id: string, active: AgentAttempt): Promise<void>;
18
20
  export declare function persistAgentAttempts(store: RunStore, id: string, attempts: readonly AgentAttempt[]): Promise<void>;
19
- export type { AgentIdentity, FunctionIdentity, ShellIdentity, WorkflowBridge, WorkflowExecution } from "./types.js";
21
+ export type { AgentIdentity, FunctionIdentity, ShellIdentity, ToolIdentity, WorkflowBridge, WorkflowExecution } from "./types.js";
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  import { StringDecoder } from "node:string_decoder";
6
6
  import { structuralPath as operationPath } from "./persistence.js";
7
7
  import { WorkflowError, roleNameOf, sumAccounting, zeroAccounting } from "./types.js";
8
- import { asWorkflowError, errorText, fail, isWorkflowAuthored, isWorkflowErrorCode, jsonValue, markWorkflowAuthored, object, positiveInteger } from "./utils.js";
8
+ import { asWorkflowError, errorText, fail, isWorkflowAuthored, isWorkflowErrorCode, jsonDigest, jsonValue, markWorkflowAuthored, object, positiveInteger } from "./utils.js";
9
9
  import { instrumentWorkflow, validateAgentOptions, validateShellCommand, validateShellOptions } from "./validation.js";
10
10
  export const RPC_LIMIT_BYTES = 10 * 1024 * 1024;
11
11
  export const HEARTBEAT_TIMEOUT_MS = 5000;
@@ -268,6 +268,30 @@ const functions = Object.freeze(Object.fromEntries(Object.entries(config.functio
268
268
  Object.defineProperty(result, "toJSON", { value() { throw workError("INVALID_METADATA", "Workflow function result is a Promise; await it before serialization"); } });
269
269
  return result;
270
270
  }])));
271
+ // tools.<id>(args) runs a Pi session tool through the host. Any identifier resolves, so a resumed run
272
+ // replays journaled calls even when the tool is gone; the host rejects identifiers it cannot call.
273
+ // A call is the n-th call with the same arguments in its scope, so replay does not depend on the
274
+ // order in which concurrent calls start.
275
+ const NOT_TOOLS = new Set(["then", "toJSON", "toString", "valueOf", "constructor"]);
276
+ const sortedJson = value => Array.isArray(value) ? value.map(sortedJson) : value && typeof value === "object" ? Object.fromEntries(Object.keys(value).sort().map(key => [key, sortedJson(value[key])])) : value;
277
+ const toolOccurrences = new Map();
278
+ const toolCall = id => (...values) => {
279
+ if (worktreeOwners.getStore()) throw workError("INVALID_METADATA", "tools." + id + " is unavailable inside withWorktree: session tools resolve paths against the session working directory");
280
+ if (values.length > 1 || values[0] !== undefined && (!values[0] || typeof values[0] !== "object" || Array.isArray(values[0]))) throw workError("INVALID_METADATA", "tools." + id + " takes one arguments object");
281
+ const args = values[0] === undefined ? {} : values[0];
282
+ const structuralPath = inheritedAgentPath.getStore() || [];
283
+ const key = JSON.stringify([structuralPath, id, sortedJson(args)]);
284
+ const occurrence = (toolOccurrences.get(key) || 0) + 1;
285
+ toolOccurrences.set(key, occurrence);
286
+ return guardedResult(rpc("tool", [id, args, { structuralPath: [...structuralPath], occurrence }]), "tool");
287
+ };
288
+ const tools = new Proxy(Object.create(null), {
289
+ get: (_target, key) => typeof key === "string" && !NOT_TOOLS.has(key) ? toolCall(key) : undefined,
290
+ // Probing would branch on the session that resumed the run; the launch already checks named tools.
291
+ has: () => { throw workError("INVALID_METADATA", "Workflow scripts cannot probe tools with 'in'; call tools.<name>(...) directly, and the launch checks that the tool exists"); },
292
+ set: () => false,
293
+ defineProperty: () => false,
294
+ });
271
295
  const recordEntries = (value, kind) => {
272
296
  if (!value || typeof value !== "object" || Array.isArray(value)) throw workError("INVALID_METADATA", kind + " must be a record");
273
297
  return Object.entries(value);
@@ -328,7 +352,7 @@ const pipeline = async (operationName, items, stages) => {
328
352
  return Object.fromEntries(results.map(result => [result.name, result.value]));
329
353
  };
330
354
  const safeMath = Object.fromEntries(Object.getOwnPropertyNames(Math).filter(name => name !== "random").map(name => [name, Math[name]]));
331
- const sandbox = { agent, shell, withWorktree: rejectWorktree, prompt, checkpoint, parallel, pipeline, phase, log, args: config.args, Promise, JSON, Math: Object.freeze(safeMath) };
355
+ const sandbox = { agent, shell, withWorktree: rejectWorktree, prompt, checkpoint, parallel, pipeline, phase, log, tools, args: config.args, Promise, JSON, Math: Object.freeze(safeMath) };
332
356
  for (const [name, fn] of Object.entries(functions)) Object.defineProperty(sandbox, name, { value: fn, writable: false, configurable: false });
333
357
  for (const name of ["Date","eval","Function","WebAssembly","process","require","module","exports","console","fetch","XMLHttpRequest","WebSocket","performance","crypto","setTimeout","setInterval","setImmediate","queueMicrotask","Intl","SharedArrayBuffer","Atomics"]) sandbox[name] = undefined;
334
358
  const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
@@ -378,6 +402,15 @@ function readShellIdentity(value) {
378
402
  const identity = readAgentIdentity(value);
379
403
  return { structuralPath: identity.structuralPath, callSite: identity.callSite, occurrence: identity.occurrence, ...(identity.worktreeOwner ? { worktreeOwner: identity.worktreeOwner } : {}) };
380
404
  }
405
+ function readToolIdentity(value) {
406
+ if (!object(value) || !Array.isArray(value.structuralPath) || !value.structuralPath.every((part) => typeof part === "string" && Boolean(part.trim())) || !positiveInteger(value.occurrence))
407
+ fail("INTERNAL_ERROR", "Invalid workflow tool identity");
408
+ return { structuralPath: [...value.structuralPath], occurrence: value.occurrence };
409
+ }
410
+ /** The journal path of a script tool call; the arguments' digest keeps calls with different arguments apart whatever order they start in. */
411
+ export function toolIdentityPath(identifier, args, identity) {
412
+ return operationPath("tool", ...identity.structuralPath, identifier, `args:${jsonDigest(args)}`, String(identity.occurrence));
413
+ }
381
414
  export function agentHandleTurnPath(handle, turn) {
382
415
  return operationPath("agent", "handle", handle, `turn:${String(turn)}`);
383
416
  }
@@ -681,6 +714,14 @@ export function runWorkflow(script, args = null, bridge = {}, signal) {
681
714
  value = branded({ name, ok: false, failedAt: name, error: workerErrorShape(typed) });
682
715
  }
683
716
  }
717
+ else if (method === "tool") {
718
+ if (typeof values[0] !== "string" || !object(values[1]))
719
+ fail("INTERNAL_ERROR", "tool requires an identifier and object arguments");
720
+ const identity = readToolIdentity(values[2]);
721
+ if (!bridge.tool)
722
+ fail("UNKNOWN_TOOL", `tools.${values[0]} is unavailable: this run has no session tools`);
723
+ value = await bridge.tool(values[0], values[1], controller.signal, identity);
724
+ }
684
725
  else if (method === "worktree") {
685
726
  if (!bridge.worktree || typeof values[0] !== "string" || !values[0])
686
727
  fail("INTERNAL_ERROR", "worktree requires an active host bridge and scope");
@@ -108,6 +108,7 @@ const WORKFLOW_ERROR_PROSE = {
108
108
  RUN_NOT_FOUND: (detail) => /^Unknown workflow run\b/.test(detail) ? "The workflow run was not found." : `The workflow run was not found: ${detail}.`,
109
109
  RPC_LIMIT_EXCEEDED: (detail) => `The workflow communication data exceeded its size limit: ${detail}.`,
110
110
  SHELL_FAILED: (detail) => `The workflow shell command failed: ${detail}.`,
111
+ TOOL_FAILED: (detail) => `The workflow tool call failed: ${detail}.`,
111
112
  AGENT_TIMEOUT: (detail) => `The workflow agent timed out: ${detail}.`,
112
113
  AGENT_FAILED: (detail) => `The workflow agent failed: ${detail}.`,
113
114
  AGENT_RESULT_COLLECTED: (detail) => `The nested agent result was already collected: ${detail}.`,
@@ -1,6 +1,7 @@
1
1
  import { type ExtensionUIContext, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { RunStore, type PersistedRun } from "./persistence.js";
3
3
  import { type WorkflowRegistryApi } from "./registry.js";
4
+ import { type WorkflowProgressStyles } from "./host-view.js";
4
5
  import { type WorkflowRecoveryContext, type createWorkflowRecovery } from "./host-recovery.js";
5
6
  import { type WorkflowRunRecord } from "./host-runtime.js";
6
7
  import { type TrajectoryPublisherProvider } from "./trajectory-host-handle.js";
@@ -34,6 +35,23 @@ export type UiHostCapabilities = {
34
35
  };
35
36
  type ReportBlocked = (active: boolean, label?: string) => void;
36
37
  export declare function uiHostCapabilities(ui: unknown): UiHostCapabilities | undefined;
38
+ type WorkflowKeybindings = {
39
+ matches(data: string, binding: string): boolean;
40
+ };
41
+ export declare function workflowKeyMatches(keybindings: WorkflowKeybindings, data: string, binding: string): boolean;
42
+ export declare function workflowKeyLabel(keybindings: unknown, binding: string, fallback: string, labels?: Readonly<Record<string, string>>): string;
43
+ /**
44
+ * A yes/no question drawn inside a navigator dashboard. Pi's confirm dialog replaces a custom component and afterwards
45
+ * restores the editor rather than the component, which leaves the dashboard's promise pending and the session waiting on it.
46
+ */
47
+ export declare function createInlineConfirm(requestRender: () => void): {
48
+ ask(title: string, message: string): Promise<boolean>;
49
+ active(): boolean;
50
+ rows(width: number, styles: WorkflowProgressStyles): string[];
51
+ hint(keybindings: unknown): string;
52
+ handleInput(keybindings: WorkflowKeybindings, data: string): void;
53
+ cancel(): void;
54
+ };
37
55
  export type WorkflowNavigatorDependencies = {
38
56
  pi: Pick<ExtensionAPI, "registerCommand">;
39
57
  home: string | undefined;
@@ -110,13 +110,57 @@ function keybindingKeys(keybindings, name) {
110
110
  return isKeybindingGetKeys(getKeys) ? getKeys.call(keybindings, name) : undefined;
111
111
  }
112
112
  const WORKFLOW_VIM_KEYS = { "tui.select.up": "k", "tui.select.down": "j", "tui.editor.cursorLeft": "h", "tui.editor.cursorRight": "l" };
113
- function workflowKeyMatches(keybindings, data, binding) { return keybindings.matches(data, binding) || WORKFLOW_VIM_KEYS[binding] === data; }
114
- function workflowKeyLabel(keybindings, binding, fallback, labels) {
113
+ export function workflowKeyMatches(keybindings, data, binding) { return keybindings.matches(data, binding) || WORKFLOW_VIM_KEYS[binding] === data; }
114
+ const WORKFLOW_KEY_LABELS = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
115
+ export function workflowKeyLabel(keybindings, binding, fallback, labels = WORKFLOW_KEY_LABELS) {
115
116
  const keys = keybindingKeys(keybindings, binding);
116
117
  const configured = keys?.length ? keys.map((key) => labels[key] ?? key) : [fallback];
117
118
  const vim = WORKFLOW_VIM_KEYS[binding];
118
119
  return [...new Set(vim ? [...configured, vim] : configured)].join("/");
119
120
  }
121
+ /**
122
+ * A yes/no question drawn inside a navigator dashboard. Pi's confirm dialog replaces a custom component and afterwards
123
+ * restores the editor rather than the component, which leaves the dashboard's promise pending and the session waiting on it.
124
+ */
125
+ export function createInlineConfirm(requestRender) {
126
+ let pending;
127
+ const answer = (value) => {
128
+ const current = pending;
129
+ pending = undefined;
130
+ current?.resolve(value);
131
+ requestRender();
132
+ };
133
+ return {
134
+ ask(title, message) {
135
+ answer(false);
136
+ return new Promise((resolve) => { pending = { title, message, yes: true, resolve }; requestRender(); });
137
+ },
138
+ active() { return pending !== undefined; },
139
+ rows(width, styles) {
140
+ if (!pending)
141
+ return [];
142
+ const yes = pending.yes;
143
+ const message = truncateToVisualLines(pending.message, Number.MAX_SAFE_INTEGER, Math.max(1, width), 0).visualLines.map((line) => line.trimEnd());
144
+ return [styles.bold(pending.title), ...message, ...["Yes", "No"].map((option) => (option === "Yes") === yes ? `→ ${styles.accent(option)}` : ` ${option}`)];
145
+ },
146
+ hint(keybindings) {
147
+ return `${workflowKeyLabel(keybindings, "tui.select.up", "↑")}/${workflowKeyLabel(keybindings, "tui.select.down", "↓")} select · ${workflowKeyLabel(keybindings, "tui.select.confirm", "enter")} confirm · ${workflowKeyLabel(keybindings, "tui.select.cancel", "esc")} cancel`;
148
+ },
149
+ handleInput(keybindings, data) {
150
+ if (!pending)
151
+ return;
152
+ if (workflowKeyMatches(keybindings, data, "tui.select.up") || workflowKeyMatches(keybindings, data, "tui.select.down")) {
153
+ pending.yes = !pending.yes;
154
+ requestRender();
155
+ }
156
+ else if (workflowKeyMatches(keybindings, data, "tui.select.confirm"))
157
+ answer(pending.yes);
158
+ else if (workflowKeyMatches(keybindings, data, "tui.select.cancel"))
159
+ answer(false);
160
+ },
161
+ cancel() { answer(false); },
162
+ };
163
+ }
120
164
  export function registerWorkflowNavigator(deps) {
121
165
  const { pi, home, clipboard, extensionAgentDir, runs, terminalRunStates, hardTerminalRunStates, ensureSessionLease, answerCheckpoint, recovery, stopWorkflowRun, moveForegroundToBackground, isForegroundAttached, liveAgents, registry, projectTrusted, resumeHostContext, resumeSelectedWorkflow, reportBlocked, setNavigatorOpen, trajectoryProvider, coordinateRunMutation: coordinateRunMutationDependency } = deps;
122
166
  const coordinateRunMutation = coordinateRunMutationDependency ?? ((task) => task());
@@ -160,7 +204,7 @@ export function registerWorkflowNavigator(deps) {
160
204
  const setStatus = uiHostCapabilities(ctx.ui)?.setStatus;
161
205
  setStatus?.call(ctx.ui, "workflow-stop", text);
162
206
  };
163
- const runAction = async (actionCommand, status = setWorkflowStatus) => {
207
+ const runAction = async (actionCommand, status = setWorkflowStatus, confirm = (title, message) => confirmWithBlocked(ctx.ui, reportBlocked, title, message)) => {
164
208
  const [action, runId, ...rest] = actionCommand.split(/\s+/);
165
209
  try {
166
210
  const run = runId ? runs.get(runId) : undefined;
@@ -252,7 +296,7 @@ export function registerWorkflowNavigator(deps) {
252
296
  }
253
297
  if (action === "stop" && run) {
254
298
  const workflowName = stored?.loaded.run.workflowName ?? run.metadata.name;
255
- if (!await confirmWithBlocked(ctx.ui, reportBlocked, "Stop workflow?", `Stop workflow ${workflowName} (${run.store.runId})? This cannot be undone.`))
299
+ if (!await confirm("Stop workflow?", `Stop workflow ${workflowName} (${run.store.runId})? This cannot be undone.`))
256
300
  return "dashboard";
257
301
  status(`Stopping workflow ${workflowName}...`);
258
302
  await stopWorkflowRun(run.store.runId);
@@ -651,8 +695,18 @@ export function registerWorkflowNavigator(deps) {
651
695
  let selectedNodeId = tree.nodes[0]?.id;
652
696
  let expandedNodeIds = new Set(workflowPhaseTreeInitialExpanded(tree));
653
697
  const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_PANEL_FOOTER_ROWS);
654
- const keyLabels = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
655
- const keyLabel = (binding, fallback) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
698
+ const keyLabel = (binding, fallback) => workflowKeyLabel(keybindings, binding, fallback);
699
+ const inlineConfirm = createInlineConfirm(() => { if (!disposed)
700
+ tui.requestRender(); });
701
+ const confirmInDashboard = async (title, message) => {
702
+ reportBlocked?.(true, title);
703
+ try {
704
+ return await inlineConfirm.ask(title, message);
705
+ }
706
+ finally {
707
+ reportBlocked?.(false);
708
+ }
709
+ };
656
710
  const progressNow = () => {
657
711
  const now = Date.now();
658
712
  if (hardTerminalRunStates.has(view.run.state)) {
@@ -738,7 +792,7 @@ export function registerWorkflowNavigator(deps) {
738
792
  setWorkflowStatus(status);
739
793
  if (!disposed)
740
794
  tui.requestRender();
741
- }).then(async (outcome) => {
795
+ }, confirmInDashboard).then(async (outcome) => {
742
796
  if (outcome === "stopped") {
743
797
  done("__stopped__");
744
798
  return;
@@ -778,7 +832,8 @@ export function registerWorkflowNavigator(deps) {
778
832
  const content = [...statusLines, ...phaseLines];
779
833
  const rows = terminalRows();
780
834
  const hintRows = rows >= 3 ? 1 : 0;
781
- const viewport = Math.max(1, rows - hintRows);
835
+ const confirmRows = inlineConfirm.rows(width, styles);
836
+ const viewport = Math.max(1, rows - hintRows - confirmRows.length);
782
837
  const maxOffset = Math.max(0, content.length - viewport);
783
838
  dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
784
839
  if (actionMode) {
@@ -804,11 +859,15 @@ export function registerWorkflowNavigator(deps) {
804
859
  dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
805
860
  const selectedNode = selectedNodeId ? tree.byId.get(selectedNodeId) : undefined;
806
861
  const enterAction = selectedNode?.kind === "workflow" ? "run actions" : selectedNode?.kind === "agent" ? "agent actions" : selectedNode?.children.length ? "expand/collapse" : narrow ? "inspect" : "focus details";
807
- 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")} ${enterAction} · 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] ?? "";
808
- return [...content.slice(dashboardOffset, dashboardOffset + viewport), ...(hintRows ? [hint] : [])];
862
+ const hint = truncateToVisualLines(theme.fg("dim", inlineConfirm.active() ? inlineConfirm.hint(keybindings) : 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")} ${enterAction} · 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] ?? "";
863
+ return [...content.slice(dashboardOffset, dashboardOffset + viewport), ...confirmRows, ...(hintRows ? [hint] : [])];
809
864
  },
810
865
  invalidate() { },
811
866
  handleInput(data) {
867
+ if (inlineConfirm.active()) {
868
+ inlineConfirm.handleInput(keybindings, data);
869
+ return;
870
+ }
812
871
  if (stopRequested || editorRunning)
813
872
  return;
814
873
  const narrow = renderedWidth < 80;
@@ -958,7 +1017,7 @@ export function registerWorkflowNavigator(deps) {
958
1017
  }
959
1018
  tui.requestRender();
960
1019
  },
961
- dispose() { disposed = true; stopTimer(); setWorkflowStatus(undefined); },
1020
+ dispose() { disposed = true; stopTimer(); setWorkflowStatus(undefined); inlineConfirm.cancel(); },
962
1021
  };
963
1022
  })
964
1023
  : await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Back"]);
@@ -1016,8 +1075,7 @@ export function registerWorkflowNavigator(deps) {
1016
1075
  const currentLayout = layout();
1017
1076
  const maxOffset = Math.max(0, renderedLines.length - currentLayout.contentViewport);
1018
1077
  offset = Math.min(offset, maxOffset);
1019
- const keyLabels = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
1020
- const keyLabel = (binding, fallback) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
1078
+ const keyLabel = (binding, fallback) => workflowKeyLabel(keybindings, binding, fallback);
1021
1079
  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] ?? "";
1022
1080
  const controls = currentLayout.compactControls
1023
1081
  ? [options.map((option, index) => `${index === selectedIndex ? "[" : " "}${option}${index === selectedIndex ? "]" : " "}`).join(" ")]
@@ -1,8 +1,8 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
- import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { type ExtensionAPI, type ExtensionToolContext } from "@earendil-works/pi-coding-agent";
3
3
  import { FairAgentScheduler, WorkflowAgentExecutor, type AgentDefinition, type AgentExecutionRoot, type AgentProviderFailure, type AgentProviderRecovery } from "./agent-execution.js";
4
4
  import { RunStore, type PersistedRun } from "./persistence.js";
5
- import { WorkflowError, type AgentIdentity, type AgentResourcePolicy, type BudgetApprovalRequest, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type WorkflowMetadata, type WorkflowWorktreeReference } from "./types.js";
5
+ import { WorkflowError, type AgentIdentity, type AgentResourcePolicy, type BudgetApprovalRequest, type JsonValue, type LaunchSnapshot, type ModelSpec, type RunState, type ShellIdentity, type ShellOptions, type ShellResult, type ToolIdentity, type WorkflowMetadata, type WorkflowWorktreeReference } from "./types.js";
6
6
  import { type WorkflowRegistryApi } from "./registry.js";
7
7
  import { RunLifecycle, WorkflowEventPublisher, type WorkflowRunRecord, type WorkflowToolUpdate } from "./host-runtime.js";
8
8
  import { type CompletionDeliveryContext, type CompletionDeliveryResult } from "./host-delivery.js";
@@ -21,6 +21,7 @@ export type WorkflowRecoveryContext = {
21
21
  } | undefined;
22
22
  modelRegistry: ModelRegistryCapability | undefined;
23
23
  deliveryContext: CompletionDeliveryContext;
24
+ toolContext?: ExtensionToolContext | undefined;
24
25
  signal?: AbortSignal;
25
26
  resolvedAliases?: Readonly<Record<string, string>>;
26
27
  blockedAliases?: ReadonlySet<string>;
@@ -70,6 +71,7 @@ export type WorkflowRecoveryDependencies = {
70
71
  };
71
72
  workflowAgentHandler: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, executor: WorkflowAgentExecutor, cwd: string, runId: string, captureRole?: (role: string, model: ModelSpec) => Promise<void>) => (prompt: string, options: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: AgentIdentity) => Promise<JsonValue>;
72
73
  shellForRun: (store: RunStore, metadata: WorkflowMetadata, lifecycle: RunLifecycle, command: string, options: ShellOptions, signal: AbortSignal, identity: ShellIdentity) => Promise<ShellResult>;
74
+ toolForRun: (store: RunStore, lifecycle: RunLifecycle, context: ExtensionToolContext | undefined, identifier: string, args: Readonly<Record<string, JsonValue>>, signal: AbortSignal, identity: ToolIdentity) => Promise<JsonValue>;
73
75
  resolveWorktree: (store: RunStore, metadata: WorkflowMetadata, owner: string) => Promise<Readonly<WorkflowWorktreeReference>>;
74
76
  checkpointBridge: (runId: string, store: RunStore, metadata: WorkflowMetadata, foreground: boolean, ui?: {
75
77
  select?: (prompt: string, options: string[]) => Promise<string | undefined>;
@@ -104,6 +106,7 @@ export declare function createWorkflowRecovery(deps: WorkflowRecoveryDependencie
104
106
  } | undefined;
105
107
  modelRegistry: WorkflowRecoveryContext["modelRegistry"];
106
108
  deliveryContext: CompletionDeliveryContext;
109
+ toolContext?: ExtensionToolContext | undefined;
107
110
  signal?: AbortSignal | undefined;
108
111
  resolvedAliases?: Readonly<Record<string, string>>;
109
112
  blockedAliases?: ReadonlySet<string>;
@@ -46,7 +46,7 @@ function assertExpectedWorkflowState(expectedState, actualState) {
46
46
  }
47
47
  export function persistedFailure(run, error) { const failedAt = workflowFailedAt(error); return { ...run, error: { code: error.code, message: error.message, ...(failedAt ? { failedAt } : {}) }, ...(failedAt ? { failedAt } : {}) }; }
48
48
  export function createWorkflowRecovery(deps) {
49
- const { pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue, resumeRoles, workflowAgentHandler, shellForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver, deliverTerminal, workflowToolUpdate, registry, modelSpec } = deps;
49
+ const { pi, home, runs, scheduler, eventPublisher, persistRunState, projectTrusted, resumeHostContext, ensureSessionLease, createAgentExecutor, activeSnapshotTools, frozenResourcePolicy, resolveLaunchPrologue, resumeRoles, workflowAgentHandler, shellForRun, toolForRun, resolveWorktree, checkpointBridge, phaseBridge, logBridge, lifecycleFor, createProviderErrorRecovery, cleanupTerminalRun, deliver, deliverTerminal, workflowToolUpdate, registry, modelSpec } = deps;
50
50
  const coordinateRunMutation = deps.coordinateRunMutation ?? ((task) => task());
51
51
  const budgetDecisionDelivery = (metadata, request) => `Workflow ${metadata.name} budget adjustment ${request.proposalId} for run ${request.runId} requires approval. Consumed usage: ${JSON.stringify(request.consumed)}. Previous limits: ${JSON.stringify(request.previous)}. Proposed limits: ${JSON.stringify(request.proposed)}. Respond with workflow_respond using proposalId ${request.proposalId}.`;
52
52
  const appendBudgetDecisionEvent = async (run, request, type) => {
@@ -119,7 +119,7 @@ export function createWorkflowRecovery(deps) {
119
119
  run.executor.setRunContext(runContext);
120
120
  await scheduler.cancelRun(run.store.runId);
121
121
  await run.lifecycle.resume();
122
- 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, roles.capture), 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.store, run.lifecycle, run.metadata.name) }, run.store, runContext, registry, snapshot.settings.extensionSettings), controller.signal);
122
+ const execution = runWorkflow(script, loaded.snapshot.args, withWorkflowFunctions({ tool: (identifier, args, signal, identity) => toolForRun(run.store, run.lifecycle, context?.toolContext, identifier, args, signal, identity), 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, roles.capture), 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.store, run.lifecycle, run.metadata.name) }, run.store, runContext, registry, snapshot.settings.extensionSettings), controller.signal);
123
123
  run.execution = execution;
124
124
  const completion = execution.result.then(async (value) => {
125
125
  await scheduler.flush(run.store.runId);
@@ -333,7 +333,7 @@ export function createWorkflowRecovery(deps) {
333
333
  await eventPublisher.runStarted(prepared.childRun.store, prepared.childRun.metadata);
334
334
  const { hasUI, ui } = recoveryUi(context);
335
335
  const recoveryContext = resumeHostContext(context);
336
- const completed = await coldResumeRun(prepared.childRun, hasUI, ui, projectTrusted(context), { model: prepared.hostModel, modelRegistry: prepared.modelRegistry, deliveryContext: recoveryContext.deliveryContext, resolvedAliases: prepared.currentAliases, blockedAliases: prepared.blockedAliases, blockedAliasTargets: prepared.blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
336
+ const completed = await coldResumeRun(prepared.childRun, hasUI, ui, projectTrusted(context), { model: prepared.hostModel, modelRegistry: prepared.modelRegistry, deliveryContext: recoveryContext.deliveryContext, toolContext: recoveryContext.toolContext, resolvedAliases: prepared.currentAliases, blockedAliases: prepared.blockedAliases, blockedAliasTargets: prepared.blockedAliasTargets, ...(signal ? { signal } : {}) }, modeOverride);
337
337
  const completion = runs.get(prepared.childRun.store.runId)?.completion;
338
338
  if (completion) {
339
339
  childStarted = true;
@@ -12,7 +12,16 @@ export interface WorkflowProgressStyles {
12
12
  dim(text: string): string;
13
13
  bold(text: string): string;
14
14
  }
15
- export declare function formatWorkflowProgress(run: PersistedRun, spinner?: string, styles?: WorkflowProgressStyles, now?: number, expanded?: boolean, width?: number, showHint?: boolean): string;
15
+ export declare const PLAIN_WORKFLOW_PROGRESS_STYLES: WorkflowProgressStyles;
16
+ export declare function runStateGlyph(state: string, running: string): string;
17
+ export declare function progressStyleForState(state: string, styles: WorkflowProgressStyles): (text: string) => string;
18
+ export declare function formatWorkflowRuntime(durationMs: number): string;
19
+ /**
20
+ * Renders the run; with `maxLines`, a view taller than that limit switches to a compact one so the live block stays on screen.
21
+ * A block taller than the terminal pushes its changing header above the viewport, and pi-tui then clears and redraws the whole
22
+ * scrollback on every update, which snaps the reader back to the bottom.
23
+ */
24
+ export declare function formatWorkflowProgress(run: PersistedRun, spinner?: string, styles?: WorkflowProgressStyles, now?: number, expanded?: boolean, width?: number, showHint?: boolean, maxLines?: number): string;
16
25
  export declare function textBlock(text: string): {
17
26
  render(width: number): string[];
18
27
  invalidate(): void;
@@ -75,6 +84,11 @@ export declare function navigatorRunLabels(entries: readonly {
75
84
  export declare function agentBreadcrumbParts(agent: AgentRecord, byId: Map<string, AgentRecord>, includeStructuralPath?: boolean): string[];
76
85
  export declare function agentBreadcrumb(agent: AgentRecord, byId: Map<string, AgentRecord>, includeStructuralPath?: boolean): string;
77
86
  export declare function formatStalledDuration(durationMs: number): string;
87
+ export declare function stalledDuration(agent: Pick<AgentDetailPresentation, "state" | "lastEventAt">, now: number): number | undefined;
88
+ export declare function agentActivityLabel(agent: {
89
+ readonly activity?: AgentRecord["activity"];
90
+ readonly toolCalls?: AgentRecord["toolCalls"];
91
+ }): string;
78
92
  export interface AgentDetailPresentation {
79
93
  readonly state: string;
80
94
  readonly activity?: AgentRecord["activity"];
@@ -93,6 +107,7 @@ export interface AgentDetailPresentation {
93
107
  readonly message: string;
94
108
  };
95
109
  }
110
+ export declare function formatAgentError(error: NonNullable<AgentDetailPresentation["error"]>, styles?: WorkflowProgressStyles): string;
96
111
  export declare function formatAgentDetail(agent: Readonly<AgentDetailPresentation>, styles?: WorkflowProgressStyles, now?: number, options?: Readonly<{
97
112
  includeError?: boolean;
98
113
  }>): string[];
@@ -113,4 +128,9 @@ export declare function formatNavigatorRun(loaded: {
113
128
  snapshot: Readonly<LaunchSnapshot>;
114
129
  }, checkpoints: readonly AwaitingCheckpoint[], worktrees: readonly WorktreeReference[], now?: number): string;
115
130
  export declare function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>, width: number, selection?: WorkflowPhaseSelection, styles?: WorkflowProgressStyles, now?: number): string[];
131
+ /** The navigator layout: a sidebar beside details at 80 columns or wider, otherwise one of them (or both stacked). */
132
+ export declare function formatNavigatorColumns(sidebar: readonly string[], detail: readonly string[], width: number, layout?: Readonly<{
133
+ treeOnly?: boolean | undefined;
134
+ detailsOnly?: boolean | undefined;
135
+ }>): string[];
116
136
  export declare function formatCheckpointReview(checkpoint: AwaitingCheckpoint): string;