@oh-my-pi/pi-tui 18.10.0 → 18.10.2
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 +42 -154
- package/THIRD-PARTY-NOTICES.txt +3 -3
- package/dist/types/app-keybindings.d.ts +8 -8
- package/dist/types/apps/git/diff-pane.d.ts +5 -0
- package/dist/types/apps/git/state.d.ts +11 -0
- package/dist/types/chat/assistant-message.d.ts +9 -4
- package/dist/types/chat/tool-execution.d.ts +32 -20
- package/dist/types/chat/user-message.d.ts +3 -2
- package/dist/types/chrome/countdown-timer.d.ts +3 -2
- package/dist/types/native/backend.d.ts +5 -4
- package/dist/types/native/blobs.d.ts +13 -1
- package/dist/types/overlays/annotation-overlay.d.ts +19 -0
- package/dist/types/overlays/ask-dialog.d.ts +9 -4
- 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/model-browser.d.ts +6 -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/status-line/context-usage.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +2 -1
- package/dist/types/tools/eval.d.ts +2 -2
- package/dist/types/tools/grouped-file-output.d.ts +4 -1
- package/dist/types/tools/native-view.d.ts +1 -1
- package/dist/types/tools/read.d.ts +7 -1
- package/dist/types/tools/renderer.d.ts +2 -0
- package/package.json +9 -9
- package/src/app-keybindings.ts +8 -8
- package/src/apps/autoresearch-dashboard.ts +440 -27
- package/src/apps/git/diff-pane.ts +39 -35
- package/src/apps/git/git-tui.ts +53 -4
- package/src/apps/git/state.ts +12 -0
- package/src/chat/assistant-message.ts +68 -19
- package/src/chat/tool-execution.ts +73 -30
- package/src/chat/user-message.ts +32 -19
- package/src/chrome/countdown-timer.ts +3 -3
- package/src/native/backend.ts +13 -5
- package/src/native/blobs.ts +34 -1
- 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 +154 -90
- 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/model-browser.ts +23 -1
- package/src/overlays/model-hub.ts +2 -9
- 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/tree-selector.ts +117 -2
- package/src/overlays/usage-dashboard.ts +495 -143
- package/src/status-line/context-usage.ts +154 -57
- package/src/tools/bash.ts +9 -8
- package/src/tools/eval.ts +2 -2
- package/src/tools/grep.ts +1 -0
- package/src/tools/grouped-file-output.ts +11 -2
- package/src/tools/native-view.ts +2 -2
- package/src/tools/read.ts +12 -4
- package/src/tools/renderer.ts +2 -0
- package/src/tools/wait.ts +16 -30
|
@@ -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;
|
|
@@ -570,6 +570,43 @@ interface AssetDocument {
|
|
|
570
570
|
/** Result of a left click inside the pane. */
|
|
571
571
|
export type PaneClick = { type: "hunk-action"; hunk: HunkBlock; action: HunkAction } | { type: "handled" } | null;
|
|
572
572
|
|
|
573
|
+
/**
|
|
574
|
+
* The heading of one side of a media/binary change (`Before · PNG · Git LFS`):
|
|
575
|
+
* `label` plus the side's kind, shared by the ANSI pane and the native compare.
|
|
576
|
+
*/
|
|
577
|
+
export function assetTitle(label: string, side: FileAssetSide): string {
|
|
578
|
+
let kind: string;
|
|
579
|
+
let lfs = false;
|
|
580
|
+
switch (side.kind) {
|
|
581
|
+
case "empty":
|
|
582
|
+
return label;
|
|
583
|
+
case "image":
|
|
584
|
+
kind =
|
|
585
|
+
side.image.sourceMimeType === "image/svg+xml"
|
|
586
|
+
? "SVG"
|
|
587
|
+
: side.image.sourceMimeType.replace(/^image\//, "").toUpperCase();
|
|
588
|
+
lfs = side.image.lfsOid !== undefined;
|
|
589
|
+
break;
|
|
590
|
+
case "text":
|
|
591
|
+
kind = "Text";
|
|
592
|
+
lfs = side.lfsOid !== undefined;
|
|
593
|
+
break;
|
|
594
|
+
case "binary":
|
|
595
|
+
kind = "Binary";
|
|
596
|
+
lfs = side.lfsOid !== undefined;
|
|
597
|
+
break;
|
|
598
|
+
case "tooLarge":
|
|
599
|
+
kind = "Too large";
|
|
600
|
+
lfs = side.lfsOid !== undefined;
|
|
601
|
+
break;
|
|
602
|
+
case "lfsMissing":
|
|
603
|
+
kind = "LFS missing";
|
|
604
|
+
lfs = true;
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
return `${label} · ${kind}${lfs ? " · Git LFS" : ""}`;
|
|
608
|
+
}
|
|
609
|
+
|
|
573
610
|
/**
|
|
574
611
|
* Scrollable diff viewport. The root component feeds it key/mouse input and
|
|
575
612
|
* composes its rendered lines into the frame.
|
|
@@ -1129,11 +1166,11 @@ export class DiffPane {
|
|
|
1129
1166
|
for (let index = 0; index < height; index++) {
|
|
1130
1167
|
const leftSource =
|
|
1131
1168
|
index === 0
|
|
1132
|
-
? centerLine(theme.bold(
|
|
1169
|
+
? centerLine(theme.bold(assetTitle("Before", asset.old)), leftWidth).trimEnd()
|
|
1133
1170
|
: (oldLines[index - 1] ?? "");
|
|
1134
1171
|
const rightSource =
|
|
1135
1172
|
index === 0
|
|
1136
|
-
? centerLine(theme.bold(
|
|
1173
|
+
? centerLine(theme.bold(assetTitle("After", asset.new)), rightWidth).trimEnd()
|
|
1137
1174
|
: (newLines[index - 1] ?? "");
|
|
1138
1175
|
const left = truncateToWidth(leftSource, leftWidth);
|
|
1139
1176
|
const right = truncateToWidth(rightSource, rightWidth);
|
|
@@ -1208,39 +1245,6 @@ export class DiffPane {
|
|
|
1208
1245
|
return Array.from({ length: height }, (_, index) => content[index - top] ?? "");
|
|
1209
1246
|
}
|
|
1210
1247
|
|
|
1211
|
-
#assetTitle(label: string, side: FileAssetSide): string {
|
|
1212
|
-
let kind: string;
|
|
1213
|
-
let lfs = false;
|
|
1214
|
-
switch (side.kind) {
|
|
1215
|
-
case "empty":
|
|
1216
|
-
return label;
|
|
1217
|
-
case "image":
|
|
1218
|
-
kind =
|
|
1219
|
-
side.image.sourceMimeType === "image/svg+xml"
|
|
1220
|
-
? "SVG"
|
|
1221
|
-
: side.image.sourceMimeType.replace(/^image\//, "").toUpperCase();
|
|
1222
|
-
lfs = side.image.lfsOid !== undefined;
|
|
1223
|
-
break;
|
|
1224
|
-
case "text":
|
|
1225
|
-
kind = "Text";
|
|
1226
|
-
lfs = side.lfsOid !== undefined;
|
|
1227
|
-
break;
|
|
1228
|
-
case "binary":
|
|
1229
|
-
kind = "Binary";
|
|
1230
|
-
lfs = side.lfsOid !== undefined;
|
|
1231
|
-
break;
|
|
1232
|
-
case "tooLarge":
|
|
1233
|
-
kind = "Too large";
|
|
1234
|
-
lfs = side.lfsOid !== undefined;
|
|
1235
|
-
break;
|
|
1236
|
-
case "lfsMissing":
|
|
1237
|
-
kind = "LFS missing";
|
|
1238
|
-
lfs = true;
|
|
1239
|
-
break;
|
|
1240
|
-
}
|
|
1241
|
-
return `${label} · ${kind}${lfs ? " · Git LFS" : ""}`;
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
1248
|
#renderStreaming(width: number, height: number): string[] {
|
|
1245
1249
|
const streaming = this.#streaming;
|
|
1246
1250
|
if (!streaming) return [];
|