@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 +2 -2
- package/src/index.ts +1 -7
- package/src/inline-render.ts +88 -0
- package/src/run-tree-view.ts +35 -59
- package/src/run-view-state.ts +6 -7
- package/src/session-keys.ts +10 -5
- package/src/snapshot-render.ts +16 -3
- package/src/stop-prompt.ts +8 -9
- package/src/widget-render.ts +0 -88
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yaag/tui",
|
|
3
|
-
"version": "0.1
|
|
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
|
|
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
|
+
}
|
package/src/run-tree-view.ts
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Run view (
|
|
3
|
-
*
|
|
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
|
|
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
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
61
|
-
|
|
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
|
|
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
|
|
95
|
-
*
|
|
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
|
|
86
|
+
* Builds the Run view over a `TreeState` the caller owns.
|
|
104
87
|
*
|
|
105
|
-
* `host.done` is called exactly once,
|
|
106
|
-
*
|
|
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` (
|
|
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`
|
|
120
|
-
//
|
|
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(
|
|
114
|
+
finish();
|
|
136
115
|
return;
|
|
137
116
|
}
|
|
138
117
|
};
|
|
139
118
|
|
|
140
|
-
const finish = (
|
|
119
|
+
const finish = (): void => {
|
|
141
120
|
if (exited) return;
|
|
142
121
|
exited = true;
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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);
|
package/src/run-view-state.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
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
|
|
6
|
-
* no action reachable from it produces `stop
|
|
7
|
-
* stop a Run
|
|
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" | "
|
|
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
|
}
|
package/src/session-keys.ts
CHANGED
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The session layer's key table (
|
|
3
|
-
*
|
|
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
|
-
/**
|
|
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 (
|
|
29
|
+
* routed here can stop a Run by accident (ADR-0008).
|
|
25
30
|
*/
|
|
26
31
|
export function routeSessionKey(
|
|
27
32
|
data: string,
|
package/src/snapshot-render.ts
CHANGED
|
@@ -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,
|
|
20
|
-
*
|
|
21
|
-
* the Run settled with a
|
|
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
|
}
|
package/src/stop-prompt.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
3
|
-
*
|
|
2
|
+
* The Run view's stop prompt (ADR-0008): the two-way choice `ctrl+q` raises over
|
|
3
|
+
* a live Run.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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" | "
|
|
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
|
|
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: "
|
|
24
|
-
{ action: "resume", label: "Keep watching" },
|
|
23
|
+
{ action: "resume", label: "Cancel" },
|
|
25
24
|
];
|
|
26
25
|
|
|
27
26
|
const FOOTER = " ↑↓ choose ↵ confirm esc back";
|
package/src/widget-render.ts
DELETED
|
@@ -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
|
-
}
|