@oh-my-pi/pi-tui 18.8.7 → 18.8.8
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 +28 -64
- package/dist/types/chat/execution-shared.d.ts +6 -5
- package/dist/types/keybinding-matchers.d.ts +3 -0
- package/dist/types/overlays/annotation-overlay.d.ts +12 -0
- package/dist/types/overlays/annotation-types.d.ts +2 -0
- package/dist/types/overlays/hook-selector.d.ts +9 -4
- package/dist/types/overlays/model-browser.d.ts +2 -2
- package/dist/types/overlays/model-hub.d.ts +21 -4
- package/dist/types/overlays/session-observer-registry.d.ts +2 -0
- package/dist/types/overlays/usage-dashboard.d.ts +3 -1
- package/dist/types/status-line/types.d.ts +1 -1
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/tools/eval.d.ts +1 -1
- package/dist/types/tools/native-view.d.ts +47 -0
- package/dist/types/tools/renderer.d.ts +15 -2
- package/dist/types/tui.d.ts +20 -0
- package/dist/types/vim.d.ts +19 -5
- package/package.json +9 -9
- package/src/chat/execution-shared.ts +42 -25
- package/src/chat/tool-execution.ts +9 -2
- package/src/components/editor.ts +182 -5
- package/src/keybinding-matchers.ts +6 -2
- package/src/overlays/agent-hub.ts +7 -3
- package/src/overlays/annotation-overlay.ts +87 -6
- package/src/overlays/annotation-types.ts +2 -0
- package/src/overlays/hook-selector.ts +12 -5
- package/src/overlays/model-browser.ts +3 -3
- package/src/overlays/model-hub.ts +107 -27
- package/src/overlays/session-observer-registry.ts +10 -2
- package/src/overlays/usage-dashboard.ts +70 -23
- package/src/prompt/composer.ts +1 -0
- package/src/status-line/segments.ts +3 -0
- package/src/status-line/types.ts +1 -1
- package/src/theme/symbols.ts +5 -0
- package/src/tools/bash.ts +82 -31
- package/src/tools/eval.ts +149 -123
- package/src/tools/native-view.ts +102 -1
- package/src/tools/renderer.ts +15 -2
- package/src/tui.ts +68 -0
- package/src/vim.ts +333 -18
package/src/tools/bash.ts
CHANGED
|
@@ -25,9 +25,9 @@ import type {
|
|
|
25
25
|
RenderResultOptions,
|
|
26
26
|
ToolRenderer,
|
|
27
27
|
} from "./renderer";
|
|
28
|
-
import {
|
|
29
|
-
import
|
|
30
|
-
import {
|
|
28
|
+
import { INTENT_FIELD, type TspTone } from "@oh-my-pi/pi-wire";
|
|
29
|
+
import { plainText } from "../native/spans";
|
|
30
|
+
import { resultText, runBox, runFoot, runInput, runOutput, type RunState } from "./native-view";
|
|
31
31
|
|
|
32
32
|
/** Default collapsed shell output preview height. */
|
|
33
33
|
export const BASH_DEFAULT_PREVIEW_LINES = DEFAULT_TERMINAL_PREVIEW_LINES;
|
|
@@ -367,34 +367,41 @@ function bashStatsParts(
|
|
|
367
367
|
}
|
|
368
368
|
|
|
369
369
|
/**
|
|
370
|
-
* Native head facts of a shell call: title, the
|
|
371
|
-
*
|
|
372
|
-
*
|
|
370
|
+
* Native head facts of a shell call: title, the model's intent as the target
|
|
371
|
+
* (none without one), the command line for Copy command, the `exit N` chip
|
|
372
|
+
* and a `timed out` note only when the deadline hit. The command itself and a
|
|
373
|
+
* backgrounded job's state draw in the run box.
|
|
373
374
|
*/
|
|
374
375
|
function shellToolHead(
|
|
375
376
|
title: string,
|
|
376
377
|
command: string,
|
|
378
|
+
intent: string | undefined,
|
|
377
379
|
details: BashToolDetails | undefined,
|
|
378
380
|
settled: boolean,
|
|
379
381
|
): NativeToolHead {
|
|
380
382
|
const exit = settled && details?.exitCode !== undefined && details.exitCode !== 0 ? details.exitCode : undefined;
|
|
381
383
|
return {
|
|
382
384
|
title,
|
|
383
|
-
target:
|
|
384
|
-
targetKind: "
|
|
385
|
-
|
|
386
|
-
badges:
|
|
387
|
-
details?.async?.state === "running"
|
|
388
|
-
? [{ text: "background", title: `Backgrounded as job ${details.async.jobId}` }]
|
|
389
|
-
: undefined,
|
|
385
|
+
target: intent,
|
|
386
|
+
targetKind: intent ? "text" : undefined,
|
|
387
|
+
command,
|
|
390
388
|
exit,
|
|
391
389
|
note: settled && details?.timedOut === true ? "timed out" : undefined,
|
|
392
390
|
};
|
|
393
391
|
}
|
|
394
392
|
|
|
395
|
-
/** The
|
|
393
|
+
/** The model's intent line (`i` arg) as plain head text, or undefined when absent. */
|
|
394
|
+
function callIntent(args: unknown): string | undefined {
|
|
395
|
+
if (!args || typeof args !== "object" || !(INTENT_FIELD in args)) return undefined;
|
|
396
|
+
const value = args[INTENT_FIELD];
|
|
397
|
+
if (typeof value !== "string") return undefined;
|
|
398
|
+
return plainText(value).trim() || undefined;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** The run foot's facts under shell output: background job, service state and the full-output artifact. */
|
|
396
402
|
function shellFootParts(details: BashToolDetails | undefined, artifactId: string | undefined): string[] {
|
|
397
403
|
const parts: string[] = [];
|
|
404
|
+
if (details?.async?.state === "running") parts.push(`Job ${details.async.jobId}`);
|
|
398
405
|
const service = details?.service;
|
|
399
406
|
if (service) {
|
|
400
407
|
parts.push(`Service ${service.name}`, service.state);
|
|
@@ -405,6 +412,27 @@ function shellFootParts(details: BashToolDetails | undefined, artifactId: string
|
|
|
405
412
|
return parts;
|
|
406
413
|
}
|
|
407
414
|
|
|
415
|
+
/** The command line as a run's input: wrapped shell source. */
|
|
416
|
+
const SHELL_INPUT = { role: "omp.tool.bash.command", lang: "bash", wrap: true } as const;
|
|
417
|
+
|
|
418
|
+
/** Box tint per run state: failed runs read as errors, timed-out ones as warnings. */
|
|
419
|
+
const RUN_BOX_TONE: Partial<Record<RunState, TspTone>> = { failed: "error", "timed-out": "warning" };
|
|
420
|
+
|
|
421
|
+
/** The foot state of a shell call from its result. */
|
|
422
|
+
function shellRunState(
|
|
423
|
+
details: BashToolDetails | undefined,
|
|
424
|
+
isError: boolean,
|
|
425
|
+
isPartial: boolean,
|
|
426
|
+
cancelled: boolean,
|
|
427
|
+
): RunState {
|
|
428
|
+
if (cancelled) return "cancelled";
|
|
429
|
+
if (isPartial) return "running";
|
|
430
|
+
if (details?.timedOut === true) return "timed-out";
|
|
431
|
+
if (details?.async?.state === "running") return "background";
|
|
432
|
+
if (isError || (details?.exitCode !== undefined && details.exitCode !== 0)) return "failed";
|
|
433
|
+
return "done";
|
|
434
|
+
}
|
|
435
|
+
|
|
408
436
|
function toBashRenderArgs<TArgs>(args: TArgs | undefined, config: ShellRendererConfig<TArgs>): BashRenderArgs {
|
|
409
437
|
return {
|
|
410
438
|
command: config.resolveCommand?.(args),
|
|
@@ -610,10 +638,21 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
610
638
|
},
|
|
611
639
|
describeCall(args: TArgs, options: RenderResultOptions): NativeToolView {
|
|
612
640
|
const command = formatBashCommandText(toBashRenderArgs(args, config));
|
|
613
|
-
//
|
|
641
|
+
// No foot until the command starts; once it runs (before any output) the clock shows.
|
|
642
|
+
const started = options.executionStarted === true;
|
|
614
643
|
return {
|
|
615
|
-
tool: shellToolHead(config.resolveTitle(args, options), command, undefined, false),
|
|
616
|
-
|
|
644
|
+
tool: shellToolHead(config.resolveTitle(args, options), command, callIntent(args), undefined, false),
|
|
645
|
+
body: [
|
|
646
|
+
runBox(
|
|
647
|
+
[
|
|
648
|
+
runInput(command, SHELL_INPUT),
|
|
649
|
+
started &&
|
|
650
|
+
runFoot({ state: options.cancelled ? "cancelled" : "running", elapsedMs: options.elapsedMs }),
|
|
651
|
+
],
|
|
652
|
+
{ key: "run" },
|
|
653
|
+
),
|
|
654
|
+
],
|
|
655
|
+
preview: "children",
|
|
617
656
|
};
|
|
618
657
|
},
|
|
619
658
|
|
|
@@ -637,22 +676,34 @@ export function createShellRenderer<TArgs>(config: ShellRendererConfig<TArgs>) {
|
|
|
637
676
|
const showingFullOutput = expanded && renderContext?.isFullOutput === true;
|
|
638
677
|
const command = formatBashCommandText(toBashRenderArgs(args, config));
|
|
639
678
|
const previewLines = renderContext?.previewLines ?? BASH_DEFAULT_PREVIEW_LINES;
|
|
640
|
-
|
|
641
|
-
//
|
|
679
|
+
const state = shellRunState(details, isError, isPartial, options.cancelled === true);
|
|
680
|
+
// Settled: the measured wall time wins. A background job outlives the call: its run time is unknown here.
|
|
681
|
+
const elapsedMs =
|
|
682
|
+
state === "background"
|
|
683
|
+
? undefined
|
|
684
|
+
: isPartial
|
|
685
|
+
? options.elapsedMs
|
|
686
|
+
: (details?.wallTimeMs ?? options.elapsedMs);
|
|
642
687
|
const meta = details?.meta;
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
688
|
+
const box = runBox(
|
|
689
|
+
[
|
|
690
|
+
runInput(command, SHELL_INPUT),
|
|
691
|
+
output.trim().length > 0 && runOutput(output, { role: "omp.tool.bash.output", previewLines }),
|
|
692
|
+
runFoot({
|
|
693
|
+
state,
|
|
694
|
+
exitCode: details?.exitCode,
|
|
695
|
+
elapsedMs,
|
|
696
|
+
facts: shellFootParts(details, stripped.artifactId),
|
|
697
|
+
meta: { ...meta, truncation: showingFullOutput ? undefined : meta?.truncation },
|
|
698
|
+
}),
|
|
699
|
+
],
|
|
700
|
+
{ key: "run", tone: RUN_BOX_TONE[state] },
|
|
701
|
+
);
|
|
651
702
|
return {
|
|
652
|
-
tool: shellToolHead(config.resolveTitle(args, options), command, details, !isPartial),
|
|
653
|
-
body,
|
|
654
|
-
tone:
|
|
655
|
-
preview:
|
|
703
|
+
tool: shellToolHead(config.resolveTitle(args, options), command, callIntent(args), details, !isPartial),
|
|
704
|
+
body: [box],
|
|
705
|
+
tone: state === "timed-out" ? "warning" : undefined,
|
|
706
|
+
preview: "children",
|
|
656
707
|
};
|
|
657
708
|
},
|
|
658
709
|
mergeCallAndResult: true,
|
package/src/tools/eval.ts
CHANGED
|
@@ -9,10 +9,10 @@ import type {
|
|
|
9
9
|
ToolRenderer,
|
|
10
10
|
} from "./renderer";
|
|
11
11
|
import type { TspSpan } from "@oh-my-pi/pi-wire";
|
|
12
|
-
import {
|
|
12
|
+
import { compact, keyed, md, node, span, text } from "../native/describe";
|
|
13
13
|
import type { NativeChild, NativeNode } from "../native/node";
|
|
14
14
|
import { plainText } from "../native/spans";
|
|
15
|
-
import {
|
|
15
|
+
import { resultText, runBox, runFoot, type RunFootInput, runInput, runOutput, type RunState } from "./native-view";
|
|
16
16
|
import { renderAgentTreeRow } from "./agent-tree";
|
|
17
17
|
import { truncateToVisualLines } from "../chrome/visual-truncate";
|
|
18
18
|
import { getMarkdownTheme, type Theme } from "../theme/theme";
|
|
@@ -694,89 +694,104 @@ function splitConsoleTables(output: string): EvalOutputPart[] {
|
|
|
694
694
|
return parts;
|
|
695
695
|
}
|
|
696
696
|
|
|
697
|
-
/**
|
|
698
|
-
|
|
697
|
+
/**
|
|
698
|
+
* One cell's outputs: markdown, else terminal text (following its tail,
|
|
699
|
+
* clamped to the last `previewLines` while folded) with any `console.table`
|
|
700
|
+
* blocks as tables.
|
|
701
|
+
*/
|
|
702
|
+
function evalOutputNodes(output: string, markdown: boolean, previewLines: number): NativeNode[] {
|
|
699
703
|
if (output.trim().length === 0) return [];
|
|
700
|
-
if (markdown
|
|
701
|
-
|
|
702
|
-
return splitConsoleTables(output).map(part =>
|
|
704
|
+
if (markdown) return [keyed(md(output), "output")];
|
|
705
|
+
return splitConsoleTables(output).map((part, i) =>
|
|
703
706
|
part.kind === "text"
|
|
704
|
-
?
|
|
705
|
-
: node(
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
id: `
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
707
|
+
? runOutput(part.text, { role: "omp.tool.eval.output", previewLines, key: i === 0 ? "output" : `output-${i}` })
|
|
708
|
+
: node(
|
|
709
|
+
"table",
|
|
710
|
+
{
|
|
711
|
+
// Node's index column header is noise in a real table.
|
|
712
|
+
cols: part.head.map((head, c) => ({ id: `c${c}`, head: head === "(index)" ? "" : head })),
|
|
713
|
+
rows: part.rows.map((cells, r) => ({
|
|
714
|
+
id: `r${r}`,
|
|
715
|
+
cells: Object.fromEntries(cells.map((cell, c) => [`c${c}`, cell])),
|
|
716
|
+
})),
|
|
717
|
+
role: "omp.tool.eval.table",
|
|
718
|
+
},
|
|
719
|
+
undefined,
|
|
720
|
+
`table-${i}`,
|
|
721
|
+
),
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** Status lines a folded cell keeps: the newest ones. */
|
|
726
|
+
const RUN_STATUS_TAIL = 8;
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* A cell's status events (`omp.run.status`): all of them when expanded, else
|
|
730
|
+
* the newest {@link RUN_STATUS_TAIL} after a muted `N earlier` line.
|
|
731
|
+
*/
|
|
732
|
+
function evalStatusSection(events: readonly EvalStatusEvent[], expanded: boolean): NativeNode | undefined {
|
|
733
|
+
if (events.length === 0) return undefined;
|
|
734
|
+
const hidden = expanded ? 0 : Math.max(0, events.length - RUN_STATUS_TAIL);
|
|
735
|
+
const lines = events.slice(hidden).map((event, i) => keyed(describeStatusEvent(event), `s${hidden + i}`));
|
|
736
|
+
return node(
|
|
737
|
+
"col",
|
|
738
|
+
{ role: "omp.run.status", gap: "none" },
|
|
739
|
+
compact<NativeNode>([hidden > 0 && keyed(text([span(`${hidden} earlier`, "muted")]), "earlier"), ...lines]),
|
|
740
|
+
"status",
|
|
714
741
|
);
|
|
715
742
|
}
|
|
716
743
|
|
|
717
|
-
/**
|
|
718
|
-
|
|
744
|
+
/** The foot state of a cell, or undefined while it has not started. */
|
|
745
|
+
function evalCellState(status: EvalCellResult["status"], cancelled: boolean): RunState | undefined {
|
|
746
|
+
switch (status) {
|
|
747
|
+
case "pending":
|
|
748
|
+
return undefined;
|
|
749
|
+
case "running":
|
|
750
|
+
return cancelled ? "cancelled" : "running";
|
|
751
|
+
case "complete":
|
|
752
|
+
return "done";
|
|
753
|
+
case "error":
|
|
754
|
+
return "failed";
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/** Inputs of one eval cell box. */
|
|
759
|
+
interface EvalCellRun {
|
|
719
760
|
readonly language: EvalLanguage;
|
|
720
761
|
readonly code: string;
|
|
721
762
|
readonly title?: string;
|
|
722
|
-
readonly
|
|
723
|
-
readonly
|
|
724
|
-
|
|
763
|
+
readonly outputs?: readonly NativeChild[];
|
|
764
|
+
readonly status?: NativeNode;
|
|
765
|
+
/** Omitted while the cell has not started. */
|
|
766
|
+
readonly foot?: RunFootInput;
|
|
725
767
|
}
|
|
726
768
|
|
|
727
769
|
/**
|
|
728
|
-
* A notebook cell
|
|
729
|
-
*
|
|
730
|
-
* Only multi-cell calls repeat titles; marks never invent execution counts.
|
|
770
|
+
* A notebook cell as one run box: the caption (multi-cell calls only), the
|
|
771
|
+
* source, its outputs, status lines and foot. A failed cell tints its box.
|
|
731
772
|
*/
|
|
732
|
-
function
|
|
733
|
-
let
|
|
773
|
+
function evalCellBox(cell: EvalCellRun, index: number, total: number): NativeNode {
|
|
774
|
+
let caption: NativeNode | undefined;
|
|
734
775
|
if (total > 1) {
|
|
735
|
-
|
|
776
|
+
const spans: TspSpan[] = [span(`${index + 1}/${total}`, "muted")];
|
|
736
777
|
const title = plainText(cell.title ?? "").trim();
|
|
737
|
-
if (title)
|
|
738
|
-
|
|
739
|
-
if (cell.durationMs !== undefined) head.push(span(` · ${(cell.durationMs / 1000).toFixed(2)}s`, "muted"));
|
|
778
|
+
if (title) spans.push(span(` ${title}`, "toolTitle"));
|
|
779
|
+
caption = keyed(text(spans, { role: "omp.run.caption" }), "caption");
|
|
740
780
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
role: "omp.tool.eval.
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
})
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
},
|
|
756
|
-
compact<NativeChild>([
|
|
757
|
-
head ? text(head, { role: "omp.tool.eval.caption" }) : undefined,
|
|
758
|
-
node(
|
|
759
|
-
"row",
|
|
760
|
-
{ role: "omp.tool.eval.input", align: "start" },
|
|
761
|
-
[
|
|
762
|
-
inputMark,
|
|
763
|
-
keyed(codeNode(cell.code, { lang: languageForHighlighter(cell.language), numbers: false }), "code"),
|
|
764
|
-
],
|
|
765
|
-
"input",
|
|
766
|
-
),
|
|
767
|
-
cell.output?.length
|
|
768
|
-
? node(
|
|
769
|
-
"row",
|
|
770
|
-
{ role: "omp.tool.eval.result", align: "start" },
|
|
771
|
-
[
|
|
772
|
-
node("icon", { name: "arrow-right", role: "omp.tool.eval.prompt", aria: "Output" }),
|
|
773
|
-
node("col", { role: "omp.tool.eval.outputs" }, cell.output),
|
|
774
|
-
],
|
|
775
|
-
"output",
|
|
776
|
-
)
|
|
777
|
-
: undefined,
|
|
778
|
-
]),
|
|
779
|
-
`cell-${index}`,
|
|
781
|
+
return runBox(
|
|
782
|
+
[
|
|
783
|
+
caption,
|
|
784
|
+
cell.code.trim().length > 0 &&
|
|
785
|
+
runInput(cell.code, {
|
|
786
|
+
role: "omp.tool.eval.input",
|
|
787
|
+
lang: languageForHighlighter(cell.language),
|
|
788
|
+
wrap: false,
|
|
789
|
+
}),
|
|
790
|
+
...(cell.outputs ?? []),
|
|
791
|
+
cell.status,
|
|
792
|
+
cell.foot && runFoot(cell.foot),
|
|
793
|
+
],
|
|
794
|
+
{ key: `cell-${index}`, tone: cell.foot?.state === "failed" ? "error" : undefined },
|
|
780
795
|
);
|
|
781
796
|
}
|
|
782
797
|
|
|
@@ -1129,16 +1144,20 @@ export const evalToolRenderer = {
|
|
|
1129
1144
|
};
|
|
1130
1145
|
},
|
|
1131
1146
|
|
|
1132
|
-
describeCall(args: EvalRenderArgs,
|
|
1147
|
+
describeCall(args: EvalRenderArgs, options: RenderResultOptions): NativeToolView {
|
|
1133
1148
|
const cells = getRenderCells(args);
|
|
1149
|
+
// No foot until the call starts; then the first cell runs (before any result names it).
|
|
1150
|
+
const startFoot: RunFootInput | undefined = options.executionStarted
|
|
1151
|
+
? { state: options.cancelled ? "cancelled" : "running", elapsedMs: options.elapsedMs }
|
|
1152
|
+
: undefined;
|
|
1134
1153
|
return {
|
|
1135
1154
|
tool: evalToolHead(
|
|
1136
1155
|
cells[0]?.title,
|
|
1137
1156
|
cells.map(cell => cell.language),
|
|
1138
1157
|
cells.length,
|
|
1139
1158
|
),
|
|
1140
|
-
body: cells.map((cell, i) =>
|
|
1141
|
-
preview:
|
|
1159
|
+
body: cells.map((cell, i) => evalCellBox({ ...cell, foot: i === 0 ? startFoot : undefined }, i, cells.length)),
|
|
1160
|
+
preview: "children",
|
|
1142
1161
|
};
|
|
1143
1162
|
},
|
|
1144
1163
|
|
|
@@ -1149,82 +1168,89 @@ export const evalToolRenderer = {
|
|
|
1149
1168
|
): NativeToolView {
|
|
1150
1169
|
const details = result.details;
|
|
1151
1170
|
const isPartial = options.isPartial === true;
|
|
1171
|
+
const cancelled = options.cancelled === true;
|
|
1172
|
+
const expanded = options.renderContext?.expanded ?? options.expanded;
|
|
1152
1173
|
const previewLines = options.renderContext?.previewLines ?? EVAL_DEFAULT_PREVIEW_LINES;
|
|
1153
1174
|
const jsonNodes = (details?.jsonOutputs ?? []).map((value, index) =>
|
|
1154
1175
|
keyed(describeJsonTree(value, { hiddenRootKeys: [] }), `display-${index}`),
|
|
1155
1176
|
);
|
|
1156
|
-
//
|
|
1157
|
-
const
|
|
1177
|
+
// The call's notice and truncation join the last run's foot facts (a background job is a head badge).
|
|
1178
|
+
const callFacts = { facts: compact([details?.notice]), meta: details?.meta };
|
|
1158
1179
|
const cellResults = details?.cells;
|
|
1159
|
-
let cells: NativeNode[];
|
|
1160
|
-
let head: NativeToolHead;
|
|
1161
1180
|
if (cellResults && cellResults.length > 0) {
|
|
1162
1181
|
const languages = cellResults.map(cell => cell.language ?? details?.language ?? "python");
|
|
1163
|
-
|
|
1164
|
-
|
|
1182
|
+
const states = cellResults.map(cell => evalCellState(cell.status, cancelled));
|
|
1183
|
+
// A cell that never started has no foot to carry the call's facts: the last one that ran does.
|
|
1184
|
+
const factsIndex = states.findLastIndex(state => state !== undefined);
|
|
1185
|
+
let earlierMs = 0;
|
|
1186
|
+
const cells = cellResults.map((cell, i) => {
|
|
1165
1187
|
const language = languages[i]!;
|
|
1166
|
-
|
|
1188
|
+
const state = states[i];
|
|
1189
|
+
// A running cell's clock is the call's minus the cells that finished before it.
|
|
1190
|
+
const elapsedMs =
|
|
1191
|
+
state === "running" || state === "cancelled"
|
|
1192
|
+
? options.elapsedMs === undefined
|
|
1193
|
+
? undefined
|
|
1194
|
+
: Math.max(0, options.elapsedMs - earlierMs)
|
|
1195
|
+
: cell.durationMs;
|
|
1196
|
+
earlierMs += cell.durationMs ?? 0;
|
|
1197
|
+
return evalCellBox(
|
|
1167
1198
|
{
|
|
1168
1199
|
language,
|
|
1169
1200
|
code: formatEvalCodeForDisplay(cell.code, language),
|
|
1170
1201
|
title: cell.title,
|
|
1171
|
-
|
|
1172
|
-
durationMs: cell.durationMs,
|
|
1173
|
-
output: [
|
|
1202
|
+
outputs: [
|
|
1174
1203
|
...evalOutputNodes(
|
|
1175
1204
|
cell.output,
|
|
1176
|
-
cell.hasMarkdown === true,
|
|
1177
|
-
|
|
1178
|
-
cell.status === "error",
|
|
1205
|
+
cell.hasMarkdown === true && cell.status !== "error",
|
|
1206
|
+
previewLines,
|
|
1179
1207
|
),
|
|
1180
|
-
...(cell.statusEvents ?? []).map(describeStatusEvent),
|
|
1181
1208
|
...(i === cellResults.length - 1 ? jsonNodes : []),
|
|
1182
1209
|
],
|
|
1210
|
+
status: evalStatusSection(cell.statusEvents ?? [], expanded),
|
|
1211
|
+
foot: state && {
|
|
1212
|
+
state,
|
|
1213
|
+
exitCode: cell.exitCode,
|
|
1214
|
+
elapsedMs,
|
|
1215
|
+
...(i === factsIndex ? callFacts : {}),
|
|
1216
|
+
},
|
|
1183
1217
|
},
|
|
1184
1218
|
i,
|
|
1185
1219
|
cellResults.length,
|
|
1186
1220
|
);
|
|
1187
1221
|
});
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1222
|
+
return {
|
|
1223
|
+
tool: evalToolHead(cellResults[0]!.title, languages, cellResults.length, details),
|
|
1224
|
+
body: cells,
|
|
1225
|
+
preview: "children",
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
// No per-cell results (older details): the call's cells, the whole output and foot under the last.
|
|
1229
|
+
const argCells = getRenderCells(args);
|
|
1230
|
+
const languages = details?.languages ?? (details?.language ? [details.language] : []);
|
|
1231
|
+
const rawOutput = options.renderContext?.output ?? resultText(result).trimEnd();
|
|
1232
|
+
const state: RunState = cancelled ? "cancelled" : isPartial ? "running" : result.isError ? "failed" : "done";
|
|
1233
|
+
const last: Omit<EvalCellRun, "language" | "code" | "title"> = {
|
|
1234
|
+
outputs: [
|
|
1235
|
+
...evalOutputNodes(stripOutputNotice(rawOutput, details?.meta).trimEnd(), false, previewLines),
|
|
1236
|
+
...jsonNodes,
|
|
1237
|
+
],
|
|
1238
|
+
status: evalStatusSection(details?.statusEvents ?? [], expanded),
|
|
1239
|
+
foot: { state, elapsedMs: options.elapsedMs, ...callFacts },
|
|
1240
|
+
};
|
|
1241
|
+
const runs: EvalCellRun[] =
|
|
1242
|
+
argCells.length > 0
|
|
1243
|
+
? argCells.map((cell, i) => (i === argCells.length - 1 ? { ...cell, ...last } : cell))
|
|
1244
|
+
: [{ language: languages[0] ?? "python", code: "", ...last }];
|
|
1245
|
+
return {
|
|
1246
|
+
tool: evalToolHead(
|
|
1193
1247
|
argCells[0]?.title,
|
|
1194
1248
|
argCells.length > 0 ? argCells.map(cell => cell.language) : languages,
|
|
1195
1249
|
argCells.length,
|
|
1196
1250
|
details,
|
|
1197
|
-
)
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
...evalOutputNodes(
|
|
1201
|
-
stripOutputNotice(rawOutput, details?.meta).trimEnd(),
|
|
1202
|
-
false,
|
|
1203
|
-
isPartial,
|
|
1204
|
-
result.isError === true,
|
|
1205
|
-
),
|
|
1206
|
-
...(details?.statusEvents ?? []).map(describeStatusEvent),
|
|
1207
|
-
...jsonNodes,
|
|
1208
|
-
];
|
|
1209
|
-
cells =
|
|
1210
|
-
argCells.length > 0
|
|
1211
|
-
? argCells.map((cell, i) =>
|
|
1212
|
-
evalCellSection(
|
|
1213
|
-
{
|
|
1214
|
-
...cell,
|
|
1215
|
-
status: isPartial ? "running" : result.isError ? "error" : "complete",
|
|
1216
|
-
output: i === argCells.length - 1 ? output : undefined,
|
|
1217
|
-
},
|
|
1218
|
-
i,
|
|
1219
|
-
argCells.length,
|
|
1220
|
-
),
|
|
1221
|
-
)
|
|
1222
|
-
: output;
|
|
1223
|
-
}
|
|
1224
|
-
return {
|
|
1225
|
-
tool: head,
|
|
1226
|
-
body: compact<NativeChild>([...cells, footer]),
|
|
1227
|
-
preview: { lines: previewLines },
|
|
1251
|
+
),
|
|
1252
|
+
body: runs.map((run, i) => evalCellBox(run, i, runs.length)),
|
|
1253
|
+
preview: "children",
|
|
1228
1254
|
};
|
|
1229
1255
|
},
|
|
1230
1256
|
|
package/src/tools/native-view.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { pathToFileURL } from "node:url";
|
|
8
8
|
import type { TspSpan, TspText, TspTone } from "@oh-my-pi/pi-wire";
|
|
9
|
-
import { code, keyed, node, row, span, text } from "../native/describe";
|
|
9
|
+
import { ansi, code, compact, keyed, node, row, span, text } from "../native/describe";
|
|
10
10
|
import type { NativeChild, NativeNode } from "../native/node";
|
|
11
11
|
import { type ParsedDiagnostic, parseDiagnosticMessage, shortenPath } from "../render/render-utils";
|
|
12
12
|
import { plainText } from "../native/spans";
|
|
@@ -189,6 +189,107 @@ export function footnoteText(parts: readonly string[], meta?: OutputMeta): Nativ
|
|
|
189
189
|
};
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
/** Drawn lines of a run's input (command, cell code) before its "N more lines" button. */
|
|
193
|
+
export const RUN_INPUT_PREVIEW_LINES = 6;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* What a run's foot names: `running` (live clock), `background` (an async job
|
|
197
|
+
* outlives the call), and the settled outcomes. A failure with an exit code
|
|
198
|
+
* reads `exit N`; without one, `Failed`.
|
|
199
|
+
*/
|
|
200
|
+
export type RunState = "running" | "background" | "done" | "failed" | "cancelled" | "timed-out";
|
|
201
|
+
|
|
202
|
+
const RUN_STATE: Record<RunState, { readonly word: string; readonly tone: TspTone }> = {
|
|
203
|
+
running: { word: "Running", tone: "pending" },
|
|
204
|
+
background: { word: "In background", tone: "pending" },
|
|
205
|
+
done: { word: "Done", tone: "success" },
|
|
206
|
+
failed: { word: "Failed", tone: "error" },
|
|
207
|
+
cancelled: { word: "Cancelled", tone: "muted" },
|
|
208
|
+
"timed-out": { word: "Timed out", tone: "warning" },
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/** Inputs of {@link runFoot}. */
|
|
212
|
+
export interface RunFootInput {
|
|
213
|
+
readonly state: RunState;
|
|
214
|
+
/** Exit code of a `failed` run; a non-zero code names the state `exit N`. */
|
|
215
|
+
readonly exitCode?: number;
|
|
216
|
+
/** Milliseconds run so far (`running`) or in total; the time is omitted when undefined. */
|
|
217
|
+
readonly elapsedMs?: number;
|
|
218
|
+
/** Quiet facts (job id, service state, artifact), muted and ` · `-joined. */
|
|
219
|
+
readonly facts?: readonly string[];
|
|
220
|
+
/** Truncation / artifact-capture notices, appended to the facts in `warning`. */
|
|
221
|
+
readonly meta?: OutputMeta;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The foot line of a run box (`omp.run.foot`): the state word in its tone,
|
|
226
|
+
* the run time (a live clock while running, frozen once settled) and the
|
|
227
|
+
* quiet facts. Glyphs are the terminal's styling, never text.
|
|
228
|
+
*/
|
|
229
|
+
export function runFoot(input: RunFootInput): NativeNode {
|
|
230
|
+
const failedWithCode = input.state === "failed" && input.exitCode !== undefined && input.exitCode !== 0;
|
|
231
|
+
const state = failedWithCode
|
|
232
|
+
? { word: `exit ${input.exitCode}`, tone: RUN_STATE.failed.tone }
|
|
233
|
+
: RUN_STATE[input.state];
|
|
234
|
+
const facts = (input.facts ?? []).map(fact => plainText(fact).trim()).filter(fact => fact.length > 0);
|
|
235
|
+
const notices: string[] = [];
|
|
236
|
+
if (input.meta?.truncation) notices.push(formatTruncationMetaNotice(input.meta.truncation, input.meta.source));
|
|
237
|
+
if (input.meta?.artifactError) notices.push(formatArtifactErrorNotice(input.meta.artifactError));
|
|
238
|
+
const factSpans: TspSpan[] = [];
|
|
239
|
+
if (facts.length > 0) factSpans.push(span(facts.join(" · "), "muted"));
|
|
240
|
+
for (const notice of notices) {
|
|
241
|
+
if (factSpans.length > 0) factSpans.push(span(" · ", "muted"));
|
|
242
|
+
factSpans.push(span(notice, "warning"));
|
|
243
|
+
}
|
|
244
|
+
const ms = input.elapsedMs === undefined ? undefined : Math.max(0, Math.round(input.elapsedMs));
|
|
245
|
+
const time =
|
|
246
|
+
ms === undefined
|
|
247
|
+
? undefined
|
|
248
|
+
: node(
|
|
249
|
+
"elapsed",
|
|
250
|
+
input.state === "running"
|
|
251
|
+
? { role: "omp.run.time", age: ms, format: "short" }
|
|
252
|
+
: { role: "omp.run.time", age: ms, stopped: ms, format: "short" },
|
|
253
|
+
undefined,
|
|
254
|
+
"time",
|
|
255
|
+
);
|
|
256
|
+
return node(
|
|
257
|
+
"row",
|
|
258
|
+
{ role: "omp.run.foot", gap: "sm", align: "center" },
|
|
259
|
+
compact<NativeNode>([
|
|
260
|
+
keyed(text([span(state.word)], { role: "omp.run.state", tone: state.tone }), "state"),
|
|
261
|
+
time,
|
|
262
|
+
factSpans.length > 0 && keyed(text(factSpans, { role: "omp.run.facts", wrap: "word" }), "facts"),
|
|
263
|
+
]),
|
|
264
|
+
"foot",
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A run's input section: the command or cell source, clamped to {@link RUN_INPUT_PREVIEW_LINES}. */
|
|
269
|
+
export function runInput(source: string, p: { role: string; lang: string; wrap: boolean }): NativeNode {
|
|
270
|
+
return keyed(
|
|
271
|
+
code(source.trimEnd(), { role: p.role, lang: p.lang, wrap: p.wrap, preview: { lines: RUN_INPUT_PREVIEW_LINES } }),
|
|
272
|
+
"input",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** A run's terminal output: follows its tail, clamped to the last `previewLines` lines while folded. */
|
|
277
|
+
export function runOutput(output: string, p: { role: string; previewLines: number; key?: string }): NativeNode {
|
|
278
|
+
return keyed(ansi(output, { role: p.role, follow: true, preview: { lines: p.previewLines } }), p.key ?? "output");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* One run as one box (`omp.run`): input, output, status lines and foot in
|
|
283
|
+
* order. `tone` tints the box: `error` for a failed run, `warning` for a
|
|
284
|
+
* timed-out one.
|
|
285
|
+
*/
|
|
286
|
+
export function runBox(
|
|
287
|
+
children: readonly (NativeChild | undefined | false)[],
|
|
288
|
+
p: { key: string; tone?: TspTone },
|
|
289
|
+
): NativeNode {
|
|
290
|
+
return node("col", { role: "omp.run", gap: "none", tone: p.tone }, compact(children), p.key);
|
|
291
|
+
}
|
|
292
|
+
|
|
192
293
|
/** Diagnostic rows shown before `+N more`. */
|
|
193
294
|
const DIAGNOSTIC_ROWS = 5;
|
|
194
295
|
|