@yaag/tui 0.9.0 → 0.10.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 +2 -2
- package/src/tree/index.ts +1 -0
- package/src/tree/tree-agents.ts +21 -0
- package/src/tree/tree-fold.ts +27 -5
- package/src/tree/tree-lineage.ts +40 -0
- package/src/tree/tree-model.ts +31 -4
- package/src/tree/tree-state.ts +5 -0
- package/src/view/compact-render.ts +9 -6
- package/src/view/inline-render.ts +13 -9
- package/src/view/snapshot-render.ts +32 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yaag/tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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.
|
|
22
|
+
"@yaag/runtime": "0.10.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,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
|
+
}
|
package/src/tree/tree-fold.ts
CHANGED
|
@@ -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.
|
|
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
|
|
43
|
-
* `lastAtDepth` flags are a pure function of
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/tree/tree-model.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "../text/index.ts";
|
|
10
10
|
import { type AgentTimers, agentTimers } from "./agent-timers.ts";
|
|
11
11
|
import { type AskRow, askDwellMs } from "./ask-ledger.ts";
|
|
12
|
+
import { type Lineage, lineageOf } from "./tree-lineage.ts";
|
|
12
13
|
import { graftNestedNodes } from "./tree-nested.ts";
|
|
13
14
|
import type { TreeNode } from "./tree-node.ts";
|
|
14
15
|
import type { TreeState } from "./tree-state.ts";
|
|
@@ -22,21 +23,46 @@ export interface TreeModelOptions {
|
|
|
22
23
|
* Builds the Run's node tree from the folded state.
|
|
23
24
|
*
|
|
24
25
|
* Pure: the same state and options always build the same tree. Agents come in
|
|
25
|
-
* first-observed order,
|
|
26
|
+
* first-observed order, an Agent with a Parent Link sits under its parent, each
|
|
27
|
+
* Agent's children are its Ask rows then its child Agents, and each Ask's
|
|
26
28
|
* children are the Nested Nodes grafted under it by path. Pruned rows become
|
|
27
29
|
* one trailing "and N more finished" roll-up node.
|
|
28
30
|
*/
|
|
29
31
|
export function buildTree(state: TreeState, options: TreeModelOptions): readonly TreeNode[] {
|
|
30
|
-
|
|
32
|
+
// Resolved once, so every later step reads an Agent that is known to exist.
|
|
33
|
+
const known = new Map<string, AgentInfo>();
|
|
31
34
|
for (const name of state.agentOrder) {
|
|
32
35
|
const agent = state.summary.agents[name];
|
|
36
|
+
if (agent !== undefined) known.set(name, agent);
|
|
37
|
+
}
|
|
38
|
+
const lineage = lineageOf([...known.keys()], (name) => state.parentLinkOf(name));
|
|
39
|
+
return subtrees(state, lineage.roots, known, lineage, options.now);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function subtrees(
|
|
43
|
+
state: TreeState,
|
|
44
|
+
names: readonly string[],
|
|
45
|
+
known: ReadonlyMap<string, AgentInfo>,
|
|
46
|
+
lineage: Lineage,
|
|
47
|
+
now: number,
|
|
48
|
+
): readonly TreeNode[] {
|
|
49
|
+
const nodes: TreeNode[] = [];
|
|
50
|
+
for (const name of names) {
|
|
51
|
+
const agent = known.get(name);
|
|
33
52
|
if (agent === undefined) continue;
|
|
34
|
-
|
|
53
|
+
const children = subtrees(state, lineage.children(name), known, lineage, now);
|
|
54
|
+
nodes.push(agentNode(state, name, agent, now, children));
|
|
35
55
|
}
|
|
36
56
|
return nodes;
|
|
37
57
|
}
|
|
38
58
|
|
|
39
|
-
function agentNode(
|
|
59
|
+
function agentNode(
|
|
60
|
+
state: TreeState,
|
|
61
|
+
name: string,
|
|
62
|
+
agent: AgentInfo,
|
|
63
|
+
now: number,
|
|
64
|
+
childAgents: readonly TreeNode[],
|
|
65
|
+
): TreeNode {
|
|
40
66
|
const ledger = state.ledger(name);
|
|
41
67
|
const nested = graftNestedNodes(name, agent.nodes, now);
|
|
42
68
|
const gist = agent.activity === null ? null : activityText(agent.activity);
|
|
@@ -48,6 +74,7 @@ function agentNode(state: TreeState, name: string, agent: AgentInfo, now: number
|
|
|
48
74
|
agentModel: agent.model,
|
|
49
75
|
}),
|
|
50
76
|
);
|
|
77
|
+
children.push(...childAgents);
|
|
51
78
|
const finished = agent.finishedNodesPruned + ledger.settledPruned;
|
|
52
79
|
if (finished > 0) children.push(rollup(`${name}#pruned`, `and ${finished} more finished`));
|
|
53
80
|
return {
|
package/src/tree/tree-state.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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((
|
|
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)
|
|
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
|
-
|
|
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 [
|
|
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((
|
|
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
|
|
71
|
+
function keepWatched(agents: readonly FlatAgent[], count: number): readonly FlatAgent[] {
|
|
68
72
|
if (count <= 0) return [];
|
|
69
73
|
const ranked = agents
|
|
70
|
-
.map((
|
|
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((
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
lines.push(compactAgentLine(agent, options.width));
|
|
40
|
-
const info = state.summary.agents[
|
|
41
|
-
|
|
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
|
}
|