@linxiraos/pi-tui 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +188 -125
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES.txt +55 -5
- package/dist/types/app-keybindings.d.ts +18 -8
- package/dist/types/apps/git/diff-pane.d.ts +5 -0
- package/dist/types/apps/git/git-tui.d.ts +16 -2
- package/dist/types/apps/git/state.d.ts +11 -0
- package/dist/types/autocomplete.d.ts +2 -0
- package/dist/types/charts/chart-svg.d.ts +7 -13
- package/dist/types/chat/advisor-message.d.ts +4 -1
- package/dist/types/chat/assistant-message.d.ts +9 -4
- package/dist/types/chat/figure-markdown.d.ts +7 -2
- package/dist/types/chat/messages.d.ts +3 -1
- package/dist/types/chat/read-tool-group.d.ts +2 -2
- package/dist/types/chat/table-chart.d.ts +11 -15
- 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/chrome/diff.d.ts +2 -3
- package/dist/types/chrome/transcript-container.d.ts +2 -0
- package/dist/types/components/image.d.ts +2 -0
- package/dist/types/components/markdown.d.ts +49 -3
- package/dist/types/components/text.d.ts +2 -0
- package/dist/types/debug-server.d.ts +2 -0
- package/dist/types/keybinding-matchers.d.ts +6 -0
- package/dist/types/multiplexers/tmux.d.ts +1 -0
- package/dist/types/multiplexers/types.d.ts +6 -0
- package/dist/types/native/backend.d.ts +5 -4
- package/dist/types/native/blobs.d.ts +24 -12
- package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
- package/dist/types/overlays/annotation-overlay.d.ts +19 -0
- package/dist/types/overlays/approval-dialog.d.ts +79 -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/job-tree.d.ts +50 -0
- package/dist/types/overlays/jobs-panel.d.ts +25 -11
- 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/model-picker.d.ts +2 -0
- package/dist/types/overlays/pause-screen.d.ts +5 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +4 -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/session-selector.d.ts +3 -1
- package/dist/types/overlays/usage-dashboard.d.ts +6 -6
- package/dist/types/prompt/composer.d.ts +21 -20
- package/dist/types/prompt/custom-editor.d.ts +20 -10
- package/dist/types/prompt/image-references.d.ts +22 -0
- package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
- package/dist/types/prompt/macos-spelling.d.ts +2 -1
- package/dist/types/prompt/prose-gate.d.ts +3 -0
- package/dist/types/prompt/queued-messages.d.ts +1 -1
- package/dist/types/prompt/typing-word.d.ts +25 -0
- package/dist/types/prompt/welcome.d.ts +26 -6
- package/dist/types/render/hanging-wrap.d.ts +7 -0
- package/dist/types/render/hyperlink.d.ts +2 -0
- package/dist/types/render/render-utils.d.ts +9 -1
- package/dist/types/render/utils.d.ts +1 -1
- package/dist/types/setup/scenes/splash.d.ts +6 -5
- package/dist/types/status-line/context-usage.d.ts +4 -1
- package/dist/types/status-line/host.d.ts +6 -4
- package/dist/types/status-line/startup.d.ts +6 -6
- package/dist/types/status-line/types.d.ts +3 -0
- package/dist/types/stdin-buffer.d.ts +7 -0
- package/dist/types/terminal-capabilities.d.ts +16 -1
- package/dist/types/terminal-multiplexer.d.ts +7 -0
- package/dist/types/terminal.d.ts +8 -4
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme.d.ts +11 -0
- package/dist/types/thinking.d.ts +5 -0
- package/dist/types/tmux.d.ts +39 -4
- package/dist/types/tools/ast-edit.d.ts +2 -2
- package/dist/types/tools/ast-grep.d.ts +2 -2
- package/dist/types/tools/bash.d.ts +6 -1
- package/dist/types/tools/edit.d.ts +2 -0
- package/dist/types/tools/eval.d.ts +53 -4
- package/dist/types/tools/find.d.ts +14 -4
- package/dist/types/tools/glob.d.ts +3 -3
- package/dist/types/tools/grep.d.ts +2 -2
- package/dist/types/tools/grouped-file-output.d.ts +9 -4
- package/dist/types/tools/native-view.d.ts +1 -1
- package/dist/types/tools/read.d.ts +32 -10
- package/dist/types/tools/renderer.d.ts +2 -0
- package/dist/types/tools/task.d.ts +31 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/tui.d.ts +20 -3
- package/package.json +10 -10
- package/src/app-keybindings.d.ts +18 -8
- package/src/app-keybindings.ts +20 -8
- package/src/apps/autoresearch-dashboard.ts +444 -27
- package/src/apps/debug/raw-sse.ts +2 -2
- package/src/apps/git/diff-pane.ts +39 -41
- package/src/apps/git/git-tui.ts +80 -14
- package/src/apps/git/sidebar.ts +10 -1
- package/src/apps/git/state.ts +12 -0
- package/src/autocomplete.d.ts +2 -0
- package/src/autocomplete.ts +21 -19
- package/src/bracketed-paste.ts +9 -2
- package/src/charts/chart-svg.d.ts +7 -13
- package/src/charts/chart-svg.ts +19 -22
- package/src/chat/advisor-message.d.ts +4 -1
- package/src/chat/advisor-message.ts +77 -24
- package/src/chat/assistant-message.d.ts +9 -4
- package/src/chat/assistant-message.ts +127 -31
- package/src/chat/bash-execution.ts +7 -2
- package/src/chat/figure-markdown.d.ts +7 -2
- package/src/chat/figure-markdown.ts +10 -3
- package/src/chat/image-loading.ts +9 -6
- package/src/chat/messages.d.ts +3 -1
- package/src/chat/messages.ts +4 -1
- package/src/chat/read-tool-group.d.ts +2 -2
- package/src/chat/read-tool-group.ts +97 -42
- package/src/chat/svg-figure.ts +3 -1
- package/src/chat/svg-source.ts +6 -3
- package/src/chat/table-chart.d.ts +11 -15
- package/src/chat/table-chart.ts +24 -57
- package/src/chat/tool-execution.d.ts +32 -20
- package/src/chat/tool-execution.ts +103 -36
- package/src/chat/user-message.d.ts +3 -2
- package/src/chat/user-message.ts +54 -34
- package/src/chrome/countdown-timer.ts +3 -3
- package/src/chrome/diff.d.ts +2 -3
- package/src/chrome/diff.ts +4 -9
- package/src/chrome/transcript-container.d.ts +2 -0
- package/src/chrome/transcript-container.ts +212 -28
- package/src/components/editor.ts +12 -12
- package/src/components/image.d.ts +2 -0
- package/src/components/image.ts +54 -20
- package/src/components/markdown.d.ts +49 -3
- package/src/components/markdown.ts +461 -11
- package/src/components/settings-list.ts +2 -4
- package/src/components/text.d.ts +2 -0
- package/src/components/text.ts +11 -1
- package/src/debug-server.d.ts +2 -0
- package/src/debug-server.ts +51 -69
- package/src/hotkeys-markdown.ts +2 -0
- package/src/keybinding-matchers.d.ts +6 -0
- package/src/keybinding-matchers.ts +19 -0
- package/src/multiplexers/tmux.d.ts +1 -0
- package/src/multiplexers/tmux.ts +3 -0
- package/src/multiplexers/types.d.ts +6 -0
- package/src/multiplexers/types.ts +6 -0
- package/src/native/backend.d.ts +5 -4
- package/src/native/backend.ts +13 -5
- package/src/native/blobs.d.ts +24 -12
- package/src/native/blobs.ts +56 -7
- package/src/native/reconcile.ts +43 -21
- package/src/overlays/advisor-config.ts +259 -8
- package/src/overlays/agent-hub-projection.d.ts +10 -5
- package/src/overlays/agent-hub-projection.ts +63 -12
- package/src/overlays/annotation-overlay.ts +422 -168
- package/src/overlays/approval-dialog.ts +389 -0
- package/src/overlays/ask-dialog.ts +244 -105
- package/src/overlays/btw-history-panel.ts +208 -147
- package/src/overlays/extensions/extension-list.ts +4 -6
- package/src/overlays/hook-editor.ts +90 -32
- package/src/overlays/hook-input.ts +105 -19
- package/src/overlays/job-tree.ts +177 -0
- package/src/overlays/jobs-panel.ts +222 -142
- package/src/overlays/login-dialog.ts +201 -91
- package/src/overlays/mcp-add-wizard.ts +562 -279
- package/src/overlays/model-browser.ts +32 -7
- package/src/overlays/model-hub.ts +4 -10
- package/src/overlays/model-picker.ts +58 -21
- package/src/overlays/pause-screen.ts +81 -23
- package/src/overlays/plan-review-overlay.ts +16 -0
- 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 +199 -76
- package/src/overlays/session-selector.ts +15 -10
- package/src/overlays/settings-selector.ts +3 -0
- package/src/overlays/show-images-selector.ts +1 -0
- package/src/overlays/snapcompact-shape-preview.ts +13 -6
- 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 +498 -144
- package/src/prompt/composer-cache.ts +1 -1
- package/src/prompt/composer.ts +194 -74
- package/src/prompt/custom-editor.ts +168 -65
- package/src/prompt/image-references.d.ts +22 -0
- package/src/prompt/image-references.ts +79 -2
- package/src/prompt/internal-url-autocomplete.ts +1 -0
- package/src/prompt/macos-spelling.ts +28 -5
- package/src/prompt/prose-gate.ts +3 -0
- package/src/prompt/queued-messages.ts +9 -2
- package/src/prompt/tips.txt +1 -2
- package/src/prompt/typing-word.ts +59 -0
- package/src/prompt/welcome.ts +97 -45
- package/src/render/code-cell.ts +18 -5
- package/src/render/hanging-wrap.d.ts +7 -0
- package/src/render/hanging-wrap.ts +58 -0
- package/src/render/hyperlink.d.ts +2 -0
- package/src/render/hyperlink.ts +5 -0
- package/src/render/render-utils.d.ts +9 -1
- package/src/render/render-utils.ts +16 -2
- package/src/render/utils.d.ts +1 -1
- package/src/render/utils.ts +1 -1
- package/src/render/width-aware-text.ts +6 -5
- package/src/setup/scenes/splash.ts +22 -8
- package/src/status-line/component.ts +23 -10
- package/src/status-line/context-usage.ts +176 -73
- package/src/status-line/footer.ts +18 -35
- package/src/status-line/host.ts +6 -1
- package/src/status-line/segments.ts +16 -5
- package/src/status-line/startup.ts +6 -6
- package/src/status-line/types.ts +1 -0
- package/src/stdin-buffer.d.ts +7 -0
- package/src/stdin-buffer.ts +11 -1
- package/src/terminal-capabilities.d.ts +16 -1
- package/src/terminal-capabilities.ts +66 -28
- package/src/terminal-multiplexer.d.ts +7 -0
- package/src/terminal-multiplexer.ts +14 -0
- package/src/terminal.d.ts +8 -4
- package/src/terminal.ts +160 -46
- package/src/theme/symbols.d.ts +1 -1
- package/src/theme/symbols.ts +1 -0
- package/src/theme/theme-class.ts +1 -0
- package/src/theme/theme.d.ts +11 -0
- package/src/theme/theme.ts +46 -14
- package/src/thinking.d.ts +5 -0
- package/src/thinking.ts +9 -1
- package/src/tmux.d.ts +39 -4
- package/src/tmux.ts +110 -16
- package/src/tools/ask.ts +20 -9
- package/src/tools/ast-edit.d.ts +2 -2
- package/src/tools/ast-edit.ts +30 -7
- package/src/tools/ast-grep.d.ts +2 -2
- package/src/tools/ast-grep.ts +39 -35
- package/src/tools/bash.d.ts +6 -1
- package/src/tools/bash.ts +15 -9
- package/src/tools/debug.ts +2 -1
- package/src/tools/default-renderer.ts +3 -2
- package/src/tools/edit.d.ts +2 -0
- package/src/tools/edit.ts +100 -21
- package/src/tools/eval.d.ts +53 -4
- package/src/tools/eval.ts +173 -137
- package/src/tools/find.d.ts +14 -4
- package/src/tools/find.ts +71 -18
- package/src/tools/glob.d.ts +3 -3
- package/src/tools/glob.ts +51 -17
- package/src/tools/grep.d.ts +2 -2
- package/src/tools/grep.ts +99 -22
- package/src/tools/grouped-file-output.d.ts +9 -4
- package/src/tools/grouped-file-output.ts +21 -6
- package/src/tools/lsp.ts +30 -18
- package/src/tools/mcp.ts +2 -2
- package/src/tools/native-view.d.ts +1 -1
- package/src/tools/native-view.ts +2 -2
- package/src/tools/read.d.ts +32 -10
- package/src/tools/read.ts +89 -43
- package/src/tools/renderer.d.ts +2 -0
- package/src/tools/renderer.ts +2 -0
- package/src/tools/task.d.ts +31 -0
- package/src/tools/task.ts +40 -7
- package/src/tools/todo.ts +3 -2
- package/src/tools/wait.ts +16 -30
- package/src/tools/web-search.ts +2 -1
- package/src/tools/write.d.ts +2 -0
- package/src/tools/write.ts +36 -12
- package/src/tools/yield.ts +2 -2
- package/src/tui.d.ts +20 -3
- package/src/tui.ts +140 -36
- package/src/utils.ts +5 -3
- package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
- package/src/tools/eval-format/code-mode-declarations.ts +0 -61
|
@@ -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 "@linxiraos/pi-wire";
|
|
18
|
+
import { fmtNum, formatNum, type ExperimentResult, type ExperimentState } from "../tools/autoresearch";
|
|
19
|
+
import type { TspScrollBy, TspSpan, TspTableColumn, TspText } from "@linxiraos/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,17 @@ 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 {
|
|
157
|
+
return {
|
|
158
|
+
role: "zeta.overlay.autoresearch",
|
|
159
|
+
head: describeSheetHead(runtime),
|
|
160
|
+
size: "lg" as const,
|
|
161
|
+
};
|
|
155
162
|
},
|
|
156
163
|
describe(cx: DescribeContext): NativeNode {
|
|
157
164
|
const state = runtime.state;
|
|
@@ -159,6 +166,7 @@ export function createDashboardController(): DashboardController {
|
|
|
159
166
|
return native.get(
|
|
160
167
|
[
|
|
161
168
|
chart,
|
|
169
|
+
scroll,
|
|
162
170
|
state,
|
|
163
171
|
state.results.length,
|
|
164
172
|
state.results.at(-1),
|
|
@@ -171,29 +179,9 @@ export function createDashboardController(): DashboardController {
|
|
|
171
179
|
runtime.runningExperiment,
|
|
172
180
|
],
|
|
173
181
|
() => {
|
|
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
|
-
);
|
|
182
|
+
const children = describeSheet(runtime, chart);
|
|
195
183
|
children.push(actionBar([null, actionButton("Close", "close", { keys: "escape" })]));
|
|
196
|
-
return col(children, { gap: "md", role: "zeta.app.autoresearch" });
|
|
184
|
+
return { ...col(children, { gap: "md", role: "zeta.app.autoresearch" }), scroll };
|
|
197
185
|
},
|
|
198
186
|
);
|
|
199
187
|
},
|
|
@@ -223,19 +211,27 @@ export function createDashboardController(): DashboardController {
|
|
|
223
211
|
done(undefined);
|
|
224
212
|
return;
|
|
225
213
|
}
|
|
214
|
+
let by: TspScrollBy | undefined;
|
|
226
215
|
if (matchesKey(data, "up") || matchesKey(data, "k")) {
|
|
216
|
+
by = "line-up";
|
|
227
217
|
scrollView?.scroll(-1);
|
|
228
218
|
} else if (matchesKey(data, "down") || matchesKey(data, "j")) {
|
|
219
|
+
by = "line-down";
|
|
229
220
|
scrollView?.scroll(1);
|
|
230
221
|
} else if (matchesKey(data, "pageUp")) {
|
|
222
|
+
by = "page-up";
|
|
231
223
|
scrollView?.page(-1);
|
|
232
224
|
} else if (matchesKey(data, "pageDown")) {
|
|
225
|
+
by = "page-down";
|
|
233
226
|
scrollView?.page(1);
|
|
234
227
|
} else if (data === "g") {
|
|
228
|
+
by = "start";
|
|
235
229
|
scrollView?.scrollToTop();
|
|
236
230
|
} else if (data === "G") {
|
|
231
|
+
by = "end";
|
|
237
232
|
scrollView?.scrollToBottom();
|
|
238
233
|
}
|
|
234
|
+
if (by) scroll = { by, n: (scroll?.n ?? 0) + 1 };
|
|
239
235
|
tui.requestRender();
|
|
240
236
|
},
|
|
241
237
|
invalidate(): void {},
|
|
@@ -513,6 +509,427 @@ function describeDashboard(runtime: AutoresearchDashboardRuntime, maxRows: numbe
|
|
|
513
509
|
return [keyed(col(children, { gap: "sm" }), "dashboard")];
|
|
514
510
|
}
|
|
515
511
|
|
|
512
|
+
// -- native sheet --------------------------------------------------------------
|
|
513
|
+
|
|
514
|
+
/** Sheet head: the experiment's name, then what the sheet is. */
|
|
515
|
+
function describeSheetHead(runtime: AutoresearchDashboardRuntime): TspSpan[] {
|
|
516
|
+
const name = runtime.state.name?.trim();
|
|
517
|
+
return name ? [span(replaceTabs(name)), span(" · Autoresearch", "muted")] : [span("Autoresearch")];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const STATUS_LABEL: Record<ExperimentResult["status"], string> = {
|
|
521
|
+
keep: "kept",
|
|
522
|
+
discard: "discarded",
|
|
523
|
+
crash: "crashed",
|
|
524
|
+
checks_failed: "checks failed",
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const STATUS_TOKEN: Record<ExperimentResult["status"], string> = {
|
|
528
|
+
keep: "success",
|
|
529
|
+
discard: "dim",
|
|
530
|
+
crash: "error",
|
|
531
|
+
checks_failed: "error",
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
/** Fraction digits (0–2) that show every value exactly, so numbers that sit together align. */
|
|
535
|
+
function fractionDigits(values: readonly number[]): number {
|
|
536
|
+
let digits = 0;
|
|
537
|
+
for (const value of values) {
|
|
538
|
+
while (digits < 2 && Math.abs(Math.round(value * 10 ** digits) - value * 10 ** digits) > 1e-6) digits += 1;
|
|
539
|
+
}
|
|
540
|
+
return digits;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** A metric split into its number and unit, so the unit can sit a step quieter. */
|
|
544
|
+
function metricSpans(value: number, unit: string, digits: number, numberStyle?: string): TspSpan[] {
|
|
545
|
+
const spans = [span(fmtNum(value, digits), numberStyle)];
|
|
546
|
+
if (unit) spans.push(span(unit, "muted"));
|
|
547
|
+
return spans;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Signed change from `base` to `value` in percent (`−12.4%`), or null when there is nothing to compare. */
|
|
551
|
+
function percentChange(value: number, base: number | null | undefined): string | null {
|
|
552
|
+
if (base === null || base === undefined || base === 0 || value === base) return null;
|
|
553
|
+
const delta = ((value - base) / base) * 100;
|
|
554
|
+
return `${delta > 0 ? "+" : "−"}${Math.abs(delta).toFixed(1)}%`;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** What the sheet's mode says, with the span token it reads in. */
|
|
558
|
+
function modeLabel(runtime: AutoresearchDashboardRuntime): [label: string, token: string] {
|
|
559
|
+
const status = renderModeStatus(runtime, runtime.state);
|
|
560
|
+
switch (status) {
|
|
561
|
+
case "mode on":
|
|
562
|
+
return ["Mode on", "success"];
|
|
563
|
+
case "baseline pending":
|
|
564
|
+
return ["Baseline pending", "warning"];
|
|
565
|
+
case "segment complete":
|
|
566
|
+
return ["Segment complete", "muted"];
|
|
567
|
+
default:
|
|
568
|
+
return ["Mode off", "muted"];
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* The overlay's body: the goal and setup, the headline numbers (best,
|
|
574
|
+
* baseline, confidence, runs), what is happening now, the metric per run and
|
|
575
|
+
* the runs newest first. Empty segments explain what comes next instead.
|
|
576
|
+
*/
|
|
577
|
+
function describeSheet(runtime: AutoresearchDashboardRuntime, chart: boolean): NativeNode[] {
|
|
578
|
+
const state = runtime.state;
|
|
579
|
+
const current = currentResults(state.results, state.currentSegment);
|
|
580
|
+
const archived = state.results.length - current.length;
|
|
581
|
+
const children: NativeNode[] = [describeIntro(runtime, archived > 0)];
|
|
582
|
+
const now = describeNow(runtime);
|
|
583
|
+
if (now) children.push(now);
|
|
584
|
+
if (current.length === 0) {
|
|
585
|
+
children.push(describeEmpty(runtime, archived));
|
|
586
|
+
return children;
|
|
587
|
+
}
|
|
588
|
+
children.push(describeStats(runtime, current));
|
|
589
|
+
const indexed = state.results
|
|
590
|
+
.map((result, index) => ({ result, index }))
|
|
591
|
+
.filter(({ result }) => result.segment === state.currentSegment);
|
|
592
|
+
if (chart && indexed.length >= 2) children.push(describeTrend(runtime, indexed));
|
|
593
|
+
children.push(describeRuns(runtime, indexed));
|
|
594
|
+
if (archived > 0) {
|
|
595
|
+
children.push(
|
|
596
|
+
keyed(
|
|
597
|
+
text([span(`${archived} ${archived === 1 ? "run" : "runs"} archived from earlier segments`)], {
|
|
598
|
+
role: "zeta.autoresearch.note",
|
|
599
|
+
}),
|
|
600
|
+
"archived",
|
|
601
|
+
),
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
return children;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** The goal as a sentence, then one quiet line of setup: mode, metric and direction, segment, branch. */
|
|
608
|
+
function describeIntro(runtime: AutoresearchDashboardRuntime, segmented: boolean): NativeNode {
|
|
609
|
+
const state = runtime.state;
|
|
610
|
+
const children: NativeNode[] = [];
|
|
611
|
+
const goal = state.goal?.trim();
|
|
612
|
+
if (goal) children.push(keyed(text(replaceTabs(goal), { wrap: "word", role: "zeta.autoresearch.goal" }), "goal"));
|
|
613
|
+
const [mode, modeToken] = modeLabel(runtime);
|
|
614
|
+
const meta: TspSpan[] = [
|
|
615
|
+
span("● ", modeToken),
|
|
616
|
+
span(mode),
|
|
617
|
+
span(" · "),
|
|
618
|
+
span(state.metricName, "mono"),
|
|
619
|
+
span(`, ${state.bestDirection} is better`),
|
|
620
|
+
];
|
|
621
|
+
if (segmented) meta.push(span(" · "), span(`Segment ${state.currentSegment + 1}`));
|
|
622
|
+
if (state.maxExperiments !== null) meta.push(span(" · "), span(`${state.maxExperiments} runs per segment`));
|
|
623
|
+
if (state.branch) meta.push(span(" · "), span(replaceTabs(state.branch), "mono"));
|
|
624
|
+
children.push(keyed(text(meta, { truncate: "end", role: "zeta.autoresearch.meta" }), "meta"));
|
|
625
|
+
return keyed(col(children, { gap: "xs", role: "zeta.autoresearch.intro" }), "intro");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** What is happening right now: a benchmark running, or a finished run waiting to be logged. */
|
|
629
|
+
function describeNow(runtime: AutoresearchDashboardRuntime): NativeNode | undefined {
|
|
630
|
+
const state = runtime.state;
|
|
631
|
+
const running = runtime.runningExperiment;
|
|
632
|
+
if (running) {
|
|
633
|
+
return keyed(
|
|
634
|
+
row(
|
|
635
|
+
[
|
|
636
|
+
node("spinner", { label: [span("Running")], tone: "pending" }),
|
|
637
|
+
elapsed(Date.now() - running.startedAt),
|
|
638
|
+
text([span(replaceTabs(running.command), "mono")], {
|
|
639
|
+
truncate: "end",
|
|
640
|
+
role: "zeta.autoresearch.command",
|
|
641
|
+
}),
|
|
642
|
+
],
|
|
643
|
+
{ gap: "sm", align: "center", role: "zeta.autoresearch.running", tone: "pending" },
|
|
644
|
+
),
|
|
645
|
+
"now",
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
const pending = runtime.lastRunSummary;
|
|
649
|
+
if (!pending) return undefined;
|
|
650
|
+
const result: TspSpan[] = [span(`Run #${pending.runNumber} ${pending.passed ? "passed" : "failed"}`, "strong")];
|
|
651
|
+
if (pending.parsedPrimary !== null) {
|
|
652
|
+
result.push(
|
|
653
|
+
span(` · ${state.metricName} `),
|
|
654
|
+
...metricSpans(pending.parsedPrimary, state.metricUnit, fractionDigits([pending.parsedPrimary]), "mono"),
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
return keyed(
|
|
658
|
+
row(
|
|
659
|
+
[
|
|
660
|
+
text(result, { truncate: "end" }),
|
|
661
|
+
text([span("Log it with "), span("log_experiment", "mono"), span(" before the next run")], {
|
|
662
|
+
truncate: "end",
|
|
663
|
+
role: "zeta.autoresearch.next",
|
|
664
|
+
}),
|
|
665
|
+
],
|
|
666
|
+
{ gap: "md", align: "center", role: "zeta.autoresearch.pending", tone: "warning" },
|
|
667
|
+
),
|
|
668
|
+
"now",
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** No runs in this segment yet: what the dashboard is waiting for. */
|
|
673
|
+
function describeEmpty(runtime: AutoresearchDashboardRuntime, archived: number): NativeNode {
|
|
674
|
+
const [title, detail] = runtime.lastRunSummary
|
|
675
|
+
? ["Waiting for the first log", "Log the finished run to set this segment's baseline."]
|
|
676
|
+
: runtime.runningExperiment
|
|
677
|
+
? ["Measuring the baseline", "Every later run is compared with this first one."]
|
|
678
|
+
: runtime.autoresearchMode
|
|
679
|
+
? [
|
|
680
|
+
"No baseline yet",
|
|
681
|
+
"Run the benchmark once and log it as the baseline. Every later run is compared with it.",
|
|
682
|
+
]
|
|
683
|
+
: ["No experiments logged yet", "Turn autoresearch mode on to start measuring."];
|
|
684
|
+
const children: NativeNode[] = [
|
|
685
|
+
text([span(title)], { role: "zeta.autoresearch.empty.title" }),
|
|
686
|
+
text([span(detail)], { wrap: "word", role: "zeta.autoresearch.empty.detail" }),
|
|
687
|
+
];
|
|
688
|
+
if (archived > 0) {
|
|
689
|
+
children.push(
|
|
690
|
+
text([span(`${archived} ${archived === 1 ? "run" : "runs"} archived from earlier segments`)], {
|
|
691
|
+
role: "zeta.autoresearch.note",
|
|
692
|
+
}),
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
return keyed(col(children, { gap: "xs", align: "center", role: "zeta.autoresearch.empty" }), "empty");
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** One headline number: a label, the value, and one line saying what it means. */
|
|
699
|
+
function stat(key: string, label: string, value: TspSpan[], detail: NativeNode[]): NativeNode {
|
|
700
|
+
return keyed(
|
|
701
|
+
col(
|
|
702
|
+
[
|
|
703
|
+
text(label, { role: "zeta.autoresearch.stat.k" }),
|
|
704
|
+
text(value, { wrap: "none", truncate: "end", role: "zeta.autoresearch.stat.v" }),
|
|
705
|
+
...detail,
|
|
706
|
+
],
|
|
707
|
+
{ role: "zeta.autoresearch.stat" },
|
|
708
|
+
),
|
|
709
|
+
key,
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function statDetail(spans: TspSpan[]): NativeNode {
|
|
714
|
+
return text(spans, { truncate: "end", role: "zeta.autoresearch.stat.sub" });
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** Best, baseline, confidence and the run tally, then the secondary metrics at the best run. */
|
|
718
|
+
function describeStats(runtime: AutoresearchDashboardRuntime, current: readonly ExperimentResult[]): NativeNode {
|
|
719
|
+
const state = runtime.state;
|
|
720
|
+
const unit = state.metricUnit;
|
|
721
|
+
const best = findBestResult(state);
|
|
722
|
+
const baseline = findBaselineMetric(state.results, state.currentSegment);
|
|
723
|
+
const baselineRun = findBaselineRunNumber(state.results, state.currentSegment);
|
|
724
|
+
const baselineResult = current.find(result => result.status === "keep" && !result.flagged);
|
|
725
|
+
const counts = statusCounts(current);
|
|
726
|
+
const digits = fractionDigits(
|
|
727
|
+
[best?.result.metric, baseline].filter((value): value is number => value !== undefined && value !== null),
|
|
728
|
+
);
|
|
729
|
+
const tiles: NativeNode[] = [];
|
|
730
|
+
|
|
731
|
+
if (best) {
|
|
732
|
+
const run = best.result.runNumber ?? best.index + 1;
|
|
733
|
+
const change = percentChange(best.result.metric, baseline);
|
|
734
|
+
const better = baseline !== null && isBetter(best.result.metric, baseline, state.bestDirection);
|
|
735
|
+
tiles.push(
|
|
736
|
+
stat("best", "Best", metricSpans(best.result.metric, unit, digits), [
|
|
737
|
+
statDetail(
|
|
738
|
+
change
|
|
739
|
+
? [span(change, better ? "success" : "error"), span(` vs baseline · run #${run}`)]
|
|
740
|
+
: [span(`The baseline · run #${run}`)],
|
|
741
|
+
),
|
|
742
|
+
]),
|
|
743
|
+
);
|
|
744
|
+
} else {
|
|
745
|
+
tiles.push(stat("best", "Best", [span("—", "muted")], [statDetail([span("No kept run yet")])]));
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const baselineDetail: TspSpan[] = [];
|
|
749
|
+
if (baselineRun !== null) baselineDetail.push(span(`Run #${baselineRun}`));
|
|
750
|
+
if (baselineResult?.commit) baselineDetail.push(span(" · "), span(baselineResult.commit.slice(0, 7), "mono"));
|
|
751
|
+
tiles.push(
|
|
752
|
+
stat("baseline", "Baseline", baseline === null ? [span("—", "muted")] : metricSpans(baseline, unit, digits), [
|
|
753
|
+
statDetail(baselineDetail.length > 0 ? baselineDetail : [span("Not logged yet")]),
|
|
754
|
+
]),
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
const confidence = state.confidence;
|
|
758
|
+
tiles.push(
|
|
759
|
+
stat(
|
|
760
|
+
"confidence",
|
|
761
|
+
"Confidence",
|
|
762
|
+
confidence === null
|
|
763
|
+
? [span("—", "muted")]
|
|
764
|
+
: [span(confidence.toFixed(1), confidenceToken(confidence)), span("×", "muted")],
|
|
765
|
+
[
|
|
766
|
+
statDetail([
|
|
767
|
+
span(
|
|
768
|
+
confidence === null
|
|
769
|
+
? "Needs three runs and a gain"
|
|
770
|
+
: confidence >= 2
|
|
771
|
+
? "Gain is clear of run noise"
|
|
772
|
+
: confidence >= 1
|
|
773
|
+
? "Gain is near run noise"
|
|
774
|
+
: "Gain is within run noise",
|
|
775
|
+
),
|
|
776
|
+
]),
|
|
777
|
+
],
|
|
778
|
+
),
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
const total = Math.max(state.maxExperiments ?? 0, current.length);
|
|
782
|
+
const failed = counts.crash + counts.checks_failed;
|
|
783
|
+
const tally: TspSpan[] = [span(`${counts.keep} kept · ${counts.discard} discarded`)];
|
|
784
|
+
if (failed > 0) tally.push(span(" · "), span(`${failed} failed`, "error"));
|
|
785
|
+
tiles.push(
|
|
786
|
+
stat(
|
|
787
|
+
"runs",
|
|
788
|
+
"Runs",
|
|
789
|
+
state.maxExperiments !== null
|
|
790
|
+
? [span(String(current.length)), span(` / ${state.maxExperiments}`, "muted")]
|
|
791
|
+
: [span(String(current.length))],
|
|
792
|
+
[
|
|
793
|
+
node("meter", {
|
|
794
|
+
value: current.length / total,
|
|
795
|
+
parts: [
|
|
796
|
+
{ value: counts.keep / total, token: "success", label: `${counts.keep} kept` },
|
|
797
|
+
{ value: counts.discard / total, token: "muted", label: `${counts.discard} discarded` },
|
|
798
|
+
{
|
|
799
|
+
value: failed / total,
|
|
800
|
+
token: "error",
|
|
801
|
+
label: `${counts.crash} crashed · ${counts.checks_failed} checks failed`,
|
|
802
|
+
},
|
|
803
|
+
],
|
|
804
|
+
size: "sm",
|
|
805
|
+
role: "zeta.autoresearch.tally",
|
|
806
|
+
}),
|
|
807
|
+
statDetail(tally),
|
|
808
|
+
],
|
|
809
|
+
),
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
const children: NativeNode[] = [row(tiles, { role: "zeta.autoresearch.stats" })];
|
|
813
|
+
if (best && state.secondaryMetrics.length > 0) {
|
|
814
|
+
const baselineSecondary = findBaselineSecondary(state.results, state.currentSegment, state.secondaryMetrics);
|
|
815
|
+
const spans: TspSpan[] = [];
|
|
816
|
+
for (const metric of state.secondaryMetrics) {
|
|
817
|
+
const value = best.result.metrics[metric.name];
|
|
818
|
+
if (value === undefined) continue;
|
|
819
|
+
spans.push(span(spans.length === 0 ? "At best " : " · "));
|
|
820
|
+
spans.push(
|
|
821
|
+
span(`${metric.name} `, "mono muted"),
|
|
822
|
+
...metricSpans(value, metric.unit, fractionDigits([value]), "mono"),
|
|
823
|
+
);
|
|
824
|
+
const change = percentChange(value, baselineSecondary[metric.name]);
|
|
825
|
+
if (change) spans.push(span(` ${change}`, "mono"));
|
|
826
|
+
}
|
|
827
|
+
if (spans.length > 0) {
|
|
828
|
+
children.push(keyed(text(spans, { truncate: "end", role: "zeta.autoresearch.secondary" }), "secondary"));
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return keyed(col(children, { role: "zeta.autoresearch.summary" }), "summary");
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** The primary metric per run, in run order. */
|
|
835
|
+
function describeTrend(
|
|
836
|
+
runtime: AutoresearchDashboardRuntime,
|
|
837
|
+
indexed: readonly { result: ExperimentResult; index: number }[],
|
|
838
|
+
): NativeNode {
|
|
839
|
+
const state = runtime.state;
|
|
840
|
+
return node(
|
|
841
|
+
"chart",
|
|
842
|
+
{
|
|
843
|
+
kind: "bars",
|
|
844
|
+
series: indexed.map(({ result, index }) => {
|
|
845
|
+
const run = result.runNumber ?? index + 1;
|
|
846
|
+
return {
|
|
847
|
+
label: `#${run}`,
|
|
848
|
+
value: result.metric,
|
|
849
|
+
title: `#${run} ${STATUS_LABEL[result.status]} · ${formatNum(result.metric, state.metricUnit)} · ${replaceTabs(result.description)}`,
|
|
850
|
+
};
|
|
851
|
+
}),
|
|
852
|
+
token: "accent",
|
|
853
|
+
summary: [span(`${state.metricName} per run, ${state.bestDirection} is better`)],
|
|
854
|
+
size: "md",
|
|
855
|
+
role: "zeta.autoresearch.trend",
|
|
856
|
+
},
|
|
857
|
+
undefined,
|
|
858
|
+
"trend",
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** Every run of the segment, newest first: outcome, metric and its change from the baseline, commit, what changed. */
|
|
863
|
+
function describeRuns(
|
|
864
|
+
runtime: AutoresearchDashboardRuntime,
|
|
865
|
+
indexed: readonly { result: ExperimentResult; index: number }[],
|
|
866
|
+
): NativeNode {
|
|
867
|
+
const state = runtime.state;
|
|
868
|
+
const baseline = findBaselineResult(state.results, state.currentSegment);
|
|
869
|
+
const best = findBestResult(state);
|
|
870
|
+
// Cells carry bare numbers at one precision per column; the unit goes in the head unless the name says it.
|
|
871
|
+
const head = (name: string, unit: string): TspSpan[] =>
|
|
872
|
+
unit && !name.toLowerCase().endsWith(`_${unit.toLowerCase()}`)
|
|
873
|
+
? [span(name, "mono"), span(` ${unit}`, "muted")]
|
|
874
|
+
: [span(name, "mono")];
|
|
875
|
+
const measured = indexed.filter(({ result }) => result.status !== "crash").map(({ result }) => result);
|
|
876
|
+
const metricDigits = fractionDigits(measured.map(result => result.metric));
|
|
877
|
+
const secondaryDigits = state.secondaryMetrics.map(metric =>
|
|
878
|
+
fractionDigits(
|
|
879
|
+
measured.map(result => result.metrics[metric.name]).filter((value): value is number => value !== undefined),
|
|
880
|
+
),
|
|
881
|
+
);
|
|
882
|
+
const cols: TspTableColumn[] = [
|
|
883
|
+
{ id: "run", head: "#", align: "end", priority: 3 },
|
|
884
|
+
{ id: "status", head: "Outcome", priority: 4 },
|
|
885
|
+
{ id: "metric", head: head(state.metricName, state.metricUnit), align: "end", priority: 5 },
|
|
886
|
+
{ id: "change", head: "vs baseline", align: "end", priority: 4 },
|
|
887
|
+
...state.secondaryMetrics.map((metric, index): TspTableColumn => ({
|
|
888
|
+
id: `secondary${index}`,
|
|
889
|
+
head: head(metric.name, metric.unit),
|
|
890
|
+
align: "end",
|
|
891
|
+
priority: 1,
|
|
892
|
+
})),
|
|
893
|
+
{ id: "commit", head: "Commit", priority: 2 },
|
|
894
|
+
{ id: "description", head: "Change", truncate: "end", grow: 1, priority: 3 },
|
|
895
|
+
];
|
|
896
|
+
const rows = indexed
|
|
897
|
+
.slice()
|
|
898
|
+
.reverse()
|
|
899
|
+
.map(({ result, index }) => {
|
|
900
|
+
const isBest = best?.index === index;
|
|
901
|
+
const isBaseline = result === baseline;
|
|
902
|
+
const crashed = result.status === "crash";
|
|
903
|
+
const change = isBaseline || crashed ? null : percentChange(result.metric, baseline?.metric);
|
|
904
|
+
const better = baseline !== null && isBetter(result.metric, baseline.metric, state.bestDirection);
|
|
905
|
+
const cells: Record<string, TspText> = {
|
|
906
|
+
run: [span(String(result.runNumber ?? index + 1), "mono")],
|
|
907
|
+
status: isBest
|
|
908
|
+
? [span("● ", "success"), span("best", "strong")]
|
|
909
|
+
: [span("● ", STATUS_TOKEN[result.status]), span(STATUS_LABEL[result.status])],
|
|
910
|
+
metric: crashed
|
|
911
|
+
? [span("—", "muted")]
|
|
912
|
+
: [span(fmtNum(result.metric, metricDigits), isBest ? "mono strong" : "mono")],
|
|
913
|
+
change: isBaseline
|
|
914
|
+
? [span("baseline", "muted")]
|
|
915
|
+
: change
|
|
916
|
+
? [span(change, `mono ${result.status === "keep" ? (better ? "success" : "error") : "muted"}`)]
|
|
917
|
+
: [span("—", "muted")],
|
|
918
|
+
commit: [span(result.commit ? result.commit.slice(0, 7) : "—", result.commit ? "mono" : "muted")],
|
|
919
|
+
description: [span(replaceTabs(result.description))],
|
|
920
|
+
};
|
|
921
|
+
state.secondaryMetrics.forEach((metric, metricIndex) => {
|
|
922
|
+
const value = result.metrics[metric.name];
|
|
923
|
+
cells[`secondary${metricIndex}`] =
|
|
924
|
+
value === undefined
|
|
925
|
+
? [span("—", "muted")]
|
|
926
|
+
: [span(fmtNum(value, secondaryDigits[metricIndex] ?? 0), "mono")];
|
|
927
|
+
});
|
|
928
|
+
return { id: String(index), cells };
|
|
929
|
+
});
|
|
930
|
+
return node("table", { cols, rows, role: "zeta.autoresearch.runs" }, undefined, "runs");
|
|
931
|
+
}
|
|
932
|
+
|
|
516
933
|
function statusCounts(results: readonly ExperimentResult[]): Record<ExperimentResult["status"], number> {
|
|
517
934
|
const counts: Record<ExperimentResult["status"], number> = { keep: 0, discard: 0, crash: 0, checks_failed: 0 };
|
|
518
935
|
for (const result of results) counts[result.status] += 1;
|
|
@@ -245,7 +245,7 @@ export class RawSseViewerComponent implements Component {
|
|
|
245
245
|
/** The action bar's buttons run the same code as their keys. */
|
|
246
246
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
247
247
|
if (event.type !== "action") return;
|
|
248
|
-
if (event.act === "copy") this.#copyAll();
|
|
248
|
+
if (event.act === "copy-raw") this.#copyAll();
|
|
249
249
|
else if (event.act === "close") this.#close();
|
|
250
250
|
}
|
|
251
251
|
|
|
@@ -305,7 +305,7 @@ export class RawSseViewerComponent implements Component {
|
|
|
305
305
|
"status",
|
|
306
306
|
),
|
|
307
307
|
actionBar([
|
|
308
|
-
actionButton("Copy raw", "copy", { keys: "ctrl+c", tone: "accent" }),
|
|
308
|
+
actionButton("Copy raw", "copy-raw", { keys: "ctrl+c", tone: "accent" }),
|
|
309
309
|
null,
|
|
310
310
|
actionButton("Close", "close", { keys: "escape" }),
|
|
311
311
|
]),
|
|
@@ -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 = tuiText("gitKindText", "Text");
|
|
592
|
+
lfs = side.lfsOid !== undefined;
|
|
593
|
+
break;
|
|
594
|
+
case "binary":
|
|
595
|
+
kind = tuiText("gitKindBinary", "Binary");
|
|
596
|
+
lfs = side.lfsOid !== undefined;
|
|
597
|
+
break;
|
|
598
|
+
case "tooLarge":
|
|
599
|
+
kind = tuiText("gitKindTooLarge", "Too large");
|
|
600
|
+
lfs = side.lfsOid !== undefined;
|
|
601
|
+
break;
|
|
602
|
+
case "lfsMissing":
|
|
603
|
+
kind = tuiText("gitKindLfsMissing", "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,17 +1166,11 @@ export class DiffPane {
|
|
|
1129
1166
|
for (let index = 0; index < height; index++) {
|
|
1130
1167
|
const leftSource =
|
|
1131
1168
|
index === 0
|
|
1132
|
-
? centerLine(
|
|
1133
|
-
theme.bold(this.#assetTitle(tuiText("gitAssetBefore", "Before"), asset.old)),
|
|
1134
|
-
leftWidth,
|
|
1135
|
-
).trimEnd()
|
|
1169
|
+
? centerLine(theme.bold(assetTitle(tuiText("gitAssetBefore", "Before"), asset.old)), leftWidth).trimEnd()
|
|
1136
1170
|
: (oldLines[index - 1] ?? "");
|
|
1137
1171
|
const rightSource =
|
|
1138
1172
|
index === 0
|
|
1139
|
-
? centerLine(
|
|
1140
|
-
theme.bold(this.#assetTitle(tuiText("gitAssetAfter", "After"), asset.new)),
|
|
1141
|
-
rightWidth,
|
|
1142
|
-
).trimEnd()
|
|
1173
|
+
? centerLine(theme.bold(assetTitle(tuiText("gitAssetAfter", "After"), asset.new)), rightWidth).trimEnd()
|
|
1143
1174
|
: (newLines[index - 1] ?? "");
|
|
1144
1175
|
const left = truncateToWidth(leftSource, leftWidth);
|
|
1145
1176
|
const right = truncateToWidth(rightSource, rightWidth);
|
|
@@ -1218,39 +1249,6 @@ export class DiffPane {
|
|
|
1218
1249
|
return Array.from({ length: height }, (_, index) => content[index - top] ?? "");
|
|
1219
1250
|
}
|
|
1220
1251
|
|
|
1221
|
-
#assetTitle(label: string, side: FileAssetSide): string {
|
|
1222
|
-
let kind: string;
|
|
1223
|
-
let lfs = false;
|
|
1224
|
-
switch (side.kind) {
|
|
1225
|
-
case "empty":
|
|
1226
|
-
return label;
|
|
1227
|
-
case "image":
|
|
1228
|
-
kind =
|
|
1229
|
-
side.image.sourceMimeType === "image/svg+xml"
|
|
1230
|
-
? "SVG"
|
|
1231
|
-
: side.image.sourceMimeType.replace(/^image\//, "").toUpperCase();
|
|
1232
|
-
lfs = side.image.lfsOid !== undefined;
|
|
1233
|
-
break;
|
|
1234
|
-
case "text":
|
|
1235
|
-
kind = tuiText("gitKindText", "Text");
|
|
1236
|
-
lfs = side.lfsOid !== undefined;
|
|
1237
|
-
break;
|
|
1238
|
-
case "binary":
|
|
1239
|
-
kind = tuiText("gitKindBinary", "Binary");
|
|
1240
|
-
lfs = side.lfsOid !== undefined;
|
|
1241
|
-
break;
|
|
1242
|
-
case "tooLarge":
|
|
1243
|
-
kind = tuiText("gitKindTooLarge", "Too large");
|
|
1244
|
-
lfs = side.lfsOid !== undefined;
|
|
1245
|
-
break;
|
|
1246
|
-
case "lfsMissing":
|
|
1247
|
-
kind = tuiText("gitKindLfsMissing", "LFS missing");
|
|
1248
|
-
lfs = true;
|
|
1249
|
-
break;
|
|
1250
|
-
}
|
|
1251
|
-
return `${label} · ${kind}${lfs ? " · Git LFS" : ""}`;
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
1252
|
#renderStreaming(width: number, height: number): string[] {
|
|
1255
1253
|
const streaming = this.#streaming;
|
|
1256
1254
|
if (!streaming) return [];
|