pi-legion 2.0.3 → 2.0.4
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/CHANGELOG.md +23 -1
- package/index.ts +83 -15
- package/package.json +1 -1
- package/src/extension/legion-commands.ts +8 -3
- package/src/extension/legion-doctor.ts +1 -0
- package/src/extension/playbook.ts +2 -2
- package/src/extension/timing.ts +391 -0
- package/src/shared/timing-breakdown.ts +23 -0
- package/src/tui/live-overlay.ts +202 -21
- package/src/tui/viz/layout.ts +50 -6
- package/src/tui/viz/panels.ts +156 -3
- package/src/tui/viz/rate.ts +21 -0
- package/src/tui/viz/view.ts +69 -10
package/src/tui/viz/view.ts
CHANGED
|
@@ -1,26 +1,39 @@
|
|
|
1
|
-
/** /legion full view: galaxy + tok/s chart + git-log history with linked selection. */
|
|
1
|
+
/** /legion full view: galaxy + tok/s chart + timing + git-log history with linked selection. */
|
|
2
2
|
|
|
3
3
|
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
4
4
|
import { Canvas, cellWidth, ellipsize } from "./canvas.ts";
|
|
5
5
|
import { computeLayout, type LayoutKind } from "./layout.ts";
|
|
6
6
|
import { formatCount, formatDuration, isAgent, type VizNode } from "./model.ts";
|
|
7
7
|
import { roleColor, type VizTheme } from "./palette.ts";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
buildLog,
|
|
10
|
+
dfsOrder,
|
|
11
|
+
drawChart,
|
|
12
|
+
drawGalaxy,
|
|
13
|
+
drawLog,
|
|
14
|
+
drawTiming,
|
|
15
|
+
frame,
|
|
16
|
+
timingRowsFromNodes,
|
|
17
|
+
type Pane,
|
|
18
|
+
type ViewData,
|
|
19
|
+
} from "./panels.ts";
|
|
9
20
|
|
|
10
21
|
export interface LegionViewState {
|
|
11
22
|
focus: Pane;
|
|
12
23
|
selectedId?: string;
|
|
13
24
|
logOffset: number;
|
|
14
25
|
logCursor: number;
|
|
26
|
+
timingOffset: number;
|
|
27
|
+
timingCursor: number;
|
|
15
28
|
/** `x` pressed: an in-view y/N prompt for stopping this agent (dialogs would be hidden behind the full-screen view). */
|
|
16
29
|
pendingStopId?: string;
|
|
17
30
|
}
|
|
18
31
|
|
|
19
32
|
export function initialViewState(): LegionViewState {
|
|
20
|
-
return { focus: "galaxy", logOffset: 0, logCursor: 0 };
|
|
33
|
+
return { focus: "galaxy", logOffset: 0, logCursor: 0, timingOffset: 0, timingCursor: 0 };
|
|
21
34
|
}
|
|
22
35
|
|
|
23
|
-
const PANES: Pane[] = ["galaxy", "chart", "log"];
|
|
36
|
+
const PANES: Pane[] = ["galaxy", "chart", "timing", "log"];
|
|
24
37
|
|
|
25
38
|
/** Min grapheme cells before painting a selection badge. Shared by portrait + landscape so a truncated stub never paints (#128). Was portrait `>4` vs landscape `>0`. */
|
|
26
39
|
export const MIN_SELECTION_BADGE_W = 5;
|
|
@@ -72,8 +85,26 @@ export function renderLegionView(data: ViewData, state: LegionViewState, cols: n
|
|
|
72
85
|
drawGalaxy(cv, galaxyInner, data, state.selectedId);
|
|
73
86
|
const chartInner = frame(cv, layout.chart, portrait ? "tok/s · by role" : "tok/s · stacked by role", state.focus === "chart");
|
|
74
87
|
drawChart(cv, chartInner, data, selected, !portrait);
|
|
88
|
+
|
|
89
|
+
const timingRows = timingRowsFromNodes(data.nodes, data.now);
|
|
90
|
+
const timingVisible = Math.max(1, layout.timing.h - 3);
|
|
91
|
+
clampList(state, "timing", timingRows.length, timingVisible);
|
|
92
|
+
const timingInner = frame(
|
|
93
|
+
cv,
|
|
94
|
+
layout.timing,
|
|
95
|
+
portrait ? "timing" : "timing · wall/gen/other",
|
|
96
|
+
state.focus === "timing",
|
|
97
|
+
portrait ? undefined : "↑↓ · ⏎ sel · o open",
|
|
98
|
+
);
|
|
99
|
+
drawTiming(cv, timingInner, timingRows, {
|
|
100
|
+
offset: state.timingOffset,
|
|
101
|
+
cursor: state.timingCursor,
|
|
102
|
+
focused: state.focus === "timing",
|
|
103
|
+
...(state.selectedId ? { selectedId: state.selectedId } : {}),
|
|
104
|
+
});
|
|
105
|
+
|
|
75
106
|
const events = buildLog(data.nodes);
|
|
76
|
-
|
|
107
|
+
clampList(state, "log", events.length, Math.max(1, layout.log.h - 3));
|
|
77
108
|
const logInner = frame(cv, layout.log, "log --graph", state.focus === "log", portrait ? undefined : "↑↓ scroll · ⏎ select");
|
|
78
109
|
drawLog(cv, logInner, events, {
|
|
79
110
|
offset: state.logOffset,
|
|
@@ -112,11 +143,23 @@ function statusLabel(node: VizNode): string {
|
|
|
112
143
|
return node.status === "settled" ? "done" : node.status;
|
|
113
144
|
}
|
|
114
145
|
|
|
115
|
-
function
|
|
116
|
-
state
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
146
|
+
function clampList(
|
|
147
|
+
state: LegionViewState,
|
|
148
|
+
which: "log" | "timing",
|
|
149
|
+
count: number,
|
|
150
|
+
visible: number,
|
|
151
|
+
): void {
|
|
152
|
+
if (which === "log") {
|
|
153
|
+
state.logCursor = Math.max(0, Math.min(state.logCursor, count - 1));
|
|
154
|
+
if (state.logCursor < state.logOffset) state.logOffset = state.logCursor;
|
|
155
|
+
if (state.logCursor >= state.logOffset + visible) state.logOffset = state.logCursor - visible + 1;
|
|
156
|
+
state.logOffset = Math.max(0, Math.min(state.logOffset, Math.max(0, count - visible)));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
state.timingCursor = Math.max(0, Math.min(state.timingCursor, count - 1));
|
|
160
|
+
if (state.timingCursor < state.timingOffset) state.timingOffset = state.timingCursor;
|
|
161
|
+
if (state.timingCursor >= state.timingOffset + visible) state.timingOffset = state.timingCursor - visible + 1;
|
|
162
|
+
state.timingOffset = Math.max(0, Math.min(state.timingOffset, Math.max(0, count - visible)));
|
|
120
163
|
}
|
|
121
164
|
|
|
122
165
|
export type ViewAction = "none" | "render" | "close" | "open" | "stop";
|
|
@@ -124,6 +167,7 @@ export type ViewAction = "none" | "render" | "close" | "open" | "stop";
|
|
|
124
167
|
/** Pure key handling so it can be unit tested without a terminal. */
|
|
125
168
|
export function handleViewKey(data: string, state: LegionViewState, nodes: readonly VizNode[], page = 10): ViewAction {
|
|
126
169
|
const events = buildLog(nodes);
|
|
170
|
+
const timingRows = timingRowsFromNodes(nodes, Date.now());
|
|
127
171
|
const order = dfsOrder(nodes).filter(isAgent);
|
|
128
172
|
if (state.pendingStopId) {
|
|
129
173
|
const confirmed = matchesKey(data, "y") || data === "Y" || matchesKey(data, Key.enter);
|
|
@@ -171,6 +215,21 @@ export function handleViewKey(data: string, state: LegionViewState, nodes: reado
|
|
|
171
215
|
} else return "none";
|
|
172
216
|
return "render";
|
|
173
217
|
}
|
|
218
|
+
if (state.focus === "timing") {
|
|
219
|
+
if (down) state.timingCursor = Math.min(timingRows.length - 1, state.timingCursor + 1);
|
|
220
|
+
else if (up) state.timingCursor = Math.max(0, state.timingCursor - 1);
|
|
221
|
+
else if (matchesKey(data, Key.pageDown)) state.timingCursor = Math.min(timingRows.length - 1, state.timingCursor + page);
|
|
222
|
+
else if (matchesKey(data, Key.pageUp)) state.timingCursor = Math.max(0, state.timingCursor - page);
|
|
223
|
+
else if (matchesKey(data, Key.home)) state.timingCursor = 0;
|
|
224
|
+
else if (matchesKey(data, Key.end)) state.timingCursor = Math.max(0, timingRows.length - 1);
|
|
225
|
+
else if (matchesKey(data, Key.enter)) {
|
|
226
|
+
const row = timingRows[state.timingCursor];
|
|
227
|
+
if (!row) return "none";
|
|
228
|
+
state.selectedId = state.selectedId === row.node.id ? undefined : row.node.id;
|
|
229
|
+
if (state.selectedId === undefined) delete state.selectedId;
|
|
230
|
+
} else return "none";
|
|
231
|
+
return "render";
|
|
232
|
+
}
|
|
174
233
|
if (down || up || matchesKey(data, Key.right) || matchesKey(data, Key.left)) {
|
|
175
234
|
if (order.length === 0) return "none";
|
|
176
235
|
const step = down || matchesKey(data, Key.right) ? 1 : -1;
|