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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/src/agent-execution.js +36 -51
  3. package/dist/src/background-widget.d.ts +56 -0
  4. package/dist/src/background-widget.js +22 -15
  5. package/dist/src/decoders.js +2 -1
  6. package/dist/src/host-navigator.d.ts +18 -0
  7. package/dist/src/host-navigator.js +71 -13
  8. package/dist/src/host-view.d.ts +16 -1
  9. package/dist/src/host-view.js +109 -42
  10. package/dist/src/host.js +57 -24
  11. package/dist/src/index.d.ts +3 -1
  12. package/dist/src/index.js +867 -545
  13. package/dist/src/index.js.map +3 -3
  14. package/dist/src/registry.js +2 -1
  15. package/dist/src/roles.d.ts +70 -0
  16. package/dist/src/roles.js +325 -0
  17. package/dist/src/settings.d.ts +13 -0
  18. package/dist/src/settings.js +253 -0
  19. package/dist/src/trajectory.d.ts +29 -4
  20. package/dist/src/trajectory.js +58 -3
  21. package/dist/src/types.d.ts +0 -1
  22. package/dist/src/utils.d.ts +2 -1
  23. package/dist/src/utils.js +10 -0
  24. package/dist/src/validation.d.ts +5 -17
  25. package/dist/src/validation.js +9 -458
  26. package/dist/subagents/index.js +1132 -694
  27. package/dist/subagents/index.js.map +3 -3
  28. package/dist/subagents/src/contracts.d.ts +4 -0
  29. package/dist/subagents/src/index.d.ts +1 -1
  30. package/dist/subagents/src/index.js +2 -1
  31. package/dist/subagents/src/manager.js +29 -10
  32. package/dist/subagents/src/navigator.js +396 -247
  33. package/dist/subagents/src/view.d.ts +23 -2
  34. package/dist/subagents/src/view.js +96 -30
  35. package/dist/trajectory/assets/index.html +347 -231
  36. package/dist/trajectory/assets/prism.min.js +13 -0
  37. package/dist/trajectory/index.js +377 -257
  38. package/dist/trajectory/index.js.map +3 -3
  39. package/dist/trajectory/src/assets/index.html +347 -231
  40. package/dist/trajectory/src/assets/prism.min.js +13 -0
  41. package/dist/trajectory/src/export.js +4 -2
  42. package/dist/trajectory/src/index.d.ts +2 -0
  43. package/dist/trajectory/src/index.js +143 -42
  44. package/dist/trajectory/src/server.js +134 -9
  45. package/dist/trajectory/src/server.js.map +1 -1
  46. package/package.json +2 -1
  47. package/src/agent-execution.ts +36 -42
  48. package/src/background-widget.ts +25 -18
  49. package/src/decoders.ts +2 -1
  50. package/src/host-navigator.ts +58 -14
  51. package/src/host-view.ts +93 -37
  52. package/src/host.ts +51 -21
  53. package/src/index.ts +3 -1
  54. package/src/registry.ts +2 -1
  55. package/src/roles.ts +326 -0
  56. package/src/settings.ts +206 -0
  57. package/src/trajectory.ts +76 -7
  58. package/src/types.ts +1 -1
  59. package/src/utils.ts +8 -1
  60. package/src/validation.ts +12 -356
  61. package/subagents/README.md +1 -1
  62. package/subagents/src/contracts.ts +4 -0
  63. package/subagents/src/index.ts +3 -2
  64. package/subagents/src/manager.ts +28 -10
  65. package/subagents/src/navigator.ts +336 -180
  66. package/subagents/src/view.ts +97 -28
  67. package/trajectory/src/assets/index.html +347 -231
  68. package/trajectory/src/assets/prism.min.js +13 -0
  69. package/trajectory/src/export.ts +4 -2
  70. package/trajectory/src/index.ts +118 -38
  71. package/trajectory/src/server.ts +112 -12
package/CHANGELOG.md CHANGED
@@ -1,6 +1,73 @@
1
1
  # Changelog
2
2
  ## Unreleased
3
3
 
