@oh-my-pi/pi-tui 18.9.2 → 18.10.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/CHANGELOG.md +53 -105
- package/dist/types/app-keybindings.d.ts +5 -0
- package/dist/types/chat/assistant-message.d.ts +1 -1
- package/dist/types/chat/user-message.d.ts +3 -2
- package/dist/types/chrome/countdown-timer.d.ts +3 -2
- package/dist/types/debug-server.d.ts +2 -0
- package/dist/types/overlays/annotation-overlay.d.ts +19 -0
- package/dist/types/overlays/ask-dialog.d.ts +11 -6
- package/dist/types/overlays/btw-history-panel.d.ts +2 -6
- package/dist/types/overlays/hook-editor.d.ts +7 -2
- package/dist/types/overlays/hook-input.d.ts +12 -4
- package/dist/types/overlays/jobs-panel.d.ts +8 -7
- package/dist/types/overlays/login-dialog.d.ts +8 -3
- package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
- package/dist/types/overlays/pause-screen.d.ts +5 -0
- package/dist/types/overlays/report-panel.d.ts +5 -1
- package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
- package/dist/types/overlays/session-account-selector.d.ts +4 -0
- package/dist/types/overlays/session-info-overlay.d.ts +7 -3
- package/dist/types/overlays/usage-dashboard.d.ts +6 -6
- package/dist/types/prompt/composer.d.ts +1 -20
- package/dist/types/status-line/context-usage.d.ts +1 -1
- package/dist/types/tools/grouped-file-output.d.ts +4 -1
- package/dist/types/tui.d.ts +10 -3
- package/package.json +9 -9
- package/src/app-keybindings.ts +5 -0
- package/src/apps/autoresearch-dashboard.ts +440 -27
- package/src/chat/assistant-message.ts +41 -13
- package/src/chat/user-message.ts +32 -19
- package/src/chrome/countdown-timer.ts +3 -3
- package/src/chrome/transcript-container.ts +202 -28
- package/src/debug-server.ts +51 -69
- package/src/hotkeys-markdown.ts +1 -0
- package/src/native/reconcile.ts +24 -6
- package/src/overlays/advisor-config.ts +259 -8
- package/src/overlays/annotation-overlay.ts +420 -168
- package/src/overlays/ask-dialog.ts +249 -105
- package/src/overlays/btw-history-panel.ts +178 -124
- package/src/overlays/hook-editor.ts +90 -32
- package/src/overlays/hook-input.ts +105 -19
- package/src/overlays/jobs-panel.ts +154 -132
- package/src/overlays/login-dialog.ts +201 -91
- package/src/overlays/mcp-add-wizard.ts +559 -279
- package/src/overlays/pause-screen.ts +81 -23
- package/src/overlays/plugin-selector.ts +17 -1
- package/src/overlays/queue-mode-selector.ts +10 -0
- package/src/overlays/report-panel.ts +6 -2
- package/src/overlays/reset-usage-selector.ts +130 -21
- package/src/overlays/session-account-selector.ts +36 -0
- package/src/overlays/session-info-overlay.ts +197 -76
- package/src/overlays/show-images-selector.ts +1 -0
- package/src/overlays/theme-selector.ts +6 -1
- package/src/overlays/thinking-selector.ts +8 -4
- package/src/overlays/usage-dashboard.ts +495 -143
- package/src/prompt/composer.ts +35 -72
- package/src/status-line/context-usage.ts +154 -57
- package/src/tools/grep.ts +1 -0
- package/src/tools/grouped-file-output.ts +11 -2
- package/src/tui.ts +27 -4
|
@@ -121,9 +121,12 @@ export declare function numberedCode(lines: readonly NumberedLine[], opts?: {
|
|
|
121
121
|
* header's suffix or the match count as a chip), then its lines as
|
|
122
122
|
* {@link numberedCode} with match lines marked and `…` between non-adjacent
|
|
123
123
|
* runs. Unnumbered body lines stay plain `code`. `maxFiles` keeps only the
|
|
124
|
-
* first files (the caller says how many were left out).
|
|
124
|
+
* first files (the caller says how many were left out). `file` owns the lines
|
|
125
|
+
* before any header: a single-file search prints none, yet its lines get the
|
|
126
|
+
* same file row as a directory search's.
|
|
125
127
|
*/
|
|
126
128
|
export declare function describeGroupedOutput(lines: readonly string[], options?: {
|
|
127
129
|
lang?: (path: string) => string | undefined;
|
|
128
130
|
maxFiles?: number;
|
|
131
|
+
file?: string;
|
|
129
132
|
}): NativeNode[];
|
package/dist/types/tui.d.ts
CHANGED
|
@@ -115,9 +115,6 @@ export interface Component {
|
|
|
115
115
|
* last time.
|
|
116
116
|
*/
|
|
117
117
|
render(width: number): readonly string[];
|
|
118
|
-
/** Inline decision panels must keep displaced settled transcript rows in
|
|
119
|
-
* native scrollback even when mounted inside a transient editor container. */
|
|
120
|
-
readonly retireDisplacedTranscript?: boolean;
|
|
121
118
|
/**
|
|
122
119
|
* Describe the component semantically for a Tern Surface Protocol
|
|
123
120
|
* terminal (see `native/node.ts`). Called instead of `render()` when the
|
|
@@ -440,6 +437,16 @@ export declare class TUI extends Container {
|
|
|
440
437
|
top: number;
|
|
441
438
|
length: number;
|
|
442
439
|
};
|
|
440
|
+
/**
|
|
441
|
+
* Rows of native history the last paint left on screen above a mutable
|
|
442
|
+
* viewport that reached the bottom row, or `undefined` when that paint left
|
|
443
|
+
* rows free below it (or none happened, or a destructive reset will repaint
|
|
444
|
+
* from row zero). Rows above a bottom-reaching viewport may already sit in
|
|
445
|
+
* native scrollback, which a later shrink cannot pull back down.
|
|
446
|
+
*/
|
|
447
|
+
getFilledHistoryRows(): number | undefined;
|
|
448
|
+
/** Whether native history holds rows the frame provider committed since the last destructive reset. */
|
|
449
|
+
hasProviderHistory(): boolean;
|
|
443
450
|
/**
|
|
444
451
|
* Probe for opt-in normal-buffer click capture. The provider is read every
|
|
445
452
|
* frame; while it returns true (and no fullscreen overlay owns the
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "18.
|
|
4
|
+
"version": "18.10.1",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"gen:glyphs": "bun scripts/gen-glyphs.ts"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@oh-my-pi/omptype": "18.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "18.
|
|
46
|
-
"@oh-my-pi/pi-ai": "18.
|
|
47
|
-
"@oh-my-pi/pi-catalog": "18.
|
|
48
|
-
"@oh-my-pi/pi-natives": "18.
|
|
49
|
-
"@oh-my-pi/pi-utils": "18.
|
|
50
|
-
"@oh-my-pi/pi-wire": "18.
|
|
51
|
-
"@oh-my-pi/snapcompact": "18.
|
|
44
|
+
"@oh-my-pi/omptype": "18.10.1",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.10.1",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.10.1",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.10.1",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.10.1",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.10.1",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.10.1",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.10.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"kitty-vt-wasm": "^0.2.0"
|
package/src/app-keybindings.ts
CHANGED
|
@@ -37,6 +37,7 @@ interface AppKeybindings {
|
|
|
37
37
|
"app.model.selectTemporary": true;
|
|
38
38
|
"app.tools.expand": true;
|
|
39
39
|
"app.tools.toggleVisibility": true;
|
|
40
|
+
"app.ask.hide": true;
|
|
40
41
|
"app.editor.external": true;
|
|
41
42
|
"app.message.followUp": true;
|
|
42
43
|
"app.retry": true;
|
|
@@ -138,6 +139,10 @@ export const KEYBINDINGS = {
|
|
|
138
139
|
defaultKeys: "ctrl+shift+o",
|
|
139
140
|
description: "Show or hide tool activity",
|
|
140
141
|
},
|
|
142
|
+
"app.ask.hide": {
|
|
143
|
+
defaultKeys: "alt+h",
|
|
144
|
+
description: "Hide or show a pending ask dialog",
|
|
145
|
+
},
|
|
141
146
|
"app.editor.external": {
|
|
142
147
|
defaultKeys: "ctrl+g",
|
|
143
148
|
description: "Open external editor",
|
|
@@ -9,15 +9,16 @@ import type { Theme } from "../theme/theme";
|
|
|
9
9
|
import {
|
|
10
10
|
currentResults,
|
|
11
11
|
findBaselineMetric,
|
|
12
|
+
findBaselineResult,
|
|
12
13
|
findBaselineRunNumber,
|
|
13
14
|
findBaselineSecondary,
|
|
14
15
|
formatElapsed,
|
|
15
16
|
isBetter,
|
|
16
17
|
} from "./autoresearch-data";
|
|
17
|
-
import { formatNum, type ExperimentResult, type ExperimentState } from "../tools/autoresearch";
|
|
18
|
-
import type { TspSpan, TspTableColumn, TspText } from "@oh-my-pi/pi-wire";
|
|
18
|
+
import { fmtNum, formatNum, type ExperimentResult, type ExperimentState } from "../tools/autoresearch";
|
|
19
|
+
import type { TspScrollBy, TspSpan, TspTableColumn, TspText } from "@oh-my-pi/pi-wire";
|
|
19
20
|
import { card, col, elapsed, keyed, node, row, span, text } from "../native/describe";
|
|
20
|
-
import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node";
|
|
21
|
+
import type { DescribeContext, NativeNode, NativeScroll, NativeUiEvent } from "../native/node";
|
|
21
22
|
import { actionBar, actionButton, hintsRow } from "../native/overlay";
|
|
22
23
|
import { Memo } from "../native/memo";
|
|
23
24
|
import { isNativeRendering } from "../native/state";
|
|
@@ -147,11 +148,13 @@ export function createDashboardController(): DashboardController {
|
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
let scrollView: ScrollView | undefined;
|
|
151
|
+
/** Natively the terminal scrolls the sheet body: bumped per scroll key. */
|
|
152
|
+
let scroll: NativeScroll | undefined;
|
|
150
153
|
const native = new Memo();
|
|
151
154
|
return {
|
|
152
|
-
/** A glass sheet titled by the experiment
|
|
155
|
+
/** A glass sheet titled by the experiment; what it is follows, muted. */
|
|
153
156
|
get nativeOverlay() {
|
|
154
|
-
return { role: "omp.overlay.autoresearch", head:
|
|
157
|
+
return { role: "omp.overlay.autoresearch", head: describeSheetHead(runtime), size: "lg" as const };
|
|
155
158
|
},
|
|
156
159
|
describe(cx: DescribeContext): NativeNode {
|
|
157
160
|
const state = runtime.state;
|
|
@@ -159,6 +162,7 @@ export function createDashboardController(): DashboardController {
|
|
|
159
162
|
return native.get(
|
|
160
163
|
[
|
|
161
164
|
chart,
|
|
165
|
+
scroll,
|
|
162
166
|
state,
|
|
163
167
|
state.results.length,
|
|
164
168
|
state.results.at(-1),
|
|
@@ -171,29 +175,9 @@ export function createDashboardController(): DashboardController {
|
|
|
171
175
|
runtime.runningExperiment,
|
|
172
176
|
],
|
|
173
177
|
() => {
|
|
174
|
-
const
|
|
175
|
-
const children: NativeNode[] = describeDashboard(runtime, 0, chart);
|
|
176
|
-
if (running) {
|
|
177
|
-
children.push(
|
|
178
|
-
row(
|
|
179
|
-
[
|
|
180
|
-
node("spinner", { label: [span("running", "warning")], tone: "warning" }),
|
|
181
|
-
elapsed(Date.now() - running.startedAt),
|
|
182
|
-
text([span(replaceTabs(running.command), "warning")], { truncate: "end" }),
|
|
183
|
-
],
|
|
184
|
-
{ gap: "sm" },
|
|
185
|
-
),
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
children.push(
|
|
189
|
-
hintsRow([
|
|
190
|
-
{ keys: ["up", "down", "j", "k"], label: "scroll" },
|
|
191
|
-
{ keys: ["pageUp", "pageDown"], label: "page" },
|
|
192
|
-
{ keys: ["g", "shift+g"], label: "top/bottom" },
|
|
193
|
-
]),
|
|
194
|
-
);
|
|
178
|
+
const children = describeSheet(runtime, chart);
|
|
195
179
|
children.push(actionBar([null, actionButton("Close", "close", { keys: "escape" })]));
|
|
196
|
-
return col(children, { gap: "md", role: "omp.app.autoresearch" });
|
|
180
|
+
return { ...col(children, { gap: "md", role: "omp.app.autoresearch" }), scroll };
|
|
197
181
|
},
|
|
198
182
|
);
|
|
199
183
|
},
|
|
@@ -223,19 +207,27 @@ export function createDashboardController(): DashboardController {
|
|
|
223
207
|
done(undefined);
|
|
224
208
|
return;
|
|
225
209
|
}
|
|
210
|
+
let by: TspScrollBy | undefined;
|
|
226
211
|
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
212
|
+
by = "line-up";
|
|
227
213
|
scrollView?.scroll(-1);
|
|
228
214
|
} else if (matchesKey(data, "down") || matchesKey(data, "j")) {
|
|
215
|
+
by = "line-down";
|
|
229
216
|
scrollView?.scroll(1);
|
|
230
217
|
} else if (matchesKey(data, "pageUp")) {
|
|
218
|
+
by = "page-up";
|
|
231
219
|
scrollView?.page(-1);
|
|
232
220
|
} else if (matchesKey(data, "pageDown")) {
|
|
221
|
+
by = "page-down";
|
|
233
222
|
scrollView?.page(1);
|
|
234
223
|
} else if (data === "g") {
|
|
224
|
+
by = "start";
|
|
235
225
|
scrollView?.scrollToTop();
|
|
236
226
|
} else if (data === "G") {
|
|
227
|
+
by = "end";
|
|
237
228
|
scrollView?.scrollToBottom();
|
|
238
229
|
}
|
|
230
|
+
if (by) scroll = { by, n: (scroll?.n ?? 0) + 1 };
|
|
239
231
|
tui.requestRender();
|
|
240
232
|
},
|
|
241
233
|
invalidate(): void {},
|
|
@@ -513,6 +505,427 @@ function describeDashboard(runtime: AutoresearchDashboardRuntime, maxRows: numbe
|
|
|
513
505
|
return [keyed(col(children, { gap: "sm" }), "dashboard")];
|
|
514
506
|
}
|
|
515
507
|
|
|
508
|
+
// -- native sheet --------------------------------------------------------------
|
|
509
|
+
|
|
510
|
+
/** Sheet head: the experiment's name, then what the sheet is. */
|
|
511
|
+
function describeSheetHead(runtime: AutoresearchDashboardRuntime): TspSpan[] {
|
|
512
|
+
const name = runtime.state.name?.trim();
|
|
513
|
+
return name ? [span(replaceTabs(name)), span(" · Autoresearch", "muted")] : [span("Autoresearch")];
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const STATUS_LABEL: Record<ExperimentResult["status"], string> = {
|
|
517
|
+
keep: "kept",
|
|
518
|
+
discard: "discarded",
|
|
519
|
+
crash: "crashed",
|
|
520
|
+
checks_failed: "checks failed",
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const STATUS_TOKEN: Record<ExperimentResult["status"], string> = {
|
|
524
|
+
keep: "success",
|
|
525
|
+
discard: "dim",
|
|
526
|
+
crash: "error",
|
|
527
|
+
checks_failed: "error",
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
/** Fraction digits (0–2) that show every value exactly, so numbers that sit together align. */
|
|
531
|
+
function fractionDigits(values: readonly number[]): number {
|
|
532
|
+
let digits = 0;
|
|
533
|
+
for (const value of values) {
|
|
534
|
+
while (digits < 2 && Math.abs(Math.round(value * 10 ** digits) - value * 10 ** digits) > 1e-6) digits += 1;
|
|
535
|
+
}
|
|
536
|
+
return digits;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** A metric split into its number and unit, so the unit can sit a step quieter. */
|
|
540
|
+
function metricSpans(value: number, unit: string, digits: number, numberStyle?: string): TspSpan[] {
|
|
541
|
+
const spans = [span(fmtNum(value, digits), numberStyle)];
|
|
542
|
+
if (unit) spans.push(span(unit, "muted"));
|
|
543
|
+
return spans;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** Signed change from `base` to `value` in percent (`−12.4%`), or null when there is nothing to compare. */
|
|
547
|
+
function percentChange(value: number, base: number | null | undefined): string | null {
|
|
548
|
+
if (base === null || base === undefined || base === 0 || value === base) return null;
|
|
549
|
+
const delta = ((value - base) / base) * 100;
|
|
550
|
+
return `${delta > 0 ? "+" : "−"}${Math.abs(delta).toFixed(1)}%`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** What the sheet's mode says, with the span token it reads in. */
|
|
554
|
+
function modeLabel(runtime: AutoresearchDashboardRuntime): [label: string, token: string] {
|
|
555
|
+
const status = renderModeStatus(runtime, runtime.state);
|
|
556
|
+
switch (status) {
|
|
557
|
+
case "mode on":
|
|
558
|
+
return ["Mode on", "success"];
|
|
559
|
+
case "baseline pending":
|
|
560
|
+
return ["Baseline pending", "warning"];
|
|
561
|
+
case "segment complete":
|
|
562
|
+
return ["Segment complete", "muted"];
|
|
563
|
+
default:
|
|
564
|
+
return ["Mode off", "muted"];
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* The overlay's body: the goal and setup, the headline numbers (best,
|
|
570
|
+
* baseline, confidence, runs), what is happening now, the metric per run and
|
|
571
|
+
* the runs newest first. Empty segments explain what comes next instead.
|
|
572
|
+
*/
|
|
573
|
+
function describeSheet(runtime: AutoresearchDashboardRuntime, chart: boolean): NativeNode[] {
|
|
574
|
+
const state = runtime.state;
|
|
575
|
+
const current = currentResults(state.results, state.currentSegment);
|
|
576
|
+
const archived = state.results.length - current.length;
|
|
577
|
+
const children: NativeNode[] = [describeIntro(runtime, archived > 0)];
|
|
578
|
+
const now = describeNow(runtime);
|
|
579
|
+
if (now) children.push(now);
|
|
580
|
+
if (current.length === 0) {
|
|
581
|
+
children.push(describeEmpty(runtime, archived));
|
|
582
|
+
return children;
|
|
583
|
+
}
|
|
584
|
+
children.push(describeStats(runtime, current));
|
|
585
|
+
const indexed = state.results
|
|
586
|
+
.map((result, index) => ({ result, index }))
|
|
587
|
+
.filter(({ result }) => result.segment === state.currentSegment);
|
|
588
|
+
if (chart && indexed.length >= 2) children.push(describeTrend(runtime, indexed));
|
|
589
|
+
children.push(describeRuns(runtime, indexed));
|
|
590
|
+
if (archived > 0) {
|
|
591
|
+
children.push(
|
|
592
|
+
keyed(
|
|
593
|
+
text([span(`${archived} ${archived === 1 ? "run" : "runs"} archived from earlier segments`)], {
|
|
594
|
+
role: "omp.autoresearch.note",
|
|
595
|
+
}),
|
|
596
|
+
"archived",
|
|
597
|
+
),
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
return children;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** The goal as a sentence, then one quiet line of setup: mode, metric and direction, segment, branch. */
|
|
604
|
+
function describeIntro(runtime: AutoresearchDashboardRuntime, segmented: boolean): NativeNode {
|
|
605
|
+
const state = runtime.state;
|
|
606
|
+
const children: NativeNode[] = [];
|
|
607
|
+
const goal = state.goal?.trim();
|
|
608
|
+
if (goal) children.push(keyed(text(replaceTabs(goal), { wrap: "word", role: "omp.autoresearch.goal" }), "goal"));
|
|
609
|
+
const [mode, modeToken] = modeLabel(runtime);
|
|
610
|
+
const meta: TspSpan[] = [
|
|
611
|
+
span("● ", modeToken),
|
|
612
|
+
span(mode),
|
|
613
|
+
span(" · "),
|
|
614
|
+
span(state.metricName, "mono"),
|
|
615
|
+
span(`, ${state.bestDirection} is better`),
|
|
616
|
+
];
|
|
617
|
+
if (segmented) meta.push(span(" · "), span(`Segment ${state.currentSegment + 1}`));
|
|
618
|
+
if (state.maxExperiments !== null) meta.push(span(" · "), span(`${state.maxExperiments} runs per segment`));
|
|
619
|
+
if (state.branch) meta.push(span(" · "), span(replaceTabs(state.branch), "mono"));
|
|
620
|
+
children.push(keyed(text(meta, { truncate: "end", role: "omp.autoresearch.meta" }), "meta"));
|
|
621
|
+
return keyed(col(children, { gap: "xs", role: "omp.autoresearch.intro" }), "intro");
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** What is happening right now: a benchmark running, or a finished run waiting to be logged. */
|
|
625
|
+
function describeNow(runtime: AutoresearchDashboardRuntime): NativeNode | undefined {
|
|
626
|
+
const state = runtime.state;
|
|
627
|
+
const running = runtime.runningExperiment;
|
|
628
|
+
if (running) {
|
|
629
|
+
return keyed(
|
|
630
|
+
row(
|
|
631
|
+
[
|
|
632
|
+
node("spinner", { label: [span("Running")], tone: "pending" }),
|
|
633
|
+
elapsed(Date.now() - running.startedAt),
|
|
634
|
+
text([span(replaceTabs(running.command), "mono")], {
|
|
635
|
+
truncate: "end",
|
|
636
|
+
role: "omp.autoresearch.command",
|
|
637
|
+
}),
|
|
638
|
+
],
|
|
639
|
+
{ gap: "sm", align: "center", role: "omp.autoresearch.running", tone: "pending" },
|
|
640
|
+
),
|
|
641
|
+
"now",
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
const pending = runtime.lastRunSummary;
|
|
645
|
+
if (!pending) return undefined;
|
|
646
|
+
const result: TspSpan[] = [span(`Run #${pending.runNumber} ${pending.passed ? "passed" : "failed"}`, "strong")];
|
|
647
|
+
if (pending.parsedPrimary !== null) {
|
|
648
|
+
result.push(
|
|
649
|
+
span(` · ${state.metricName} `),
|
|
650
|
+
...metricSpans(pending.parsedPrimary, state.metricUnit, fractionDigits([pending.parsedPrimary]), "mono"),
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
return keyed(
|
|
654
|
+
row(
|
|
655
|
+
[
|
|
656
|
+
text(result, { truncate: "end" }),
|
|
657
|
+
text([span("Log it with "), span("log_experiment", "mono"), span(" before the next run")], {
|
|
658
|
+
truncate: "end",
|
|
659
|
+
role: "omp.autoresearch.next",
|
|
660
|
+
}),
|
|
661
|
+
],
|
|
662
|
+
{ gap: "md", align: "center", role: "omp.autoresearch.pending", tone: "warning" },
|
|
663
|
+
),
|
|
664
|
+
"now",
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** No runs in this segment yet: what the dashboard is waiting for. */
|
|
669
|
+
function describeEmpty(runtime: AutoresearchDashboardRuntime, archived: number): NativeNode {
|
|
670
|
+
const [title, detail] = runtime.lastRunSummary
|
|
671
|
+
? ["Waiting for the first log", "Log the finished run to set this segment's baseline."]
|
|
672
|
+
: runtime.runningExperiment
|
|
673
|
+
? ["Measuring the baseline", "Every later run is compared with this first one."]
|
|
674
|
+
: runtime.autoresearchMode
|
|
675
|
+
? [
|
|
676
|
+
"No baseline yet",
|
|
677
|
+
"Run the benchmark once and log it as the baseline. Every later run is compared with it.",
|
|
678
|
+
]
|
|
679
|
+
: ["No experiments logged yet", "Turn autoresearch mode on to start measuring."];
|
|
680
|
+
const children: NativeNode[] = [
|
|
681
|
+
text([span(title)], { role: "omp.autoresearch.empty.title" }),
|
|
682
|
+
text([span(detail)], { wrap: "word", role: "omp.autoresearch.empty.detail" }),
|
|
683
|
+
];
|
|
684
|
+
if (archived > 0) {
|
|
685
|
+
children.push(
|
|
686
|
+
text([span(`${archived} ${archived === 1 ? "run" : "runs"} archived from earlier segments`)], {
|
|
687
|
+
role: "omp.autoresearch.note",
|
|
688
|
+
}),
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
return keyed(col(children, { gap: "xs", align: "center", role: "omp.autoresearch.empty" }), "empty");
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** One headline number: a label, the value, and one line saying what it means. */
|
|
695
|
+
function stat(key: string, label: string, value: TspSpan[], detail: NativeNode[]): NativeNode {
|
|
696
|
+
return keyed(
|
|
697
|
+
col(
|
|
698
|
+
[
|
|
699
|
+
text(label, { role: "omp.autoresearch.stat.k" }),
|
|
700
|
+
text(value, { wrap: "none", truncate: "end", role: "omp.autoresearch.stat.v" }),
|
|
701
|
+
...detail,
|
|
702
|
+
],
|
|
703
|
+
{ role: "omp.autoresearch.stat" },
|
|
704
|
+
),
|
|
705
|
+
key,
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function statDetail(spans: TspSpan[]): NativeNode {
|
|
710
|
+
return text(spans, { truncate: "end", role: "omp.autoresearch.stat.sub" });
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** Best, baseline, confidence and the run tally, then the secondary metrics at the best run. */
|
|
714
|
+
function describeStats(runtime: AutoresearchDashboardRuntime, current: readonly ExperimentResult[]): NativeNode {
|
|
715
|
+
const state = runtime.state;
|
|
716
|
+
const unit = state.metricUnit;
|
|
717
|
+
const best = findBestResult(state);
|
|
718
|
+
const baseline = findBaselineMetric(state.results, state.currentSegment);
|
|
719
|
+
const baselineRun = findBaselineRunNumber(state.results, state.currentSegment);
|
|
720
|
+
const baselineResult = current.find(result => result.status === "keep" && !result.flagged);
|
|
721
|
+
const counts = statusCounts(current);
|
|
722
|
+
const digits = fractionDigits(
|
|
723
|
+
[best?.result.metric, baseline].filter((value): value is number => value !== undefined && value !== null),
|
|
724
|
+
);
|
|
725
|
+
const tiles: NativeNode[] = [];
|
|
726
|
+
|
|
727
|
+
if (best) {
|
|
728
|
+
const run = best.result.runNumber ?? best.index + 1;
|
|
729
|
+
const change = percentChange(best.result.metric, baseline);
|
|
730
|
+
const better = baseline !== null && isBetter(best.result.metric, baseline, state.bestDirection);
|
|
731
|
+
tiles.push(
|
|
732
|
+
stat("best", "Best", metricSpans(best.result.metric, unit, digits), [
|
|
733
|
+
statDetail(
|
|
734
|
+
change
|
|
735
|
+
? [span(change, better ? "success" : "error"), span(` vs baseline · run #${run}`)]
|
|
736
|
+
: [span(`The baseline · run #${run}`)],
|
|
737
|
+
),
|
|
738
|
+
]),
|
|
739
|
+
);
|
|
740
|
+
} else {
|
|
741
|
+
tiles.push(stat("best", "Best", [span("—", "muted")], [statDetail([span("No kept run yet")])]));
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const baselineDetail: TspSpan[] = [];
|
|
745
|
+
if (baselineRun !== null) baselineDetail.push(span(`Run #${baselineRun}`));
|
|
746
|
+
if (baselineResult?.commit) baselineDetail.push(span(" · "), span(baselineResult.commit.slice(0, 7), "mono"));
|
|
747
|
+
tiles.push(
|
|
748
|
+
stat("baseline", "Baseline", baseline === null ? [span("—", "muted")] : metricSpans(baseline, unit, digits), [
|
|
749
|
+
statDetail(baselineDetail.length > 0 ? baselineDetail : [span("Not logged yet")]),
|
|
750
|
+
]),
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
const confidence = state.confidence;
|
|
754
|
+
tiles.push(
|
|
755
|
+
stat(
|
|
756
|
+
"confidence",
|
|
757
|
+
"Confidence",
|
|
758
|
+
confidence === null
|
|
759
|
+
? [span("—", "muted")]
|
|
760
|
+
: [span(confidence.toFixed(1), confidenceToken(confidence)), span("×", "muted")],
|
|
761
|
+
[
|
|
762
|
+
statDetail([
|
|
763
|
+
span(
|
|
764
|
+
confidence === null
|
|
765
|
+
? "Needs three runs and a gain"
|
|
766
|
+
: confidence >= 2
|
|
767
|
+
? "Gain is clear of run noise"
|
|
768
|
+
: confidence >= 1
|
|
769
|
+
? "Gain is near run noise"
|
|
770
|
+
: "Gain is within run noise",
|
|
771
|
+
),
|
|
772
|
+
]),
|
|
773
|
+
],
|
|
774
|
+
),
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
const total = Math.max(state.maxExperiments ?? 0, current.length);
|
|
778
|
+
const failed = counts.crash + counts.checks_failed;
|
|
779
|
+
const tally: TspSpan[] = [span(`${counts.keep} kept · ${counts.discard} discarded`)];
|
|
780
|
+
if (failed > 0) tally.push(span(" · "), span(`${failed} failed`, "error"));
|
|
781
|
+
tiles.push(
|
|
782
|
+
stat(
|
|
783
|
+
"runs",
|
|
784
|
+
"Runs",
|
|
785
|
+
state.maxExperiments !== null
|
|
786
|
+
? [span(String(current.length)), span(` / ${state.maxExperiments}`, "muted")]
|
|
787
|
+
: [span(String(current.length))],
|
|
788
|
+
[
|
|
789
|
+
node("meter", {
|
|
790
|
+
value: current.length / total,
|
|
791
|
+
parts: [
|
|
792
|
+
{ value: counts.keep / total, token: "success", label: `${counts.keep} kept` },
|
|
793
|
+
{ value: counts.discard / total, token: "muted", label: `${counts.discard} discarded` },
|
|
794
|
+
{
|
|
795
|
+
value: failed / total,
|
|
796
|
+
token: "error",
|
|
797
|
+
label: `${counts.crash} crashed · ${counts.checks_failed} checks failed`,
|
|
798
|
+
},
|
|
799
|
+
],
|
|
800
|
+
size: "sm",
|
|
801
|
+
role: "omp.autoresearch.tally",
|
|
802
|
+
}),
|
|
803
|
+
statDetail(tally),
|
|
804
|
+
],
|
|
805
|
+
),
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
const children: NativeNode[] = [row(tiles, { role: "omp.autoresearch.stats" })];
|
|
809
|
+
if (best && state.secondaryMetrics.length > 0) {
|
|
810
|
+
const baselineSecondary = findBaselineSecondary(state.results, state.currentSegment, state.secondaryMetrics);
|
|
811
|
+
const spans: TspSpan[] = [];
|
|
812
|
+
for (const metric of state.secondaryMetrics) {
|
|
813
|
+
const value = best.result.metrics[metric.name];
|
|
814
|
+
if (value === undefined) continue;
|
|
815
|
+
spans.push(span(spans.length === 0 ? "At best " : " · "));
|
|
816
|
+
spans.push(
|
|
817
|
+
span(`${metric.name} `, "mono muted"),
|
|
818
|
+
...metricSpans(value, metric.unit, fractionDigits([value]), "mono"),
|
|
819
|
+
);
|
|
820
|
+
const change = percentChange(value, baselineSecondary[metric.name]);
|
|
821
|
+
if (change) spans.push(span(` ${change}`, "mono"));
|
|
822
|
+
}
|
|
823
|
+
if (spans.length > 0) {
|
|
824
|
+
children.push(keyed(text(spans, { truncate: "end", role: "omp.autoresearch.secondary" }), "secondary"));
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return keyed(col(children, { role: "omp.autoresearch.summary" }), "summary");
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/** The primary metric per run, in run order. */
|
|
831
|
+
function describeTrend(
|
|
832
|
+
runtime: AutoresearchDashboardRuntime,
|
|
833
|
+
indexed: readonly { result: ExperimentResult; index: number }[],
|
|
834
|
+
): NativeNode {
|
|
835
|
+
const state = runtime.state;
|
|
836
|
+
return node(
|
|
837
|
+
"chart",
|
|
838
|
+
{
|
|
839
|
+
kind: "bars",
|
|
840
|
+
series: indexed.map(({ result, index }) => {
|
|
841
|
+
const run = result.runNumber ?? index + 1;
|
|
842
|
+
return {
|
|
843
|
+
label: `#${run}`,
|
|
844
|
+
value: result.metric,
|
|
845
|
+
title: `#${run} ${STATUS_LABEL[result.status]} · ${formatNum(result.metric, state.metricUnit)} · ${replaceTabs(result.description)}`,
|
|
846
|
+
};
|
|
847
|
+
}),
|
|
848
|
+
token: "accent",
|
|
849
|
+
summary: [span(`${state.metricName} per run, ${state.bestDirection} is better`)],
|
|
850
|
+
size: "md",
|
|
851
|
+
role: "omp.autoresearch.trend",
|
|
852
|
+
},
|
|
853
|
+
undefined,
|
|
854
|
+
"trend",
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/** Every run of the segment, newest first: outcome, metric and its change from the baseline, commit, what changed. */
|
|
859
|
+
function describeRuns(
|
|
860
|
+
runtime: AutoresearchDashboardRuntime,
|
|
861
|
+
indexed: readonly { result: ExperimentResult; index: number }[],
|
|
862
|
+
): NativeNode {
|
|
863
|
+
const state = runtime.state;
|
|
864
|
+
const baseline = findBaselineResult(state.results, state.currentSegment);
|
|
865
|
+
const best = findBestResult(state);
|
|
866
|
+
// Cells carry bare numbers at one precision per column; the unit goes in the head unless the name says it.
|
|
867
|
+
const head = (name: string, unit: string): TspSpan[] =>
|
|
868
|
+
unit && !name.toLowerCase().endsWith(`_${unit.toLowerCase()}`)
|
|
869
|
+
? [span(name, "mono"), span(` ${unit}`, "muted")]
|
|
870
|
+
: [span(name, "mono")];
|
|
871
|
+
const measured = indexed.filter(({ result }) => result.status !== "crash").map(({ result }) => result);
|
|
872
|
+
const metricDigits = fractionDigits(measured.map(result => result.metric));
|
|
873
|
+
const secondaryDigits = state.secondaryMetrics.map(metric =>
|
|
874
|
+
fractionDigits(
|
|
875
|
+
measured.map(result => result.metrics[metric.name]).filter((value): value is number => value !== undefined),
|
|
876
|
+
),
|
|
877
|
+
);
|
|
878
|
+
const cols: TspTableColumn[] = [
|
|
879
|
+
{ id: "run", head: "#", align: "end", priority: 3 },
|
|
880
|
+
{ id: "status", head: "Outcome", priority: 4 },
|
|
881
|
+
{ id: "metric", head: head(state.metricName, state.metricUnit), align: "end", priority: 5 },
|
|
882
|
+
{ id: "change", head: "vs baseline", align: "end", priority: 4 },
|
|
883
|
+
...state.secondaryMetrics.map((metric, index): TspTableColumn => ({
|
|
884
|
+
id: `secondary${index}`,
|
|
885
|
+
head: head(metric.name, metric.unit),
|
|
886
|
+
align: "end",
|
|
887
|
+
priority: 1,
|
|
888
|
+
})),
|
|
889
|
+
{ id: "commit", head: "Commit", priority: 2 },
|
|
890
|
+
{ id: "description", head: "Change", truncate: "end", grow: 1, priority: 3 },
|
|
891
|
+
];
|
|
892
|
+
const rows = indexed
|
|
893
|
+
.slice()
|
|
894
|
+
.reverse()
|
|
895
|
+
.map(({ result, index }) => {
|
|
896
|
+
const isBest = best?.index === index;
|
|
897
|
+
const isBaseline = result === baseline;
|
|
898
|
+
const crashed = result.status === "crash";
|
|
899
|
+
const change = isBaseline || crashed ? null : percentChange(result.metric, baseline?.metric);
|
|
900
|
+
const better = baseline !== null && isBetter(result.metric, baseline.metric, state.bestDirection);
|
|
901
|
+
const cells: Record<string, TspText> = {
|
|
902
|
+
run: [span(String(result.runNumber ?? index + 1), "mono")],
|
|
903
|
+
status: isBest
|
|
904
|
+
? [span("● ", "success"), span("best", "strong")]
|
|
905
|
+
: [span("● ", STATUS_TOKEN[result.status]), span(STATUS_LABEL[result.status])],
|
|
906
|
+
metric: crashed
|
|
907
|
+
? [span("—", "muted")]
|
|
908
|
+
: [span(fmtNum(result.metric, metricDigits), isBest ? "mono strong" : "mono")],
|
|
909
|
+
change: isBaseline
|
|
910
|
+
? [span("baseline", "muted")]
|
|
911
|
+
: change
|
|
912
|
+
? [span(change, `mono ${result.status === "keep" ? (better ? "success" : "error") : "muted"}`)]
|
|
913
|
+
: [span("—", "muted")],
|
|
914
|
+
commit: [span(result.commit ? result.commit.slice(0, 7) : "—", result.commit ? "mono" : "muted")],
|
|
915
|
+
description: [span(replaceTabs(result.description))],
|
|
916
|
+
};
|
|
917
|
+
state.secondaryMetrics.forEach((metric, metricIndex) => {
|
|
918
|
+
const value = result.metrics[metric.name];
|
|
919
|
+
cells[`secondary${metricIndex}`] =
|
|
920
|
+
value === undefined
|
|
921
|
+
? [span("—", "muted")]
|
|
922
|
+
: [span(fmtNum(value, secondaryDigits[metricIndex] ?? 0), "mono")];
|
|
923
|
+
});
|
|
924
|
+
return { id: String(index), cells };
|
|
925
|
+
});
|
|
926
|
+
return node("table", { cols, rows, role: "omp.autoresearch.runs" }, undefined, "runs");
|
|
927
|
+
}
|
|
928
|
+
|
|
516
929
|
function statusCounts(results: readonly ExperimentResult[]): Record<ExperimentResult["status"], number> {
|
|
517
930
|
const counts: Record<ExperimentResult["status"], number> = { keep: 0, discard: 0, crash: 0, checks_failed: 0 };
|
|
518
931
|
for (const result of results) counts[result.status] += 1;
|