@yaag/tui 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaag/tui",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
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.9.0"
22
+ "@yaag/runtime": "0.11.0"
23
23
  }
24
24
  }
package/src/tree/index.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  * Files inside this directory import each other directly.
4
4
  */
5
5
  export { AGENT_ASK_LEDGER_MAX, type AskLedger, type AskRow } from "./ask-ledger.ts";
6
+ export { type FlatAgent, flattenAgents } from "./tree-agents.ts";
6
7
  export { moveSelection, nodeAt, nodeChain, parentOf, resolveSelection } from "./tree-cursor.ts";
7
8
  export {
8
9
  emptyFold,
@@ -0,0 +1,66 @@
1
+ import type { AgentInfo } from "@yaag/runtime";
2
+ import {
3
+ costText,
4
+ durationText,
5
+ modelText,
6
+ sanitizeTerminalLine,
7
+ tokensText,
8
+ } from "../text/index.ts";
9
+ import type { AgentTimers } from "./agent-timers.ts";
10
+
11
+ /** Everything one Agent row's facts are built from; `now` is already resolved. */
12
+ export interface AgentFactsOptions {
13
+ readonly agent: AgentInfo;
14
+ readonly settled: number;
15
+ readonly timers: AgentTimers;
16
+ /** The Agent's Parent Link resolved to a row in this tree (`lineageOf`). */
17
+ readonly parentShown: boolean;
18
+ }
19
+
20
+ /**
21
+ * The Agent row's facts. The headline fact keeps the Agent's state and both
22
+ * timers in one string, because the compact and inline frames render only the
23
+ * first fact. Fork and compaction facts stay short so the row still fits a
24
+ * narrow pane.
25
+ */
26
+ export function agentFacts(options: AgentFactsOptions): readonly string[] {
27
+ const { agent, settled, timers } = options;
28
+ const clocks = `active ${durationText(timers.activeMs)} · idle ${durationText(timers.idleMs)}`;
29
+ const facts: string[] = [];
30
+ if (agent.state === "asking") facts.push(`ask #${agent.askIndex + 1} · ${clocks}`);
31
+ else if (agent.state === "exited")
32
+ facts.push(`exited · ${settled} ask${settled === 1 ? "" : "s"} · ${clocks}`);
33
+ else facts.push(clocks);
34
+ facts.push(
35
+ `${costText(agent.cost, agent.incomplete)} · ${tokensText(agent.tokens?.total ?? null)}`,
36
+ );
37
+ if (agent.model !== null) facts.push(modelText(agent.model));
38
+ const last = agent.modelFallbacks.at(-1);
39
+ if (last !== undefined) {
40
+ facts.push(sanitizeTerminalLine(`fallback · ${last.failedModel} → ${last.resolvedModel}`));
41
+ }
42
+ const fork = forkFact(agent, options.parentShown);
43
+ if (fork !== null) facts.push(fork);
44
+ const compaction = compactionFact(agent);
45
+ if (compaction !== null) facts.push(compaction);
46
+ return facts;
47
+ }
48
+
49
+ /** Names the fork source only when its row is absent, so it is not repeated. */
50
+ function forkFact(agent: AgentInfo, parentShown: boolean): string | null {
51
+ if (agent.origin !== "fork") return null;
52
+ if (parentShown || agent.parent === null) return "fork";
53
+ return sanitizeTerminalLine(`fork of ${agent.parent}`);
54
+ }
55
+
56
+ /**
57
+ * Compaction spend is never summed here: `agent_exit` already includes it
58
+ * (ADR-0043), so the row states the count and the context sizes only.
59
+ */
60
+ function compactionFact(agent: AgentInfo): string | null {
61
+ if (agent.compactions <= 0) return null;
62
+ const count = `compacted ×${agent.compactions}`;
63
+ const last = agent.lastCompaction;
64
+ if (last === null || last.tokensBefore === null || last.tokensAfter === null) return count;
65
+ return `${count} · ${tokensText(last.tokensBefore)} → ${tokensText(last.tokensAfter)}`;
66
+ }
@@ -0,0 +1,21 @@
1
+ import type { TreeNode } from "./tree-node.ts";
2
+
3
+ /** One Agent node of a tree and how deep its Parent Link chain runs. */
4
+ export interface FlatAgent {
5
+ readonly agent: TreeNode;
6
+ readonly depth: number;
7
+ }
8
+
9
+ /**
10
+ * Every Agent node of a tree, depth-first, so a frame that draws one line per
11
+ * Agent cannot drop an Agent that a Parent Link nested (ADR-0042).
12
+ */
13
+ export function flattenAgents(nodes: readonly TreeNode[], depth = 0): readonly FlatAgent[] {
14
+ const flat: FlatAgent[] = [];
15
+ for (const node of nodes) {
16
+ if (node.kind !== "agent") continue;
17
+ flat.push({ agent: node, depth });
18
+ flat.push(...flattenAgents(node.children, depth + 1));
19
+ }
20
+ return flat;
21
+ }
@@ -18,11 +18,15 @@ export function emptyFold(): FoldState {
18
18
  * (spec §1). An Agent with no running Nested Node below it stays collapsed,
19
19
  * even when it is settled or asking. A user override for the node's path wins
20
20
  * over the default.
21
+ *
22
+ * A child Agent is not covered by this: a collapsed node still draws its
23
+ * agent-kind children, because a Parent Link places an Agent in the tree and
24
+ * must never hide it (ADR-0042). See `visibleRows`.
21
25
  */
22
26
  export function isExpanded(node: TreeNode, fold: FoldState): boolean {
23
27
  const override = fold.overrides.get(node.path);
24
28
  if (override !== undefined) return override;
25
- if (node.children.length === 0) return false;
29
+ if (!node.children.some(isFoldable)) return false;
26
30
  return leadsToRunningNested(node);
27
31
  }
28
32
 
@@ -39,8 +43,9 @@ export interface VisibleRow {
39
43
  /**
40
44
  * Flattens the tree into the rows the renderer draws, in depth-first order.
41
45
  *
42
- * A collapsed node contributes its own row only. The row order and the
43
- * `lastAtDepth` flags are a pure function of the tree and the fold state.
46
+ * A collapsed node contributes its own row and its child Agents, which a fold
47
+ * never hides. The row order and the `lastAtDepth` flags are a pure function of
48
+ * the tree and the fold state.
44
49
  */
45
50
  export function visibleRows(tree: readonly TreeNode[], fold: FoldState): readonly VisibleRow[] {
46
51
  const rows: VisibleRow[] = [];
@@ -63,15 +68,32 @@ function collect(
63
68
  node,
64
69
  depth,
65
70
  expanded,
66
- hasChildren: node.children.length > 0,
71
+ // A child Agent is always drawn, so it is no reason to offer a fold glyph.
72
+ hasChildren: node.children.some(isFoldable),
67
73
  lastAtDepth,
68
74
  });
69
- if (expanded) collect(node.children, fold, depth + 1, lastAtDepth, rows);
75
+ const drawn = expanded ? node.children : node.children.filter(isAgent);
76
+ collect(drawn, fold, depth + 1, lastAtDepth, rows);
70
77
  }
71
78
  }
72
79
 
80
+ function isAgent(node: TreeNode): boolean {
81
+ return node.kind === "agent";
82
+ }
83
+
84
+ function isFoldable(node: TreeNode): boolean {
85
+ return !isAgent(node);
86
+ }
87
+
88
+ /**
89
+ * Whether live nested work sits below this node, inside its own Agent.
90
+ *
91
+ * A child Agent is skipped: its rows are drawn whatever this node's fold says,
92
+ * so its running Nested Nodes are no reason to unfold this Agent's Ask rows.
93
+ */
73
94
  function leadsToRunningNested(node: TreeNode): boolean {
74
95
  for (const child of node.children) {
96
+ if (child.kind === "agent") continue;
75
97
  if (child.kind === "nested" && child.state === "running") return true;
76
98
  if (leadsToRunningNested(child)) return true;
77
99
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Groups Agents by their Parent Link, so the tree can draw a child under its
3
+ * parent.
4
+ *
5
+ * The input is observer data, which may be older, truncated, or hand-edited, so
6
+ * the grouping is defensive: a parent that names an unknown Agent makes its
7
+ * child a root, and so does a parent observed no earlier than its child. That
8
+ * one rule breaks every cycle, because a real Parent Link always names an Agent
9
+ * that spawned first. Order inside every level stays first-observed order.
10
+ */
11
+
12
+ /** One Run's Agent lineage, resolved to roots and child lists. */
13
+ export interface Lineage {
14
+ readonly roots: readonly string[];
15
+ children(name: string): readonly string[];
16
+ }
17
+
18
+ export function lineageOf(
19
+ order: readonly string[],
20
+ parentOf: (name: string) => string | null,
21
+ ): Lineage {
22
+ const position = new Map(order.map((name, index) => [name, index] as const));
23
+ const roots: string[] = [];
24
+ const children = new Map<string, string[]>();
25
+ for (const [index, name] of order.entries()) {
26
+ const parent = parentOf(name);
27
+ const parentIndex = parent === null ? undefined : position.get(parent);
28
+ if (parent === null || parentIndex === undefined || parentIndex >= index) {
29
+ roots.push(name);
30
+ continue;
31
+ }
32
+ const siblings = children.get(parent);
33
+ if (siblings === undefined) children.set(parent, [name]);
34
+ else siblings.push(name);
35
+ }
36
+ return {
37
+ roots,
38
+ children: (name: string): readonly string[] => children.get(name) ?? [],
39
+ };
40
+ }
@@ -1,14 +1,9 @@
1
1
  import type { AgentInfo } from "@yaag/runtime";
2
- import {
3
- activityText,
4
- costText,
5
- durationText,
6
- modelText,
7
- sanitizeTerminalLine,
8
- tokensText,
9
- } from "../text/index.ts";
10
- import { type AgentTimers, agentTimers } from "./agent-timers.ts";
2
+ import { activityText, durationText, modelText, sanitizeTerminalLine } from "../text/index.ts";
3
+ import { agentTimers } from "./agent-timers.ts";
11
4
  import { type AskRow, askDwellMs } from "./ask-ledger.ts";
5
+ import { agentFacts } from "./tree-agent-facts.ts";
6
+ import { type Lineage, lineageOf } from "./tree-lineage.ts";
12
7
  import { graftNestedNodes } from "./tree-nested.ts";
13
8
  import type { TreeNode } from "./tree-node.ts";
14
9
  import type { TreeState } from "./tree-state.ts";
@@ -22,21 +17,59 @@ export interface TreeModelOptions {
22
17
  * Builds the Run's node tree from the folded state.
23
18
  *
24
19
  * Pure: the same state and options always build the same tree. Agents come in
25
- * first-observed order, each Agent's children are its Ask rows, and each Ask's
20
+ * first-observed order, an Agent with a Parent Link sits under its parent, each
21
+ * Agent's children are its Ask rows then its child Agents, and each Ask's
26
22
  * children are the Nested Nodes grafted under it by path. Pruned rows become
27
23
  * one trailing "and N more finished" roll-up node.
28
24
  */
29
25
  export function buildTree(state: TreeState, options: TreeModelOptions): readonly TreeNode[] {
30
- const nodes: TreeNode[] = [];
26
+ // Resolved once, so every later step reads an Agent that is known to exist.
27
+ const known = new Map<string, AgentInfo>();
31
28
  for (const name of state.agentOrder) {
32
29
  const agent = state.summary.agents[name];
30
+ if (agent !== undefined) known.set(name, agent);
31
+ }
32
+ const lineage = lineageOf([...known.keys()], (name) => state.parentLinkOf(name));
33
+ return subtrees({ state, known, lineage, now: options.now }, lineage.roots, false);
34
+ }
35
+
36
+ /** What every level of the recursion shares; only the names and depth change. */
37
+ interface BuildContext {
38
+ readonly state: TreeState;
39
+ readonly known: ReadonlyMap<string, AgentInfo>;
40
+ readonly lineage: Lineage;
41
+ readonly now: number;
42
+ }
43
+
44
+ function subtrees(
45
+ context: BuildContext,
46
+ names: readonly string[],
47
+ parentShown: boolean,
48
+ ): readonly TreeNode[] {
49
+ const nodes: TreeNode[] = [];
50
+ for (const name of names) {
51
+ const agent = context.known.get(name);
33
52
  if (agent === undefined) continue;
34
- nodes.push(agentNode(state, name, agent, options.now));
53
+ const children = subtrees(context, context.lineage.children(name), true);
54
+ nodes.push(agentNode(context, { name, agent, parentShown }, children));
35
55
  }
36
56
  return nodes;
37
57
  }
38
58
 
39
- function agentNode(state: TreeState, name: string, agent: AgentInfo, now: number): TreeNode {
59
+ /** The Agent this node is for, plus whether `lineageOf` gave its parent a row. */
60
+ interface AgentSlot {
61
+ readonly name: string;
62
+ readonly agent: AgentInfo;
63
+ readonly parentShown: boolean;
64
+ }
65
+
66
+ function agentNode(
67
+ context: BuildContext,
68
+ slot: AgentSlot,
69
+ childAgents: readonly TreeNode[],
70
+ ): TreeNode {
71
+ const { state, now } = context;
72
+ const { name, agent } = slot;
40
73
  const ledger = state.ledger(name);
41
74
  const nested = graftNestedNodes(name, agent.nodes, now);
42
75
  const gist = agent.activity === null ? null : activityText(agent.activity);
@@ -48,6 +81,7 @@ function agentNode(state: TreeState, name: string, agent: AgentInfo, now: number
48
81
  agentModel: agent.model,
49
82
  }),
50
83
  );
84
+ children.push(...childAgents);
51
85
  const finished = agent.finishedNodesPruned + ledger.settledPruned;
52
86
  if (finished > 0) children.push(rollup(`${name}#pruned`, `and ${finished} more finished`));
53
87
  return {
@@ -55,11 +89,12 @@ function agentNode(state: TreeState, name: string, agent: AgentInfo, now: number
55
89
  kind: "agent",
56
90
  label: sanitizeTerminalLine(name),
57
91
  state: agentState(agent),
58
- facts: agentFacts(
92
+ facts: agentFacts({
59
93
  agent,
60
- state.settledAsks(name),
61
- agentTimers({ agent, ledger, spawnedAt: state.spawnedAt(name), now }),
62
- ),
94
+ settled: state.settledAsks(name),
95
+ timers: agentTimers({ agent, ledger, spawnedAt: state.spawnedAt(name), now }),
96
+ parentShown: slot.parentShown,
97
+ }),
63
98
  children,
64
99
  activityGist: gist,
65
100
  startedAt: agent.stateChangedAt,
@@ -111,26 +146,3 @@ function agentState(agent: AgentInfo): TreeNode["state"] {
111
146
  if (agent.state === "exited") return "exited";
112
147
  return agent.state === "asking" ? "running" : "idle";
113
148
  }
114
-
115
- /**
116
- * The Agent row's facts. The headline fact keeps the Agent's state and both
117
- * timers in one string, because the compact and inline frames render only the
118
- * first fact.
119
- */
120
- function agentFacts(agent: AgentInfo, settled: number, timers: AgentTimers): readonly string[] {
121
- const clocks = `active ${durationText(timers.activeMs)} · idle ${durationText(timers.idleMs)}`;
122
- const facts: string[] = [];
123
- if (agent.state === "asking") facts.push(`ask #${agent.askIndex + 1} · ${clocks}`);
124
- else if (agent.state === "exited")
125
- facts.push(`exited · ${settled} ask${settled === 1 ? "" : "s"} · ${clocks}`);
126
- else facts.push(clocks);
127
- facts.push(
128
- `${costText(agent.cost, agent.incomplete)} · ${tokensText(agent.tokens?.total ?? null)}`,
129
- );
130
- if (agent.model !== null) facts.push(modelText(agent.model));
131
- const last = agent.modelFallbacks.at(-1);
132
- if (last !== undefined) {
133
- facts.push(sanitizeTerminalLine(`fallback · ${last.failedModel} → ${last.resolvedModel}`));
134
- }
135
- return facts;
136
- }
@@ -141,6 +141,11 @@ export class TreeState {
141
141
  return this.#spawnedAt.get(agent) ?? this.#summary.startedAt ?? null;
142
142
  }
143
143
 
144
+ /** The Agent named as this one's parent (Parent Link), or null for a root Agent. */
145
+ parentLinkOf(agent: string): string | null {
146
+ return this.#summary.agents[agent]?.parent ?? null;
147
+ }
148
+
144
149
  /** The bounded, sanitized two-line output tail for an Agent's live Ask. */
145
150
  outputTail(agent: string): readonly string[] {
146
151
  const tail = this.#tails.get(agent);
@@ -1,6 +1,6 @@
1
1
  import { clampToWidth, costText, durationText, sanitizeTerminalLine } from "../text/index.ts";
2
2
  import type { TreeNode, TreeState } from "../tree/index.ts";
3
- import { buildTree, GLYPHS, stateGlyph } from "../tree/index.ts";
3
+ import { buildTree, flattenAgents, GLYPHS, stateGlyph } from "../tree/index.ts";
4
4
 
5
5
  /** Everything the compact background view needs; `now` keeps it clock-free. */
6
6
  export interface CompactRenderOptions {
@@ -38,19 +38,22 @@ export function compactHeaderLine(state: TreeState, options: CompactRenderOption
38
38
  );
39
39
  }
40
40
 
41
- /** One line per Agent, with a one-line nested-work gist for each live Agent. */
41
+ /**
42
+ * One line per Agent, with a one-line nested-work gist for each live Agent. A
43
+ * child Agent keeps its own line, indented one level per Parent Link.
44
+ */
42
45
  export function compactAgentLines(
43
46
  state: TreeState,
44
47
  options: CompactRenderOptions,
45
48
  ): readonly string[] {
46
- return buildTree(state, { now: options.now }).map((agent) =>
47
- compactAgentLine(agent, options.width),
49
+ return flattenAgents(buildTree(state, { now: options.now })).map((entry) =>
50
+ compactAgentLine(entry.agent, options.width, entry.depth),
48
51
  );
49
52
  }
50
53
 
51
54
  /** The one Agent line of the compact view, drawn for one Agent node. */
52
- export function compactAgentLine(agent: TreeNode, width: number): string {
53
- return clampToWidth(agentLine(agent), width);
55
+ export function compactAgentLine(agent: TreeNode, width: number, depth = 0): string {
56
+ return clampToWidth(`${" ".repeat(depth)}${agentLine(agent)}`, width);
54
57
  }
55
58
 
56
59
  function agentLine(agent: TreeNode): string {
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { clampToWidth } from "../text/index.ts";
14
14
  import type { TreeNode, TreeState } from "../tree/index.ts";
15
- import { buildTree } from "../tree/index.ts";
15
+ import { buildTree, type FlatAgent, flattenAgents } from "../tree/index.ts";
16
16
  import { compactAgentLine, compactHeaderLine } from "./compact-render.ts";
17
17
 
18
18
  /** Default line budget; pi truncates a widget past 10 lines. */
@@ -43,15 +43,19 @@ export function renderInlineRun(state: TreeState, options: InlineRenderOptions):
43
43
  ...(options.label === undefined ? {} : { label: options.label }),
44
44
  };
45
45
  const header = compactHeaderLine(state, compact);
46
- const agents = buildTree(state, { now: options.now });
46
+ // Flattened first, so a busy child Agent competes for the budget like any other.
47
+ const agents = flattenAgents(buildTree(state, { now: options.now }));
47
48
  if (budget === 1) return [header];
48
49
  if (agents.length + 1 <= budget)
49
- return [header, ...agents.map((agent) => compactAgentLine(agent, options.width))];
50
+ return [
51
+ header,
52
+ ...agents.map((entry) => compactAgentLine(entry.agent, options.width, entry.depth)),
53
+ ];
50
54
  const kept = keepWatched(agents, budget - 2);
51
55
  const hidden = agents.length - kept.length;
52
56
  return [
53
57
  header,
54
- ...kept.map((agent) => compactAgentLine(agent, options.width)),
58
+ ...kept.map((entry) => compactAgentLine(entry.agent, options.width, entry.depth)),
55
59
  clampToWidth(` … +${hidden} more`, options.width),
56
60
  ];
57
61
  }
@@ -64,19 +68,19 @@ export function renderInlineRun(state: TreeState, options: InlineRenderOptions):
64
68
  * sorts last. Ties keep the first-observed order, so the choice is
65
69
  * deterministic for a given state.
66
70
  */
67
- function keepWatched(agents: readonly TreeNode[], count: number): readonly TreeNode[] {
71
+ function keepWatched(agents: readonly FlatAgent[], count: number): readonly FlatAgent[] {
68
72
  if (count <= 0) return [];
69
73
  const ranked = agents
70
- .map((agent, index) => ({ agent, index }))
74
+ .map((entry, index) => ({ entry, index }))
71
75
  .sort(
72
76
  (left, right) =>
73
- rank(right.agent) - rank(left.agent) ||
74
- changedAt(right.agent) - changedAt(left.agent) ||
77
+ rank(right.entry.agent) - rank(left.entry.agent) ||
78
+ changedAt(right.entry.agent) - changedAt(left.entry.agent) ||
75
79
  left.index - right.index,
76
80
  )
77
81
  .slice(0, count)
78
82
  .sort((left, right) => left.index - right.index);
79
- return ranked.map((entry) => entry.agent);
83
+ return ranked.map((keeper) => keeper.entry);
80
84
  }
81
85
 
82
86
  function rank(agent: TreeNode): number {
@@ -2,7 +2,7 @@ import type { RunSummary } from "@yaag/runtime";
2
2
  import { renderNodeTable } from "../node/index.ts";
3
3
  import { clampToWidth, sanitizeTerminalLine } from "../text/index.ts";
4
4
  import type { TreeState } from "../tree/index.ts";
5
- import { buildTree } from "../tree/index.ts";
5
+ import { buildTree, flattenAgents } from "../tree/index.ts";
6
6
  import { compactAgentLine, compactHeaderLine } from "./compact-render.ts";
7
7
 
8
8
  /** Everything the model-facing snapshot needs; `now` keeps it clock-free. */
@@ -33,13 +33,21 @@ export function renderSnapshot(
33
33
  const lines: string[] = [compactHeaderLine(state, header)];
34
34
  const warning = checkpointLostLine(state.summary, options.width);
35
35
  if (warning !== undefined) lines.push(warning);
36
- const agents = buildTree(state, { now: options.now });
37
- const names = state.agentOrder.filter((name) => state.summary.agents[name] !== undefined);
38
- agents.forEach((agent, index) => {
39
- lines.push(compactAgentLine(agent, options.width));
40
- const info = state.summary.agents[names[index] ?? ""];
41
- if (info !== undefined) lines.push(...renderNodeTable(info, options.width));
42
- });
36
+ // Each Agent line is paired with the Summary by node path, so nesting cannot
37
+ // shift a Nested Node table under the wrong Agent.
38
+ for (const { agent, depth } of flattenAgents(buildTree(state, { now: options.now }))) {
39
+ lines.push(compactAgentLine(agent, options.width, depth));
40
+ const info = state.summary.agents[agent.path];
41
+ // The table is indented with its Agent, so it stays visibly that Agent's.
42
+ // It is drawn into the width the indent leaves, so no line outgrows `width`.
43
+ if (info !== undefined) {
44
+ lines.push(
45
+ ...indent(renderNodeTable(info, tableWidth(options.width, depth)), depth).map((line) =>
46
+ clampToWidth(line, options.width),
47
+ ),
48
+ );
49
+ }
50
+ }
43
51
  if (options.result === undefined) return lines;
44
52
  return [
45
53
  ...lines,
@@ -60,6 +68,22 @@ function checkpointLostLine(summary: RunSummary, width: number): string | undefi
60
68
  return clampToWidth(sanitizeTerminalLine(`! checkpoint lost: ${summary.checkpointLost}`), width);
61
69
  }
62
70
 
71
+ /** Columns one indent step takes from a nested Agent's rows. */
72
+ const INDENT = 2;
73
+
74
+ /** The narrowest table a deep nesting may shrink to, so a row keeps its shape. */
75
+ const MIN_TABLE_WIDTH = 8;
76
+
77
+ function tableWidth(width: number, depth: number): number {
78
+ return Math.max(MIN_TABLE_WIDTH, width - INDENT * depth);
79
+ }
80
+
81
+ function indent(lines: readonly string[], depth: number): readonly string[] {
82
+ if (depth === 0) return lines;
83
+ const prefix = " ".repeat(INDENT * depth);
84
+ return lines.map((line) => (line === "" ? line : `${prefix}${line}`));
85
+ }
86
+
63
87
  function labelOf(options: SnapshotRenderOptions): { readonly label?: string } {
64
88
  return options.label === undefined ? {} : { label: options.label };
65
89
  }