4
+ ## [5.17.0] - 2026-09-23
5
+
6
+ ### New capabilities
7
+
8
+ - `/subagents` opens the `/workflow` dashboard layout instead of a picker followed by a `key=value` detail dump. The run list sits beside the selected run's details at 80 columns or wider and drills down from list to details to actions below that, with the same configured and vim key hints and page scrolling. While a run is active, the active runs and the selection refresh every second. Details show the run ID and the shared agent fields in the `/workflow` agent order; the prompt, live system prompt, and result open in the editor, as in `/workflow`, and editor and copy actions leave the action menu open. A running run shows its live activity through the new opt-in `includeActivity` inspection context; `subagents_inspect` still leaves activity out. A retry selects the new run, and a repeated name carries its short ID in the list, as in the `/workflow` picker. Outside the TUI, the picker rows follow the `/workflow` picker: state glyph, name, state, model, cost, and runtime.
9
+ - `/subagents` gains the `/workflow` run actions that apply to standalone runs: `Delete`, `Delete all completed`, and `Delete all failed` (also in the picker outside the TUI), and `Copy run path`. Stop and deletions ask for confirmation. `SubagentManager` gains an optional `delete(request, context)` that removes a settled record and refuses running runs and runs whose worktree cleanup is pending; it is not a model tool.
10
+ - Background subagents appear below the editor in the workflow widget's frame, one row per run with model, tokens, cost, elapsed time, and quiet or stalled warnings, and each leaves a receipt in the transcript when it settles, like a background workflow run.
11
+ - The `subagents_run` result header shows tokens and cost, like the workflow header.
12
+
13
+ ### Fixes
14
+
15
+ - Declining the Stop confirmation in the `/workflow` dashboard no longer leaves Pi stuck. Pi's confirm dialog replaced the dashboard and then restored the editor, so the `/workflow` command never finished: later messages and extension commands such as `/subagents` queued without running, and the workflow widget stayed hidden. The question now appears inside the dashboard, as in `/subagents`; declining keeps the dashboard open on the action menu.
16
+ - A subagent that settled just before its manager was disposed, for example at session shutdown, no longer stays `running` on disk. Disposal skipped the terminal status write, and a new manager in the same Pi process treated the record as owned by a live process, so it kept showing as running and could not be retried.
17
+
18
+ ## [5.16.3] - 2026-09-23
19
+
20
+ ### Fixes
21
+
22
+ - A live foreground workflow block taller than the terminal no longer snaps pi's scrollback to the bottom on every update. Past `rows - 10` lines the block turns compact: finished phases collapse to one summary line (done, failed, wall time, tokens, cost) plus their failures, and the current phase lists failed, active, and the three latest settled agents with a count of the rest. `ctrl+o` still shows everything, and a finished block keeps the full list.
23
+
24
+ ## [5.16.2] - 2026-09-23
25
+
26
+ ### New capabilities
27
+
28
+ - Trajectory highlights workflow scripts, JSON values (tool and workflow arguments, environment, and output), and fenced code in messages with a vendored Prism 1.30.0 build (JavaScript, TypeScript, JSON, bash, diff, Python).
29
+ - Trajectory lists runs by newest completion with active runs on top. Each session shows every active run or subagent and at least three in total; the rest sit behind a "+ N more" toggle that is remembered per browser, and the selected entry always stays visible.
30
+
31
+ ### Fixes
32
+
33
+ - Pi no longer adopts a Trajectory server that answers on the port without owning the lock, such as an older server whose lock was lost. `/health` now reports the server pid and fingerprint; a mismatching server that reports its pid is replaced, and one without an identity fails the attach with a clear error instead of serving outdated code indefinitely.
34
+
35
+ ## [5.16.1] - 2026-09-23
36
+
37
+ ### Fixes
38
+
39
+ - Trajectory no longer blanks a live subagent's or agent's events for about a second when its transcript grows between a state poll and the transcript read; the stale reply keeps the cached entries until the newer revision arrives.
40
+
41
+ ## [5.16.0] - 2026-09-23
42
+
43
+ ### Breaking changes
44
+
45
+ - `extensionSettings.trajectory.themes` is removed; settings files that still set it fail validation. Persisted launch snapshots that carry it still load.
46
+
47
+ ### New capabilities
48
+
49
+ - Trajectory has a paper-style look in light and dark versions. It follows the system colour scheme, and a sidebar toggle overrides it per browser. On phones it shows one panel at a time, with Runs, Trace, and Details tabs.
50
+ - The Trajectory inspector shows workflow arguments (bounded for large values) (#300) and an Agent details Output tab (#301).
51
+
52
+ ### Performance
53
+
54
+ - Foreground workflow progress bursts collapse to one update per 100ms, and progress reads `state.json` alone instead of state plus snapshot, so Pi no longer re-renders the whole tool block for every agent event.
55
+ - Trajectory ships tool timing only for the run a browser shows and omits timing it already delivered at the same revision. The gantt patches only changed lanes and merges tool calls that overlap on screen.
56
+ - Trajectory publishers poll every 10s while no browser is attached, and hidden tabs skip rendering.
57
+
58
+ ### Fixes
59
+
60
+ - A Trajectory browser tab that drops without a WebSocket close frame is released, so viewer counts and pending requests no longer leak.
61
+ - Live Trajectory timing keeps the newest entries within its 64 KB per-agent budget, so a long agent's gantt keeps growing instead of freezing on its first calls.
62
+ - `piewf` `npx` examples use the scoped `@piewf/cli` package.
63
+
64
+ ## [5.15.0] - 2026-09-18
65
+
66
+ ### New capabilities
67
+
68
+ - Roles are exposed as a standalone `pi-extensible-workflows/roles` subpath (`discoverRoles`, `loadRole`, `resolveRole`) that resolves a role into plain data (model, tools, skills, extensions, prompt, context file scopes, unmatched selectors) without a workflow run; the workflow executor, validation, CLI, and subagents consume the same implementation (#294).
69
+ - `@piewf/cli` adds a `pi-role <role> [pi arguments...]` launcher that resolves a role and starts the regular Pi CLI with the matching model, tools, skills, extensions, and system prompt arguments; remaining arguments are forwarded to Pi (#295).
70
+
4
71
  ## [5.14.1] - 2026-09-17
5
72
 
6
73
  ### Fixes
@@ -7,7 +7,7 @@ import { Type } from "@earendil-works/pi-ai";
7
7
  import { Compile } from "typebox/compile";
8
8
  import { createAgentSession, DefaultPackageManager, DefaultResourceLoader, defineTool, getAgentDir, ModelRuntime, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
9
9
  ;
10
- import { SerialLane, assertModelThinking, byPriorityThenName, deepFreeze, errorText, jsonObject, jsonValue, mergeWorkflowExtensionSettings, object, modelAliasName, modelCapability, resolveModelReference, resourcePatternHasMagic, selectResourcesByLayers, unmatchedResourcePatterns } from "./utils.js";
10
+ import { SerialLane, assertModelThinking, byPriorityThenName, deepFreeze, errorText, jsonObject, jsonValue, mergeWorkflowExtensionSettings, object, resolveModelReference, resourcePatternHasMagic, unmatchedResourcePatterns } from "./utils.js";
11
11
  import { SETTLED_AGENT_STATES, WorkflowError, isContextFileScope, zeroAccounting } from "./types.js";
12
12
  import { createLiveSessionHandoff } from "./session-handoff.js";
13
13
  import { createToolTimingExtension } from "./tool-timing.js";
@@ -16,6 +16,7 @@ import { createPiRuntimeAgentRunner, isRuntimeAgentProviderError, normalizePiRun
16
16
  import { defaultWorkflowResultSchema } from "./runtime/workflow-result.js";
17
17
  import { validateAgentOptions, validateSchema } from "./validation.js";
18
18
  import { canonicalPath } from "./paths.js";
19
+ import { canonicalExtensionSelector, resolveRole } from "./roles.js";
19
20
  const localToolTimingExtension = createToolTimingExtension();
20
21
  function parseModel(value, fallback, aliases = {}, knownModels, settingsPath) {
21
22
  if (!value)
@@ -36,16 +37,6 @@ const WORKFLOW_HOST_ENTRIES = new Set([
36
37
  canonicalPath(resolve(workflowPackageRoot, "subagents/index.ts")),
37
38
  canonicalPath(resolve(workflowPackageRoot, "dist/subagents/index.js")),
38
39
  ]);
39
- function canonicalExtensionSelector(selector, base = process.cwd()) {
40
- const negated = selector.startsWith("!");
41
- const body = negated ? selector.slice(1) : selector;
42
- if (body === "*" || body === "**" || body.startsWith("**/"))
43
- return selector;
44
- const resolved = resolve(base, body);
45
- if (resourcePatternHasMagic(body))
46
- return `${negated ? "!" : ""}${resolved}`;
47
- return `${negated ? "!" : ""}${canonicalPath(resolved)}`;
48
- }
49
40
  const WORKFLOW_DIRECTORY = "pi-extensible-workflows";
50
41
  function workflowSystemPromptPath(cwd, agentDir, projectTrusted) {
51
42
  const projectPath = join(cwd, ".pi", WORKFLOW_DIRECTORY, "SYSTEM.md");
@@ -205,20 +196,22 @@ async function createLocalPiSessionHandle(input, sessionStartEvent) {
205
196
  const resolved = await packageManager.resolve();
206
197
  const discoveredExtensions = [...new Set(resolved.extensions.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => canonicalPath(path)))];
207
198
  const selectorSources = policy.selectorSources;
208
- const extensionLayers = [selectorSources.global.extensions, selectorSources.project.extensions, selectorSources.role?.extensions, selectorSources.call?.extensions];
209
- const extensionSelectors = extensionLayers.flatMap((layer) => (layer ?? []).map((original) => ({ original, matching: canonicalExtensionSelector(original, input.cwd) })));
210
- const selectedExtensions = selectResourcesByLayers(extensionLayers.map((layer) => layer?.map((selector) => canonicalExtensionSelector(selector, input.cwd))), discoveredExtensions);
199
+ const selection = resolveRole(undefined, {
200
+ cwd: input.cwd,
201
+ selectorSources,
202
+ resources: { extensions: discoveredExtensions },
203
+ });
204
+ const selectedExtensions = selection.selectedExtensions ?? [];
211
205
  const extensionPaths = selectedExtensions.filter((path) => !WORKFLOW_HOST_ENTRIES.has(path));
212
- const unmatchedExtensions = extensionSelectors.filter(({ matching }) => unmatchedResourcePatterns([matching], discoveredExtensions).length > 0).map(({ original }) => original);
213
206
  policy.selectedExtensions = selectedExtensions;
214
- policy.unmatchedExtensions = unmatchedExtensions;
207
+ policy.unmatchedExtensions = selection.unmatchedExtensions ?? [];
215
208
  const skillPaths = [...new Set(resolved.skills.filter(({ enabled, metadata }) => enabled && (policy.projectTrusted || metadata.scope !== "project")).map(({ path }) => path))];
216
209
  const updateSkillMatches = (skills) => {
217
210
  const names = [...new Set(skills.map(({ name }) => name))];
218
- const skillLayers = [selectorSources.global.skills, selectorSources.project.skills, selectorSources.role?.skills, selectorSources.call?.skills];
219
- const selectedSkills = selectResourcesByLayers(skillLayers, names);
211
+ const selected = resolveRole(undefined, { cwd: input.cwd, selectorSources, resources: { skills: names } });
212
+ const selectedSkills = selected.selectedSkills ?? [];
220
213
  policy.selectedSkills = selectedSkills;
221
- policy.unmatchedSkills = unmatchedResourcePatterns(skillLayers.flatMap((layer) => layer ?? []), names);
214
+ policy.unmatchedSkills = selected.unmatchedSkills ?? [];
222
215
  return new Set(names.filter((name) => !selectedSkills.includes(name)));
223
216
  };
224
217
  resourceLoader = new DefaultResourceLoader({
@@ -944,7 +937,6 @@ export class WorkflowAgentExecutor {
944
937
  const roleDefinition = roleName ? this.root.agentDefinitions?.[roleName] : undefined;
945
938
  if (roleName && !roleDefinition)
946
939
  throw new WorkflowError("UNKNOWN_AGENT_TYPE", `Unknown agent role: ${roleName}`);
947
- const candidateTools = inheritedTools !== undefined ? [...inheritedTools] : [...this.root.tools];
948
940
  if (inheritedTools === undefined)
949
941
  for (const pattern of roleDefinition?.tools ?? []) {
950
942
  const body = pattern.startsWith("!") ? pattern.slice(1) : pattern;
@@ -956,37 +948,30 @@ export class WorkflowAgentExecutor {
956
948
  }
957
949
  }
958
950
  }
959
- const selectorLayers = [
960
- this.root.resourceSelectors?.tools,
961
- roleDefinition?.tools,
962
- options.tools,
963
- ];
964
- for (const pattern of options.tools ?? []) {
965
- const body = pattern.startsWith("!") ? pattern.slice(1) : pattern;
966
- if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !candidateTools.includes(body))
967
- throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the launching session boundary: ${body}`);
968
- }
969
- const outsideEffectiveTool = options.effectiveTools?.find((tool) => !candidateTools.includes(tool));
970
- if (outsideEffectiveTool)
971
- throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the launching session boundary: ${outsideEffectiveTool}`);
972
- const requested = options.effectiveTools ?? selectResourcesByLayers(selectorLayers, candidateTools);
973
- const forbidden = requested.find((tool) => !this.root.tools.has(tool));
974
- if (forbidden)
975
- throw new WorkflowError("UNKNOWN_TOOL", `Tool is outside the launching session boundary: ${forbidden}`);
976
- const requestedModel = options.model ?? (roleDefinition?.model && roleDefinition.thinking && !roleDefinition.model.includes(":") ? `${roleDefinition.model}:${roleDefinition.thinking}` : roleDefinition?.model);
977
- const alias = requestedModel === undefined ? undefined : modelAliasName(requestedModel, this.root.modelAliases ?? {});
978
- const blockedAlias = requestedModel?.split(":", 1)[0];
979
- if (requestedModel !== undefined && blockedAlias && this.root.blockedAliases?.has(blockedAlias) && !alias) {
980
- const target = this.root.blockedAliasTargets?.[blockedAlias];
981
- throw new WorkflowError("UNKNOWN_MODEL", `Unknown model alias ${requestedModel}${target ? ` resolved to ${target}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`);
982
- }
983
- const model = options.modelOverride ?? parseModel(requestedModel, this.root.model, this.root.modelAliases, this.root.knownModels ?? this.root.availableModels, this.root.settingsPath);
984
- const availableModels = this.root.knownModels ?? this.root.availableModels ?? new Set([modelCapability(this.root.model)]);
985
- if (!availableModels.has(modelCapability(model)))
986
- throw new WorkflowError("UNKNOWN_MODEL", `Unknown model${requestedModel ? ` ${requestedModel} resolved to ${modelCapability(model)}` : ""}${this.root.settingsPath ? ` (settings: ${this.root.settingsPath})` : ""}`);
987
- const overrideSystemPrompt = roleDefinition?.overrideSystemPrompt === true;
988
- const contextFiles = options.contextFiles ?? roleDefinition?.contextFiles;
989
- return { model, ...(alias && requestedModel ? { requestedModel } : {}), tools: requested, ...(overrideSystemPrompt ? { systemPrompt: roleDefinition.prompt ?? "" } : {}), systemPromptAppend: overrideSystemPrompt ? "" : roleDefinition?.prompt ?? "", ...(contextFiles === undefined ? {} : { contextFiles: [...contextFiles] }) };
951
+ const resolved = resolveRole(roleName, {
952
+ cwd: this.root.cwd,
953
+ ...(roleDefinition === undefined ? {} : { definition: roleDefinition }),
954
+ ...(this.root.agentDefinitions === undefined ? {} : { definitions: this.root.agentDefinitions }),
955
+ ...(this.root.agentDir === undefined ? {} : { agentDir: this.root.agentDir }),
956
+ selectorSources: { global: this.root.resourceSelectors ?? {}, project: {} },
957
+ ...(this.root.modelAliases === undefined ? {} : { modelAliases: this.root.modelAliases }),
958
+ ...(this.root.knownModels === undefined ? {} : { knownModels: this.root.knownModels }),
959
+ ...(this.root.availableModels === undefined ? {} : { availableModels: this.root.availableModels }),
960
+ ...(this.root.blockedAliases === undefined ? {} : { blockedAliases: this.root.blockedAliases }),
961
+ ...(this.root.blockedAliasTargets === undefined ? {} : { blockedAliasTargets: this.root.blockedAliasTargets }),
962
+ ...(this.root.settingsPath === undefined ? {} : { settingsPath: this.root.settingsPath }),
963
+ rootModel: this.root.model,
964
+ rootTools: this.root.tools,
965
+ ...(inheritedTools === undefined ? {} : { inheritedTools }),
966
+ ...(options.model === undefined ? {} : { model: options.model }),
967
+ ...(options.modelOverride === undefined ? {} : { modelOverride: options.modelOverride }),
968
+ ...(options.tools === undefined ? {} : { tools: options.tools }),
969
+ ...(options.skills === undefined ? {} : { skills: options.skills }),
970
+ ...(options.extensions === undefined ? {} : { extensions: options.extensions }),
971
+ ...(options.contextFiles === undefined ? {} : { contextFiles: options.contextFiles }),
972
+ ...(options.effectiveTools === undefined ? {} : { effectiveTools: options.effectiveTools }),
973
+ });
974
+ return { model: resolved.model ?? this.root.model, ...(resolved.requestedModel === undefined ? {} : { requestedModel: resolved.requestedModel }), tools: resolved.tools ?? [], ...(resolved.overrideSystemPrompt ? { systemPrompt: resolved.prompt } : {}), systemPromptAppend: resolved.overrideSystemPrompt ? "" : resolved.prompt, ...(resolved.contextFiles === undefined ? {} : { contextFiles: resolved.contextFiles }) };
990
975
  }
991
976
  async execute(task, options, signal, customTools = [], setSteer, beforeRetry) {
992
977
  const executionSignal = signal ?? this.root.runContext?.signal;
@@ -22,6 +22,18 @@ export type BackgroundWidgetAPI = Pick<ExtensionAPI, "appendEntry" | "on"> & {
22
22
  registerEntryRenderer?: ExtensionAPI["registerEntryRenderer"];
23
23
  registerShortcut?: ExtensionAPI["registerShortcut"];
24
24
  };
25
+ /**
26
+ * How long an agent may go without an event before the widget says so.
27
+ *
28
+ * Deliberately shorter than the core's own stall threshold. That one answers
29
+ * "is this agent dead"; this one answers "should you go and look". An agent
30
+ * asking a question in its own pane produces exactly this signature — no tool
31
+ * calls, no events, waiting on a person — and the core cannot see it, since the
32
+ * pane is a separate process. Three minutes is longer than any single stretch
33
+ * of thinking and far shorter than the ten minutes it takes to be declared
34
+ * stalled.
35
+ */
36
+ export declare const QUIET_MS: number;
25
37
  /**
26
38
  * Overrides the switch above, for the tests that cover navigation.
27
39
  *
@@ -32,6 +44,11 @@ export type BackgroundWidgetAPI = Pick<ExtensionAPI, "appendEntry" | "on"> & {
32
44
  export declare const __navigationForTests: {
33
45
  enabled: boolean;
34
46
  };
47
+ /**
48
+ * Paints text in a themed colour role. Falls back to the bare text when no
49
+ * theme is available, so a frame rendered outside the TUI is still legible.
50
+ */
51
+ export type Paint = (role: Parameters<Theme["fg"]>[0], text: string) => string;
35
52
  interface ReceiptAgent {
36
53
  id?: string;
37
54
  parentId?: string;
@@ -62,8 +79,47 @@ interface Receipt {
62
79
  agents: readonly ReceiptAgent[];
63
80
  error?: string;
64
81
  }
82
+ /** Status mark for a finished-or-running unit of work. */
83
+ export declare function mark(state: string | undefined, now: number, paint: Paint, runState?: string): string;
84
+ export declare function formatElapsed(ms: number): string;
65
85
  export declare function formatTokens(tokens: number | undefined): string;
66
86
  export declare function formatCost(cost: number | undefined): string;
87
+ interface Row {
88
+ text: string;
89
+ /**
90
+ * Numbers to set against the right-hand rule rather than trailing the name.
91
+ *
92
+ * Names are ragged — a phase called `plan` beside one called `refactor-2` —
93
+ * so numbers that follow them are ragged too, and comparing the cost of one
94
+ * agent against another means reading across a gap that moves on every row.
95
+ * Held to the right edge, they form a column.
96
+ */
97
+ right?: string;
98
+ /** 2 = a run header or something needing an answer, 1 = live or failed work, 0 = finished detail. */
99
+ rank: number;
100
+ /**
101
+ * Set on the one row that stands for a whole run.
102
+ *
103
+ * Rank alone cannot protect it: a checkpoint needs an answer and so shares
104
+ * the top rank, and enough of them would spend the budget before a later
105
+ * run's header is reached. A run missing its header is not a terse run, it
106
+ * is an invisible one.
107
+ */
108
+ header?: boolean;
109
+ /**
110
+ * A stable name for a row that can be folded shut, and the key of the row it
111
+ * hangs from. Together they let the cursor walk a tree rather than a list:
112
+ * collapsing a phase hides its agents, collapsing a run hides everything
113
+ * under it, and the choice survives the repaint that follows.
114
+ */
115
+ key?: string;
116
+ parent?: string;
117
+ }
118
+ /**
119
+ * The box around the live rows, shared with the subagent widget so both sit
120
+ * under the editor in one style.
121
+ */
122
+ export declare function drawFrame(title: string, hint: string, body: readonly Pick<Row, "text" | "right">[], width: number, paint: Paint, accent: (text: string) => string, scrollbar?: (index: number) => string | undefined): string[];
67
123
  /** The receipt as transcript lines: the widget's tree, one level deeper. */
68
124
  export declare function renderReceipt(data: Receipt, expanded: boolean, theme: Theme): string[];
69
125
  export interface BackgroundWidgetController {
@@ -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))
@@ -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(" ")]
@@ -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;
@@ -93,6 +102,7 @@ export interface AgentDetailPresentation {
93
102
  readonly message: string;
94
103
  };
95
104
  }
105
+ export declare function formatAgentError(error: NonNullable<AgentDetailPresentation["error"]>, styles?: WorkflowProgressStyles): string;
96
106
  export declare function formatAgentDetail(agent: Readonly<AgentDetailPresentation>, styles?: WorkflowProgressStyles, now?: number, options?: Readonly<{
97
107
  includeError?: boolean;
98
108
  }>): string[];
@@ -113,4 +123,9 @@ export declare function formatNavigatorRun(loaded: {
113
123
  snapshot: Readonly<LaunchSnapshot>;
114
124
  }, checkpoints: readonly AwaitingCheckpoint[], worktrees: readonly WorktreeReference[], now?: number): string;
115
125
  export declare function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readonly<LaunchSnapshot>, width: number, selection?: WorkflowPhaseSelection, styles?: WorkflowProgressStyles, now?: number): string[];
126
+ /** The navigator layout: a sidebar beside details at 80 columns or wider, otherwise one of them (or both stacked). */
127
+ export declare function formatNavigatorColumns(sidebar: readonly string[], detail: readonly string[], width: number, layout?: Readonly<{
128
+ treeOnly?: boolean | undefined;
129
+ detailsOnly?: boolean | undefined;
130
+ }>): string[];
116
131
  export declare function formatCheckpointReview(checkpoint: AwaitingCheckpoint): string;