@yaag/tui 0.1.4 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaag/tui",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -19,6 +19,6 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@earendil-works/pi-tui": "^0.84.0",
22
- "@yaag/runtime": "0.1.4"
22
+ "@yaag/runtime": "0.2.1"
23
23
  }
24
24
  }
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ export {
28
28
  emptyDrill,
29
29
  } from "./drill-state.ts";
30
30
  export { durationText } from "./duration-text.ts";
31
+ export { type InlineRenderOptions, renderInlineRun } from "./inline-render.ts";
31
32
  export { type KeyBinding, type NamedKeybindings, routeKey } from "./key-router.ts";
32
33
  export {
33
34
  moveMenuCursor,
@@ -51,8 +52,6 @@ export {
51
52
  type RunTreeView,
52
53
  type RunTreeViewHost,
53
54
  type RunTreeViewOptions,
54
- type RunViewExit,
55
- type RunViewSurface,
56
55
  } from "./run-tree-view.ts";
57
56
  export { type RunViewResult, resultText, STDERR_TAIL_LINES } from "./run-view-result.ts";
58
57
  export {
@@ -119,8 +118,3 @@ export { TreeNavigator, type TreeNavigatorOptions } from "./tree-navigator.ts";
119
118
  export type { TreeNode, TreeNodeKind, TreeNodeState } from "./tree-node.ts";
120
119
  export { renderTree, type TreeRenderOptions } from "./tree-render.ts";
121
120
  export { TreeState, type TreeUpdate } from "./tree-state.ts";
122
- export {
123
- renderRunsWidget,
124
- type WidgetRenderOptions,
125
- type WidgetRun,
126
- } from "./widget-render.ts";
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The inline frame a blocking `yaag_run` draws while it waits (architecture §9).
3
+ *
4
+ * A pi `string[]` widget receives no input, so this frame is read-only by
5
+ * construction: it is the compact Run projection — one header line, then one
6
+ * line per Agent — capped to a line budget the Host Session can afford.
7
+ *
8
+ * Over the cap the frame keeps the Agents a reader is watching while the tool
9
+ * call blocks — the running ones first, then the latest to change state — and
10
+ * rolls the rest into one `… +N more` line.
11
+ */
12
+ import { compactAgentLine, compactHeaderLine } from "./compact-render.ts";
13
+ import { buildTree } from "./tree-model.ts";
14
+ import type { TreeNode } from "./tree-node.ts";
15
+ import { clamp } from "./tree-rows.ts";
16
+ import type { TreeState } from "./tree-state.ts";
17
+
18
+ /** Default line budget; pi truncates a widget past 10 lines. */
19
+ const DEFAULT_MAX_LINES = 10;
20
+
21
+ /** Render-time inputs; `now` keeps the renderer clock-free. */
22
+ export interface InlineRenderOptions {
23
+ readonly now: number;
24
+ readonly width: number;
25
+ /** Header text before the Program name; defaults to `yaag`. */
26
+ readonly label?: string;
27
+ /** Hard line budget for the whole frame; defaults to 10. */
28
+ readonly maxLines?: number;
29
+ }
30
+
31
+ /**
32
+ * Renders one Run into a frame that never exceeds `maxLines`.
33
+ *
34
+ * Pure over the given state, and every line is already width-clamped and
35
+ * sanitized. A budget of one draws the header alone; a budget under one is
36
+ * raised to one, so the frame always names the Run.
37
+ */
38
+ export function renderInlineRun(state: TreeState, options: InlineRenderOptions): readonly string[] {
39
+ const budget = Math.max(1, options.maxLines ?? DEFAULT_MAX_LINES);
40
+ const compact = {
41
+ now: options.now,
42
+ width: options.width,
43
+ ...(options.label === undefined ? {} : { label: options.label }),
44
+ };
45
+ const header = compactHeaderLine(state, compact);
46
+ const agents = buildTree(state, { now: options.now });
47
+ if (budget === 1) return [header];
48
+ if (agents.length + 1 <= budget)
49
+ return [header, ...agents.map((agent) => compactAgentLine(agent, options.width))];
50
+ const kept = keepWatched(agents, budget - 2);
51
+ const hidden = agents.length - kept.length;
52
+ return [
53
+ header,
54
+ ...kept.map((agent) => compactAgentLine(agent, options.width)),
55
+ clamp(` … +${hidden} more`, options.width),
56
+ ];
57
+ }
58
+
59
+ /**
60
+ * Picks the `count` Agents worth watching, drawn in tree order.
61
+ *
62
+ * A running Agent always outranks one that is idle, exited, or failed. Within
63
+ * one rank the later state change wins, and an Agent that never changed state
64
+ * sorts last. Ties keep the first-observed order, so the choice is
65
+ * deterministic for a given state.
66
+ */
67
+ function keepWatched(agents: readonly TreeNode[], count: number): readonly TreeNode[] {
68
+ if (count <= 0) return [];
69
+ const ranked = agents
70
+ .map((agent, index) => ({ agent, index }))
71
+ .sort(
72
+ (left, right) =>
73
+ rank(right.agent) - rank(left.agent) ||
74
+ changedAt(right.agent) - changedAt(left.agent) ||
75
+ left.index - right.index,
76
+ )
77
+ .slice(0, count)
78
+ .sort((left, right) => left.index - right.index);
79
+ return ranked.map((entry) => entry.agent);
80
+ }
81
+
82
+ function rank(agent: TreeNode): number {
83
+ return agent.state === "running" ? 1 : 0;
84
+ }
85
+
86
+ function changedAt(agent: TreeNode): number {
87
+ return agent.startedAt ?? Number.NEGATIVE_INFINITY;
88
+ }
@@ -1,23 +1,19 @@
1
1
  /**
2
- * The Run view (spec §4): the interactive tree a blocking `yaag_run` opens in
3
- * the Host Session, and the same component `/yaag` opens over a background
4
- * Run, plus the Run's lifecycle over it.
2
+ * The Run view (ADR-0008): the interactive tree `/yaag` opens over any Run of
3
+ * this session, plus the Run's lifecycle over it.
5
4
  *
6
5
  * It layers the session gestures over the read-only drill-in: the stop prompt
7
- * first while it is open, then the drill overlay or menu, then `q`/Ctrl-C, then
6
+ * first while it is open, then the drill overlay or menu, then `ctrl+q`, then
8
7
  * the tree. Two rules are load-bearing:
9
8
  *
10
- * - `esc` never stops the Run. It reaches the session layer through no path:
11
- * the session table is literal `q`/`ctrl+c` only (`session-keys.ts`), and the
12
- * drill layers own `esc` before it. In the foreground surface `esc` never
13
- * closes the view either; in the background surface `esc` at tree level
14
- * closes a *settled* view, because there is nothing left to watch and no tool
15
- * call to return. In a live background view `esc` still only backs out of
16
- * overlays and focus, and `q` opens the same three-way prompt, where
17
- * `Detach (keep running)` means "close the view, the Run keeps running".
18
- * - `ctrl+c` is best-effort. Pi binds `app.clear` to `ctrl+c` at app level, so a
19
- * Host Session may consume the byte before this component sees it; `q` is the
20
- * gesture to document to users.
9
+ * - `esc` never stops the Run, and always leaves one layer. The session table
10
+ * is literal `ctrl+q`/`ctrl+c` only (`session-keys.ts`), so `esc` reaches no
11
+ * stop path. An open overlay, menu, or stop prompt consumes `esc` first —
12
+ * one layer per press and at tree level `esc` closes the view, live or
13
+ * settled. A live Run keeps executing after that.
14
+ * - `ctrl+c` is a best-effort alias. Pi binds `app.clear` to `ctrl+c` at app
15
+ * level, so a Host Session may consume the byte before this component sees
16
+ * it; `ctrl+q` is the gesture to document to users.
21
17
  *
22
18
  * The view owns no clock and no I/O of its own: every capability, including the
23
19
  * reap-ladder stop, arrives through `RunTreeViewHost`.
@@ -39,32 +35,21 @@ import { renderTree } from "./tree-render.ts";
39
35
  import { clamp } from "./tree-rows.ts";
40
36
  import type { TreeState } from "./tree-state.ts";
41
37
 
42
- /** Which surface opened the view; it decides only the settled-`esc` rule. */
43
- export type RunViewSurface = "foreground" | "background";
38
+ // At tree level `esc` closes the view, live or settled, so the view replaces
39
+ // the renderer's own footer on every phase; `tree-render.ts` writes `esc back`,
40
+ // which is true only for the read-only frames that consume no key (ADR-0008).
41
+ const VIEW_FOOTER = " ↑↓ move ←→ fold ↵ actions t transcript esc close";
44
42
 
45
- const SETTLED_FOOTER_KEYS = " ↑↓ move ←→ fold ↵ actions t transcript ";
46
-
47
- // At tree level a settled background view closes on `esc`, so `esc back` would
48
- // contradict `esc close`: the surface picks exactly one of them (spec §4).
49
- function settledFooter(surface: RunViewSurface): string {
50
- return `${SETTLED_FOOTER_KEYS}${surface === "background" ? "esc close q done" : "esc back q done"}`;
51
- }
52
-
53
- /** How the reader left the view. */
54
- export type RunViewExit = "dismissed" | "detached";
55
-
56
- /** Every capability the foreground view needs from its host. */
43
+ /** Every capability the Run view needs from its host. */
57
44
  export interface RunTreeViewHost extends Omit<DrillHost, "navigator" | "dropFocus"> {
58
45
  /** Runs the reap ladder for this Run (ADR-0008). `esc` never reaches it. */
59
46
  stop(): void;
60
- /** Converts the Run to a background Run; the view then closes. */
61
- detach(): void;
62
- /** Resolves the `ctx.ui.custom()` promise with how the view ended. */
63
- done(exit: RunViewExit): void;
47
+ /** Resolves the `ctx.ui.custom()` promise; the view then closes. */
48
+ done(): void;
64
49
  }
65
50
 
66
51
  /**
67
- * What the foreground view needs to exist.
52
+ * What the view needs to exist.
68
53
  *
69
54
  * The caller owns the `TreeState`; the view only reads it, so a redraw never
70
55
  * changes the projection.
@@ -75,8 +60,6 @@ export interface RunTreeViewOptions {
75
60
  readonly host: RunTreeViewHost;
76
61
  /** The Run id, e.g. `r1`; heads the tree. */
77
62
  readonly label?: string;
78
- /** Which surface opened the view; defaults to `"foreground"`. */
79
- readonly surface?: RunViewSurface;
80
63
  now?(): number;
81
64
  }
82
65
 
@@ -91,8 +74,8 @@ export interface RunTreeView {
91
74
  /** A pushed fd 3 update landed in the TreeState; redraw. */
92
75
  touch(): void;
93
76
  /**
94
- * SIGINT (or the Run's AbortSignal) takes the same path as `q`: the first
95
- * one opens the stop prompt, a second confirms Stop Run (spec §4). While the
77
+ * SIGINT takes the same path as `ctrl+q`: the first one opens the stop
78
+ * prompt, a second confirms Stop Run (ADR-0008). While the
96
79
  * stop already runs, and on a settled view, it does nothing — a settled view
97
80
  * closes on a keystroke only.
98
81
  */
@@ -100,24 +83,23 @@ export interface RunTreeView {
100
83
  }
101
84
 
102
85
  /**
103
- * Builds the foreground Run view over a `TreeState` the caller owns.
86
+ * Builds the Run view over a `TreeState` the caller owns.
104
87
  *
105
- * `host.done` is called exactly once, on the reader's dismissal or detach; a
106
- * host that closes the view for its own reason (an abort) calls `done` itself.
88
+ * `host.done` is called exactly once, when the reader closes the view; a host
89
+ * that closes the view for its own reason (an abort) calls `done` itself.
107
90
  * Nothing here can prompt an Agent — the drill layer stays read-only, and the
108
- * only write capability is the Run's own `stop` (spec §4).
91
+ * only write capability is the Run's own `stop` (ADR-0008).
109
92
  */
110
93
  export function createRunTreeView(options: RunTreeViewOptions): RunTreeView {
111
94
  const host = options.host;
112
95
  const state = options.state;
113
96
  const now = options.now ?? Date.now;
114
- const surface: RunViewSurface = options.surface ?? "foreground";
115
97
  const navigator = new TreeNavigator({
116
98
  snapshot: () => buildTree(state, { now: now() }),
117
99
  keybindings: host.keybindings,
118
100
  });
119
- // `esc` at tree level backs out of nothing in the foreground: it must not
120
- // stop the Run and must not close the view (spec §4).
101
+ // The drill layers own `esc` while one of them is open; the tree level below
102
+ // them turns the same press into a close (ADR-0008).
121
103
  const drill = new DrillController({ ...host, navigator, dropFocus: () => {} });
122
104
  let phase: RunPhase = livePhase();
123
105
  let result: RunViewResult | undefined;
@@ -128,20 +110,16 @@ export function createRunTreeView(options: RunTreeViewOptions): RunTreeView {
128
110
  case "stop":
129
111
  host.stop();
130
112
  return;
131
- case "detach":
132
- finish("detached");
133
- return;
134
113
  case "close":
135
- finish("dismissed");
114
+ finish();
136
115
  return;
137
116
  }
138
117
  };
139
118
 
140
- const finish = (exit: RunViewExit): void => {
119
+ const finish = (): void => {
141
120
  if (exited) return;
142
121
  exited = true;
143
- if (exit === "detached") host.detach();
144
- host.done(exit);
122
+ host.done();
145
123
  };
146
124
 
147
125
  const apply = (action: Parameters<typeof applyRunViewAction>[1]): void => {
@@ -187,7 +165,7 @@ export function createRunTreeView(options: RunTreeViewOptions): RunTreeView {
187
165
  ...(result === undefined ? {} : { result: resultText(result) }),
188
166
  }),
189
167
  ];
190
- if (phase.kind === "settled") lines[lines.length - 1] = clamp(settledFooter(surface), width);
168
+ lines[lines.length - 1] = clamp(VIEW_FOOTER, width);
191
169
  const overlay = drill.overlayLines(width);
192
170
  const framed =
193
171
  overlay === undefined
@@ -207,12 +185,10 @@ export function createRunTreeView(options: RunTreeViewOptions): RunTreeView {
207
185
  apply({ kind: "quit" });
208
186
  return;
209
187
  }
210
- if (
211
- surface === "background" &&
212
- phase.kind === "settled" &&
213
- routeDrillKey(data, host.keybindings) === "back"
214
- ) {
215
- finish("dismissed");
188
+ // At tree level nothing is left to back out of, so `esc` closes the view;
189
+ // a live Run keeps executing without it (ADR-0008).
190
+ if (routeDrillKey(data, host.keybindings) === "back") {
191
+ finish();
216
192
  return;
217
193
  }
218
194
  drill.handleInput(data);
@@ -1,10 +1,11 @@
1
1
  /**
2
- * The foreground Run view's lifecycle reducer (spec §4): live → stop prompt →
2
+ * The Run view's lifecycle reducer (ADR-0008): live → stop prompt →
3
3
  * stopping → settled.
4
4
  *
5
- * Pure and I/O-free. `cancel` — the `esc` gesture — can only close the prompt:
6
- * no action reachable from it produces `stop` or `close`, so `esc` can never
7
- * stop a Run and can never dismiss the view.
5
+ * Pure and I/O-free. `cancel` — the `esc` gesture inside the prompt — can only
6
+ * close the prompt: no action reachable from it produces `stop`, so `esc` can
7
+ * never stop a Run. Closing the view is the view's own tree-level `esc`, which
8
+ * never reaches this reducer.
8
9
  */
9
10
  import { moveStopCursor, type StopChoice } from "./stop-prompt.ts";
10
11
 
@@ -27,7 +28,7 @@ export type RunViewAction =
27
28
  | { readonly kind: "settle"; readonly outcome: RunViewOutcome };
28
29
 
29
30
  /** What the host must do after a transition. */
30
- export type RunViewEffect = "stop" | "detach" | "close";
31
+ export type RunViewEffect = "stop" | "close";
31
32
 
32
33
  /** One transition: the next phase, plus the effect the host must perform. */
33
34
  export interface RunViewTransition {
@@ -81,8 +82,6 @@ function fromChoice(choice: StopChoice): RunViewTransition {
81
82
  switch (choice) {
82
83
  case "stop":
83
84
  return { phase: { kind: "stopping" }, effect: "stop" };
84
- case "detach":
85
- return { phase: livePhase(), effect: "detach" };
86
85
  case "resume":
87
86
  return { phase: livePhase() };
88
87
  }
@@ -1,27 +1,32 @@
1
1
  /**
2
- * The session layer's key table (spec §2): the one gesture that can end a
3
- * foreground Run view.
2
+ * The session layer's key table (ADR-0008): the one gesture that can stop a Run
3
+ * from the Run view.
4
4
  *
5
5
  * Literal keys only, with no `named` binding. `esc` must never stop a Run, and
6
6
  * pi binds `tui.select.cancel` to `escape, ctrl+c`; a named entry here would
7
7
  * let that binding — or any user remap of it — reach the quit path. The literal
8
8
  * table makes that structurally impossible.
9
+ *
10
+ * `ctrl+q` is the documented gesture. `ctrl+c` stays as a best-effort alias:
11
+ * pi binds `app.clear` to `ctrl+c` at app level, so a Host Session may consume
12
+ * the byte before this component sees it. Plain `q` is bound to nothing, so a
13
+ * reader who types `q` in the tree changes nothing (ADR-0008).
9
14
  */
10
15
  import { type KeyBinding, type NamedKeybindings, routeKey } from "./key-router.ts";
11
16
 
12
17
  /** The one gesture the session layer consumes. */
13
18
  export type SessionAction = "quit";
14
19
 
15
- /** `q` and Ctrl-C, and nothing else. */
20
+ /** Ctrl-Q and its best-effort Ctrl-C alias, and nothing else. */
16
21
  export const SESSION_KEY_TABLE: readonly KeyBinding<SessionAction>[] = [
17
- { action: "quit", keys: ["q", "ctrl+c"] },
22
+ { action: "quit", keys: ["ctrl+q", "ctrl+c"] },
18
23
  ];
19
24
 
20
25
  /**
21
26
  * Resolves one input byte string to the session `quit` gesture.
22
27
  *
23
28
  * Returns undefined for every other byte, `escape` included, so no gesture
24
- * routed here can stop a Run by accident (spec §4).
29
+ * routed here can stop a Run by accident (ADR-0008).
25
30
  */
26
31
  export function routeSessionKey(
27
32
  data: string,
@@ -1,3 +1,4 @@
1
+ import type { RunSummary } from "@yaag/runtime";
1
2
  import { compactAgentLine, compactHeaderLine } from "./compact-render.ts";
2
3
  import { renderNodeTable } from "./node-table.ts";
3
4
  import { sanitizeTerminalLine } from "./terminal-text.ts";
@@ -16,9 +17,10 @@ export interface SnapshotRenderOptions {
16
17
  }
17
18
 
18
19
  /**
19
- * Renders the model-facing snapshot frame: one Run header line, then one line
20
- * per Agent with that Agent's Nested Node table, then the Result block when
21
- * the Run settled with a value.
20
+ * Renders the model-facing snapshot frame: one Run header line, one warning
21
+ * line when the Run lost its Checkpoint, then one line per Agent with that
22
+ * Agent's Nested Node table, then the Result block when the Run settled with a
23
+ * value.
22
24
  *
23
25
  * It draws no key footer and reads no input, so every line is content a model
24
26
  * can parse. Pure over the state; every untrusted string is sanitized and
@@ -30,6 +32,8 @@ export function renderSnapshot(
30
32
  ): readonly string[] {
31
33
  const header = { now: options.now, width: options.width, ...labelOf(options) };
32
34
  const lines: string[] = [compactHeaderLine(state, header)];
35
+ const warning = checkpointLostLine(state.summary, options.width);
36
+ if (warning !== undefined) lines.push(warning);
33
37
  const agents = buildTree(state, { now: options.now });
34
38
  const names = state.agentOrder.filter((name) => state.summary.agents[name] !== undefined);
35
39
  agents.forEach((agent, index) => {
@@ -46,6 +50,15 @@ export function renderSnapshot(
46
50
  ];
47
51
  }
48
52
 
53
+ /**
54
+ * One warning line for a Run that asked for a Checkpoint and lost it. The Run's
55
+ * own error is reported elsewhere; this line reports only the missing artifact.
56
+ */
57
+ function checkpointLostLine(summary: RunSummary, width: number): string | undefined {
58
+ if (summary.runState !== "ended" || summary.checkpointLost === undefined) return undefined;
59
+ return clamp(sanitizeTerminalLine(`! checkpoint lost: ${summary.checkpointLost}`), width);
60
+ }
61
+
49
62
  function labelOf(options: SnapshotRenderOptions): { readonly label?: string } {
50
63
  return options.label === undefined ? {} : { label: options.label };
51
64
  }
@@ -1,15 +1,15 @@
1
1
  /**
2
- * The foreground Run view's stop prompt (spec §4): the three-way choice `q` and
3
- * Ctrl-C raise over a live Run.
2
+ * The Run view's stop prompt (ADR-0008): the two-way choice `ctrl+q` raises over
3
+ * a live Run.
4
4
  *
5
- * The spec's key table names no key for Detach, and the component consumes only
6
- * the keys of that table, so Detach lives here as a prompt choice rather than as
7
- * a new binding.
5
+ * Stopping a Run is the only write capability the view has, so it asks first.
6
+ * Leaving a Run running needs no prompt choice: `esc` closes the view and the
7
+ * Run keeps executing.
8
8
  */
9
9
  import { renderFramedBox } from "./overlay-frame.ts";
10
10
 
11
11
  /** What the reader may do with a live Run. */
12
- export type StopChoice = "stop" | "detach" | "resume";
12
+ export type StopChoice = "stop" | "resume";
13
13
 
14
14
  /** One prompt row: the choice and the label the reader sees. */
15
15
  export interface StopChoiceRow {
@@ -17,11 +17,10 @@ export interface StopChoiceRow {
17
17
  readonly label: string;
18
18
  }
19
19
 
20
- /** The three choices, in prompt order. */
20
+ /** The two choices, in prompt order. */
21
21
  export const STOP_PROMPT_CHOICES: readonly StopChoiceRow[] = [
22
22
  { action: "stop", label: "Stop Run" },
23
- { action: "detach", label: "Detach (keep running)" },
24
- { action: "resume", label: "Keep watching" },
23
+ { action: "resume", label: "Cancel" },
25
24
  ];
26
25
 
27
26
  const FOOTER = " ↑↓ choose ↵ confirm esc back";
@@ -1,88 +0,0 @@
1
- /**
2
- * The combined inline widget for every live background Run (spec §1).
3
- *
4
- * One frame holds every Run: a header line each, expanded to the compact
5
- * per-Agent lines while a Run has an Agent waiting on an Ask. Expansion is a
6
- * pure policy over the state, because a pi `string[]` widget receives no input
7
- * and there is no gesture to expand an entry with.
8
- */
9
- import { compactAgentLines, compactHeaderLine } from "./compact-render.ts";
10
- import { clamp } from "./tree-rows.ts";
11
- import type { TreeState } from "./tree-state.ts";
12
-
13
- /** Default line budget; pi truncates a widget past 10 lines. */
14
- const DEFAULT_MAX_LINES = 10;
15
-
16
- /** One Run in the widget: its Run id and the renderer projection it drives. */
17
- export interface WidgetRun {
18
- readonly id: string;
19
- readonly state: TreeState;
20
- }
21
-
22
- /** Render-time inputs; `now` keeps the renderer clock-free. */
23
- export interface WidgetRenderOptions {
24
- readonly now: number;
25
- readonly width: number;
26
- /** Hard line budget for the whole frame; defaults to 10. */
27
- readonly maxLines?: number;
28
- }
29
-
30
- interface Entry {
31
- readonly header: string;
32
- readonly agents: readonly string[];
33
- }
34
-
35
- /**
36
- * Renders every Run into one frame that never exceeds `maxLines`.
37
- *
38
- * Degrades deterministically: expanded entries collapse from the tail, then
39
- * trailing headers roll up into one `and N more Runs` line. Pure over the
40
- * given states, and every line is already width-clamped and sanitized.
41
- */
42
- export function renderRunsWidget(
43
- runs: readonly WidgetRun[],
44
- options: WidgetRenderOptions,
45
- ): readonly string[] {
46
- const budget = Math.max(1, options.maxLines ?? DEFAULT_MAX_LINES);
47
- if (runs.length === 0) return [];
48
- const entries = runs.map((run) => entryOf(run, options));
49
- let expanded = entries.map((entry) => entry.agents.length > 0);
50
- for (let index = entries.length - 1; index >= 0 && lineCount(entries, expanded) > budget; --index)
51
- expanded = expanded.map((value, at) => (at === index ? false : value));
52
- if (lineCount(entries, expanded) <= budget) return frame(entries, expanded);
53
- const kept = Math.max(0, budget - 1);
54
- const rest = entries.length - kept;
55
- return [
56
- ...entries.slice(0, kept).map((entry) => entry.header),
57
- clamp(` and ${rest} more Run${rest === 1 ? "" : "s"}`, options.width),
58
- ];
59
- }
60
-
61
- function entryOf(run: WidgetRun, options: WidgetRenderOptions): Entry {
62
- const compact = { now: options.now, width: options.width, label: run.id };
63
- return {
64
- header: compactHeaderLine(run.state, compact),
65
- agents: expandable(run.state) ? compactAgentLines(run.state, compact) : [],
66
- };
67
- }
68
-
69
- /** A Run is expanded while at least one of its Agents waits on an Ask. */
70
- function expandable(state: TreeState): boolean {
71
- return Object.values(state.summary.agents).some((agent) => agent.state === "asking");
72
- }
73
-
74
- function lineCount(entries: readonly Entry[], expanded: readonly boolean[]): number {
75
- return entries.reduce(
76
- (total, entry, index) => total + 1 + (expanded[index] === true ? entry.agents.length : 0),
77
- 0,
78
- );
79
- }
80
-
81
- function frame(entries: readonly Entry[], expanded: readonly boolean[]): readonly string[] {
82
- const lines: string[] = [];
83
- entries.forEach((entry, index) => {
84
- lines.push(entry.header);
85
- if (expanded[index] === true) lines.push(...entry.agents);
86
- });
87
- return lines;
88
- }