@oh-my-pi/pi-tui 18.12.0 → 18.12.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 +31 -76
- package/dist/types/chat/advisor-message.d.ts +4 -1
- package/dist/types/chat/messages.d.ts +3 -1
- package/dist/types/chrome/diff.d.ts +2 -3
- package/dist/types/chrome/status-notice.d.ts +2 -0
- package/dist/types/prompt/composer.d.ts +2 -0
- package/dist/types/tmux.d.ts +9 -0
- package/dist/types/tools/eval.d.ts +51 -7
- package/dist/types/tools/grouped-file-output.d.ts +3 -3
- package/dist/types/tools/read.d.ts +1 -1
- package/dist/types/tools/web-search-types.d.ts +1 -0
- package/package.json +9 -9
- package/src/charts/table-data.ts +17 -6
- package/src/chat/advisor-message.ts +77 -24
- package/src/chat/messages.ts +4 -1
- package/src/chat/read-tool-group.ts +2 -2
- package/src/chrome/diff.ts +4 -9
- package/src/chrome/status-notice.ts +5 -0
- package/src/components/markdown.ts +21 -1
- package/src/prompt/composer-cache.ts +3 -0
- package/src/prompt/composer.ts +8 -2
- package/src/status-line/component.ts +56 -1
- package/src/stdin-buffer.ts +289 -61
- package/src/terminal-capabilities.ts +7 -1
- package/src/terminal.ts +30 -17
- package/src/tmux.ts +30 -0
- package/src/tools/ast-edit.ts +2 -3
- package/src/tools/ast-grep.ts +1 -2
- package/src/tools/edit.ts +3 -3
- package/src/tools/eval.ts +132 -132
- package/src/tools/find.ts +6 -3
- package/src/tools/grep.ts +0 -2
- package/src/tools/grouped-file-output.ts +6 -5
- package/src/tools/read.ts +3 -3
- package/src/tools/web-search-types.ts +1 -0
- package/src/tools/write.ts +5 -3
- package/src/tui.ts +20 -7
package/src/tools/ast-grep.ts
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
toPathList,
|
|
15
15
|
} from "../render/render-utils";
|
|
16
16
|
import { classifyGroupedLines, describeGroupedOutput, groupLineIndicesByBlank } from "./grouped-file-output";
|
|
17
|
-
import { getLanguageFromPath } from "../lang-from-path";
|
|
18
17
|
import { code, compact, node, span } from "../native/describe";
|
|
19
18
|
import type { NativeChild } from "../native/node";
|
|
20
19
|
import { errorText, noteText, resultText, toolHead } from "./native-view";
|
|
@@ -279,7 +278,7 @@ export const astGrepToolRenderer = {
|
|
|
279
278
|
return {
|
|
280
279
|
head,
|
|
281
280
|
body: compact<NativeChild>([
|
|
282
|
-
...describeGroupedOutput(kept
|
|
281
|
+
...describeGroupedOutput(kept),
|
|
283
282
|
limitReached && noteText("limit reached; narrow path or use skip", "warning"),
|
|
284
283
|
parseNote,
|
|
285
284
|
]),
|
package/src/tools/edit.ts
CHANGED
|
@@ -1222,7 +1222,7 @@ export const editToolRenderer = {
|
|
|
1222
1222
|
diffText = args.previewDiff ?? args.diff ?? "";
|
|
1223
1223
|
body.push(editDiff(diffText, rawPath));
|
|
1224
1224
|
} else if (args.diff || args.newText || args.patch) {
|
|
1225
|
-
body.push(code(args.diff ?? args.newText ?? args.patch ?? "", {
|
|
1225
|
+
body.push(code(args.diff ?? args.newText ?? args.patch ?? "", { path: rawPath || undefined }));
|
|
1226
1226
|
} else if (renderContext?.editDiffPreview) {
|
|
1227
1227
|
const preview = renderContext.editDiffPreview;
|
|
1228
1228
|
if ("error" in preview && preview.error) body.push(errorText(preview.error));
|
|
@@ -1362,9 +1362,9 @@ function editToolHead(facts: EditHeadFacts): NativeToolHead {
|
|
|
1362
1362
|
};
|
|
1363
1363
|
}
|
|
1364
1364
|
|
|
1365
|
-
/** A file's diff: hunks
|
|
1365
|
+
/** A file's diff: Tern highlights the hunks by the file's grammar and draws no path header (the head or section names the file). */
|
|
1366
1366
|
function editDiff(diffText: string, filePath: string): NativeNode {
|
|
1367
|
-
return nativeDiff(diffText, {
|
|
1367
|
+
return nativeDiff(diffText, { path: filePath || undefined });
|
|
1368
1368
|
}
|
|
1369
1369
|
|
|
1370
1370
|
/** One file's edit result for native views: head facts plus its body (diff or error, then diagnostics). */
|
package/src/tools/eval.ts
CHANGED
|
@@ -36,7 +36,6 @@ import {
|
|
|
36
36
|
} from "./json-tree";
|
|
37
37
|
import { formatStyledTruncationWarning, stripOutputNotice } from "./output-meta";
|
|
38
38
|
import { splitPathAndSel } from "./read";
|
|
39
|
-
import { getLanguageFromPath } from "../lang-from-path";
|
|
40
39
|
import {
|
|
41
40
|
DEFAULT_TERMINAL_PREVIEW_LINES,
|
|
42
41
|
PREVIEW_LIMITS,
|
|
@@ -65,31 +64,57 @@ export interface EvalStatusEvent {
|
|
|
65
64
|
[key: string]: unknown;
|
|
66
65
|
}
|
|
67
66
|
|
|
67
|
+
/** One column of a table view. */
|
|
68
|
+
export interface EvalTableColumn {
|
|
69
|
+
head: string;
|
|
70
|
+
/** Numeric values: renderers right-align the column. */
|
|
71
|
+
numeric?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A pretty rendering of something a cell printed, shown to the user in place
|
|
76
|
+
* of its text; the model only ever sees the text.
|
|
77
|
+
* - `table`: a data frame or `console.table`; leading index columns are plain
|
|
78
|
+
* columns, and a truncated frame's elided rows/columns are `…` cells.
|
|
79
|
+
* - `json`: a structured value (dict/list, object/array) as a collapsible tree.
|
|
80
|
+
* - `markdown`: an object's Markdown representation.
|
|
81
|
+
*/
|
|
82
|
+
export type EvalView =
|
|
83
|
+
| { kind: "table"; columns: EvalTableColumn[]; rows: string[][] }
|
|
84
|
+
| { kind: "json"; value: unknown }
|
|
85
|
+
| { kind: "markdown"; text: string };
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A view and the exact printed text it stands for. Renderers locate `text` in
|
|
89
|
+
* the cell's `output` (entries in print order) and show the view there; an
|
|
90
|
+
* entry whose text is no longer in the output (truncated) is skipped.
|
|
91
|
+
*/
|
|
92
|
+
export interface EvalViewEntry {
|
|
93
|
+
text: string;
|
|
94
|
+
view: EvalView;
|
|
95
|
+
}
|
|
96
|
+
|
|
68
97
|
/** Per-cell execution result for transcript rendering. */
|
|
69
98
|
export interface EvalCellResult {
|
|
70
99
|
index: number;
|
|
71
100
|
title?: string;
|
|
72
101
|
code: string;
|
|
73
102
|
language?: EvalLanguage;
|
|
103
|
+
/** What the model saw: the cell's printed text plus image notes. */
|
|
74
104
|
output: string;
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
* from `jsonOutputs` instead. Absent when the cell displayed no JSON.
|
|
78
|
-
*/
|
|
79
|
-
textOutput?: string;
|
|
105
|
+
/** Pretty views of spans of `output`, in print order. */
|
|
106
|
+
views?: EvalViewEntry[];
|
|
80
107
|
status: "pending" | "running" | "complete" | "error";
|
|
81
108
|
durationMs?: number;
|
|
82
109
|
exitCode?: number;
|
|
83
110
|
statusEvents?: EvalStatusEvent[];
|
|
84
111
|
/** Discrete events dropped from the front of `statusEvents` (see {@link recordStatusEvent}). */
|
|
85
112
|
statusEventsElided?: number;
|
|
86
|
-
hasMarkdown?: boolean;
|
|
87
113
|
}
|
|
88
114
|
|
|
89
115
|
/** Tool result detail object surfaced to the UI/transcript. */
|
|
90
116
|
export interface EvalToolDetails {
|
|
91
117
|
cells?: EvalCellResult[];
|
|
92
|
-
jsonOutputs?: unknown[];
|
|
93
118
|
images?: ImageContent[];
|
|
94
119
|
statusEvents?: EvalStatusEvent[];
|
|
95
120
|
/** Discrete events dropped from the front of `statusEvents` (see {@link recordStatusEvent}). */
|
|
@@ -593,8 +618,7 @@ function formatCellOutputLines(
|
|
|
593
618
|
theme: Theme,
|
|
594
619
|
width: number,
|
|
595
620
|
): { lines: readonly string[]; hiddenCount: number } {
|
|
596
|
-
|
|
597
|
-
const output = cell.textOutput ?? cell.output;
|
|
621
|
+
const output = cell.output;
|
|
598
622
|
if (!output) {
|
|
599
623
|
return { lines: [], hiddenCount: 0 };
|
|
600
624
|
}
|
|
@@ -607,23 +631,47 @@ function formatCellOutputLines(
|
|
|
607
631
|
// exact hidden-count row, so the pane marker stays off here.
|
|
608
632
|
const innerWidth = outputBlockContentWidth(width);
|
|
609
633
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
634
|
+
const isError = cell.status === "error";
|
|
635
|
+
const styleText = (line: string): string => theme.fg(isError ? "error" : "toolOutput", replaceTabs(line));
|
|
636
|
+
// A table's printed text is already a terminal table, aligned to the line it starts on: keep it as text.
|
|
637
|
+
const parts = splitEvalOutput(
|
|
638
|
+
output,
|
|
639
|
+
cell.views?.filter(entry => entry.view.kind !== "table"),
|
|
640
|
+
);
|
|
641
|
+
if (parts.some(part => part.kind === "view")) {
|
|
642
|
+
// Printed text keeps its place; json and markdown views replace theirs with a tree / rendered Markdown.
|
|
643
|
+
const treeDepth = expanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
|
|
644
|
+
const treeLineCap = expanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
|
|
645
|
+
const treeScalarLen = expanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
|
|
646
|
+
const allLines: string[] = [];
|
|
647
|
+
for (const part of parts) {
|
|
648
|
+
if (part.kind === "text") {
|
|
649
|
+
for (const line of part.text.split("\n")) allLines.push(styleText(line));
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
const view = part.view;
|
|
653
|
+
if (view.kind === "json") {
|
|
654
|
+
const tree = renderJsonTreeLines(view.value, theme, treeDepth, treeLineCap, treeScalarLen);
|
|
655
|
+
allLines.push(...tree.lines);
|
|
656
|
+
if (tree.truncated) allLines.push(theme.fg("dim", "…"));
|
|
657
|
+
} else if (view.kind === "markdown") {
|
|
658
|
+
allLines.push(...new Markdown(view.text, 0, 0, getMarkdownTheme()).render(innerWidth));
|
|
659
|
+
}
|
|
660
|
+
}
|
|
613
661
|
const formatted = formatOutputPaneLines(
|
|
614
662
|
{
|
|
615
663
|
lines: allLines,
|
|
616
664
|
expanded,
|
|
617
665
|
collapsedMaxLines: previewLines,
|
|
618
666
|
edge: "tail",
|
|
667
|
+
visual: true,
|
|
668
|
+
width: innerWidth,
|
|
619
669
|
showHiddenMarker: false,
|
|
620
670
|
},
|
|
621
671
|
theme,
|
|
622
672
|
);
|
|
623
673
|
return { lines: formatted.lines, hiddenCount: formatted.hiddenCount };
|
|
624
674
|
}
|
|
625
|
-
|
|
626
|
-
const isError = cell.status === "error";
|
|
627
675
|
const formatted = formatOutputPaneLines(
|
|
628
676
|
{
|
|
629
677
|
lines: output.split("\n"),
|
|
@@ -632,7 +680,7 @@ function formatCellOutputLines(
|
|
|
632
680
|
edge: "tail",
|
|
633
681
|
visual: true,
|
|
634
682
|
width: innerWidth,
|
|
635
|
-
styleLine:
|
|
683
|
+
styleLine: styleText,
|
|
636
684
|
showHiddenMarker: false,
|
|
637
685
|
},
|
|
638
686
|
theme,
|
|
@@ -664,13 +712,12 @@ function describeStatusEvent(event: EvalStatusEvent, expanded: boolean): NativeN
|
|
|
664
712
|
if (!preview) return line;
|
|
665
713
|
const head = cappedHeadLines(sanitizeDisplayLines(preview), PREVIEW_LIMITS.OUTPUT_EXPANDED).lines;
|
|
666
714
|
const path = eventString(event.path);
|
|
667
|
-
const lang = path ? getLanguageFromPath(splitPathAndSel(path).path) : undefined;
|
|
668
715
|
return node("col", { role: "omp.tool.eval.status.event", gap: "none" }, [
|
|
669
716
|
keyed(line, "line"),
|
|
670
717
|
keyed(
|
|
671
718
|
code(head.join("\n"), {
|
|
672
719
|
role: "omp.tool.eval.status.preview",
|
|
673
|
-
|
|
720
|
+
path: path ? splitPathAndSel(path).path : undefined,
|
|
674
721
|
wrap: false,
|
|
675
722
|
preview: { lines: PREVIEW_LIMITS.COLLAPSED_LINES },
|
|
676
723
|
}),
|
|
@@ -702,96 +749,79 @@ function evalToolHead(
|
|
|
702
749
|
};
|
|
703
750
|
}
|
|
704
751
|
|
|
705
|
-
/**
|
|
706
|
-
type EvalOutputPart =
|
|
707
|
-
| { readonly kind: "text"; readonly text: string }
|
|
708
|
-
| { readonly kind: "table"; readonly head: readonly string[]; readonly rows: readonly string[][] };
|
|
709
|
-
|
|
710
|
-
/** The cells of a `│ a │ b │` row, trimmed; undefined when the line is not a table row. */
|
|
711
|
-
function tableRowCells(line: string): string[] | undefined {
|
|
712
|
-
const plain = plainText(line).trim();
|
|
713
|
-
if (plain.length < 2 || !plain.startsWith("│") || !plain.endsWith("│")) return undefined;
|
|
714
|
-
return plain
|
|
715
|
-
.slice(1, -1)
|
|
716
|
-
.split("│")
|
|
717
|
-
.map(cell => cell.trim());
|
|
718
|
-
}
|
|
752
|
+
/** One ordered piece of a cell's output: printed text, or a view shown in place of the text it matched. */
|
|
753
|
+
export type EvalOutputPart = { kind: "text"; text: string } | { kind: "view"; view: EvalView };
|
|
719
754
|
|
|
720
755
|
/**
|
|
721
|
-
* Split `
|
|
722
|
-
*
|
|
723
|
-
*
|
|
756
|
+
* Split `output` around its views, in order: each view's `text` is searched
|
|
757
|
+
* from the end of the previous match, unmatched entries are skipped, and
|
|
758
|
+
* blank text between views is dropped (newlines bordering a view trimmed).
|
|
724
759
|
*/
|
|
725
|
-
function
|
|
726
|
-
const lines = output.split("\n");
|
|
760
|
+
export function splitEvalOutput(output: string, views: readonly EvalViewEntry[] | undefined): EvalOutputPart[] {
|
|
727
761
|
const parts: EvalOutputPart[] = [];
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
.replace(/^\n+|\s+$/g, "");
|
|
734
|
-
if (chunk.length > 0) parts.push({ kind: "text", text: chunk });
|
|
762
|
+
const pushText = (chunk: string, trimStart: boolean, trimEnd: boolean): void => {
|
|
763
|
+
let text = chunk;
|
|
764
|
+
if (trimStart) text = text.replace(/^\n+/, "");
|
|
765
|
+
if (trimEnd) text = text.replace(/\n+$/, "");
|
|
766
|
+
if (text.trim().length > 0) parts.push({ kind: "text", text });
|
|
735
767
|
};
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
)
|
|
745
|
-
continue;
|
|
746
|
-
const rows: string[][] = [];
|
|
747
|
-
let end = i + 3;
|
|
748
|
-
for (; end < lines.length; end++) {
|
|
749
|
-
const cells = tableRowCells(lines[end]!);
|
|
750
|
-
if (!cells) break;
|
|
751
|
-
rows.push(cells);
|
|
752
|
-
}
|
|
753
|
-
if (
|
|
754
|
-
!plainText(lines[end] ?? "")
|
|
755
|
-
.trimStart()
|
|
756
|
-
.startsWith("└")
|
|
757
|
-
)
|
|
758
|
-
continue;
|
|
759
|
-
if (rows.some(row => row.length !== head.length)) continue;
|
|
760
|
-
flushText(i);
|
|
761
|
-
parts.push({ kind: "table", head, rows });
|
|
762
|
-
i = end;
|
|
763
|
-
textStart = end + 1;
|
|
768
|
+
let cursor = 0;
|
|
769
|
+
for (const entry of views ?? []) {
|
|
770
|
+
if (!entry.text) continue;
|
|
771
|
+
const at = output.indexOf(entry.text, cursor);
|
|
772
|
+
if (at < 0) continue;
|
|
773
|
+
pushText(output.slice(cursor, at), cursor > 0, true);
|
|
774
|
+
parts.push({ kind: "view", view: entry.view });
|
|
775
|
+
cursor = at + entry.text.length;
|
|
764
776
|
}
|
|
765
|
-
|
|
777
|
+
pushText(output.slice(cursor), cursor > 0, cursor > 0);
|
|
766
778
|
return parts;
|
|
767
779
|
}
|
|
768
780
|
|
|
781
|
+
/** A table view as a native `table`; numeric columns align to the end. */
|
|
782
|
+
function evalTableNode(view: Extract<EvalView, { kind: "table" }>, key: string): NativeNode {
|
|
783
|
+
return node(
|
|
784
|
+
"table",
|
|
785
|
+
{
|
|
786
|
+
cols: view.columns.map((column, c) => ({
|
|
787
|
+
id: `c${c}`,
|
|
788
|
+
head: column.head,
|
|
789
|
+
align: column.numeric ? "end" : undefined,
|
|
790
|
+
})),
|
|
791
|
+
rows: view.rows.map((cells, r) => ({
|
|
792
|
+
id: `r${r}`,
|
|
793
|
+
cells: Object.fromEntries(cells.map((cell, c) => [`c${c}`, cell])),
|
|
794
|
+
})),
|
|
795
|
+
role: "omp.tool.eval.table",
|
|
796
|
+
},
|
|
797
|
+
undefined,
|
|
798
|
+
key,
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
|
|
769
802
|
/**
|
|
770
|
-
* One cell's outputs
|
|
771
|
-
*
|
|
772
|
-
* blocks as tables.
|
|
803
|
+
* One cell's outputs in order: printed text (following its tail, clamped to
|
|
804
|
+
* the last `previewLines` while folded) with each view in place of its text.
|
|
773
805
|
*/
|
|
774
|
-
function evalOutputNodes(
|
|
806
|
+
function evalOutputNodes(
|
|
807
|
+
output: string,
|
|
808
|
+
views: readonly EvalViewEntry[] | undefined,
|
|
809
|
+
previewLines: number,
|
|
810
|
+
): NativeNode[] {
|
|
775
811
|
if (output.trim().length === 0) return [];
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
role: "omp.tool.eval.table",
|
|
790
|
-
},
|
|
791
|
-
undefined,
|
|
792
|
-
`table-${i}`,
|
|
793
|
-
),
|
|
794
|
-
);
|
|
812
|
+
return splitEvalOutput(output, views).map((part, i) => {
|
|
813
|
+
if (part.kind === "text") {
|
|
814
|
+
return runOutput(part.text, {
|
|
815
|
+
role: "omp.tool.eval.output",
|
|
816
|
+
previewLines,
|
|
817
|
+
key: i === 0 ? "output" : `output-${i}`,
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
const view = part.view;
|
|
821
|
+
if (view.kind === "table") return evalTableNode(view, `table-${i}`);
|
|
822
|
+
if (view.kind === "json") return keyed(describeJsonTree(view.value, { hiddenRootKeys: [] }), `json-${i}`);
|
|
823
|
+
return keyed(md(view.text), `markdown-${i}`);
|
|
824
|
+
});
|
|
795
825
|
}
|
|
796
826
|
|
|
797
827
|
/** Status lines a folded cell keeps: the newest ones. */
|
|
@@ -958,18 +988,6 @@ export const evalToolRenderer = {
|
|
|
958
988
|
// the styled `warningLine` below carries the same text in ⟨…⟩ form.
|
|
959
989
|
const output = stripOutputNotice(rawOutput, details?.meta).trimEnd();
|
|
960
990
|
|
|
961
|
-
const jsonOutputs = details?.jsonOutputs ?? [];
|
|
962
|
-
const treeExpanded = options.renderContext?.expanded ?? options.expanded;
|
|
963
|
-
const treeDepth = treeExpanded ? JSON_TREE_MAX_DEPTH_EXPANDED : JSON_TREE_MAX_DEPTH_COLLAPSED;
|
|
964
|
-
const treeLineCap = treeExpanded ? JSON_TREE_MAX_LINES_EXPANDED : JSON_TREE_MAX_LINES_COLLAPSED;
|
|
965
|
-
const treeScalarLen = treeExpanded ? JSON_TREE_SCALAR_LEN_EXPANDED : JSON_TREE_SCALAR_LEN_COLLAPSED;
|
|
966
|
-
const labelOutputs = jsonOutputs.length > 1;
|
|
967
|
-
const jsonLines = jsonOutputs.flatMap((value, index) => {
|
|
968
|
-
const tree = renderJsonTreeLines(value, uiTheme, treeDepth, treeLineCap, treeScalarLen);
|
|
969
|
-
const body = tree.truncated ? [...tree.lines, uiTheme.fg("dim", "…")] : tree.lines;
|
|
970
|
-
return labelOutputs ? [uiTheme.fg("dim", `display[${index + 1}]`), ...body] : body;
|
|
971
|
-
});
|
|
972
|
-
|
|
973
991
|
const timeoutSeconds = options.renderContext?.timeout;
|
|
974
992
|
const timeoutLine =
|
|
975
993
|
typeof timeoutSeconds === "number"
|
|
@@ -1093,7 +1111,6 @@ export const evalToolRenderer = {
|
|
|
1093
1111
|
lines.push("");
|
|
1094
1112
|
}
|
|
1095
1113
|
}
|
|
1096
|
-
lines.push(...jsonLines);
|
|
1097
1114
|
if (timeoutLine) {
|
|
1098
1115
|
lines.push(timeoutLine);
|
|
1099
1116
|
}
|
|
@@ -1116,9 +1133,6 @@ export const evalToolRenderer = {
|
|
|
1116
1133
|
});
|
|
1117
1134
|
}
|
|
1118
1135
|
|
|
1119
|
-
const displayOutput = output;
|
|
1120
|
-
const combinedOutput = [displayOutput, ...jsonLines].filter(Boolean).join("\n");
|
|
1121
|
-
|
|
1122
1136
|
const statusEvents = details?.statusEvents ?? [];
|
|
1123
1137
|
const statusLines = renderStatusEvents(
|
|
1124
1138
|
statusEvents,
|
|
@@ -1127,12 +1141,12 @@ export const evalToolRenderer = {
|
|
|
1127
1141
|
details?.statusEventsElided,
|
|
1128
1142
|
);
|
|
1129
1143
|
|
|
1130
|
-
if (!
|
|
1144
|
+
if (!output && statusLines.length === 0) {
|
|
1131
1145
|
const lines = [timeoutLine, noticeLine, asyncLine, warningLine].filter(Boolean) as string[];
|
|
1132
1146
|
return new Text(lines.join("\n"), 0, 0);
|
|
1133
1147
|
}
|
|
1134
1148
|
|
|
1135
|
-
if (!
|
|
1149
|
+
if (!output && statusLines.length > 0) {
|
|
1136
1150
|
const lines = [
|
|
1137
1151
|
uiTheme.fg("dim", "Status"),
|
|
1138
1152
|
...statusLines,
|
|
@@ -1145,7 +1159,7 @@ export const evalToolRenderer = {
|
|
|
1145
1159
|
}
|
|
1146
1160
|
|
|
1147
1161
|
if (options.renderContext?.expanded ?? options.expanded) {
|
|
1148
|
-
const styledOutput =
|
|
1162
|
+
const styledOutput = output
|
|
1149
1163
|
.split("\n")
|
|
1150
1164
|
.map(line => uiTheme.fg("toolOutput", line))
|
|
1151
1165
|
.join("\n");
|
|
@@ -1160,7 +1174,7 @@ export const evalToolRenderer = {
|
|
|
1160
1174
|
return new Text(lines.join("\n"), 0, 0);
|
|
1161
1175
|
}
|
|
1162
1176
|
|
|
1163
|
-
const styledOutput =
|
|
1177
|
+
const styledOutput = output
|
|
1164
1178
|
.split("\n")
|
|
1165
1179
|
.map(line => uiTheme.fg("toolOutput", line))
|
|
1166
1180
|
.join("\n");
|
|
@@ -1250,9 +1264,6 @@ export const evalToolRenderer = {
|
|
|
1250
1264
|
const cancelled = options.cancelled === true;
|
|
1251
1265
|
const expanded = options.renderContext?.expanded ?? options.expanded;
|
|
1252
1266
|
const previewLines = options.renderContext?.previewLines ?? EVAL_DEFAULT_PREVIEW_LINES;
|
|
1253
|
-
const jsonNodes = (details?.jsonOutputs ?? []).map((value, index) =>
|
|
1254
|
-
keyed(describeJsonTree(value, { hiddenRootKeys: [] }), `display-${index}`),
|
|
1255
|
-
);
|
|
1256
1267
|
// The call's notice and truncation join the last run's foot facts (a background job is a head badge).
|
|
1257
1268
|
const callFacts = { facts: compact([details?.notice]), meta: details?.meta };
|
|
1258
1269
|
const cellResults = details?.cells;
|
|
@@ -1278,15 +1289,7 @@ export const evalToolRenderer = {
|
|
|
1278
1289
|
language,
|
|
1279
1290
|
code: formatEvalCodeForDisplay(cell.code, language),
|
|
1280
1291
|
title: cell.title,
|
|
1281
|
-
outputs: [
|
|
1282
|
-
...evalOutputNodes(
|
|
1283
|
-
cell.textOutput ?? cell.output,
|
|
1284
|
-
cell.hasMarkdown === true && cell.status !== "error",
|
|
1285
|
-
previewLines,
|
|
1286
|
-
),
|
|
1287
|
-
|
|
1288
|
-
...(i === cellResults.length - 1 ? jsonNodes : []),
|
|
1289
|
-
],
|
|
1292
|
+
outputs: [...evalOutputNodes(cell.output, cell.views, previewLines)],
|
|
1290
1293
|
status: evalStatusSection(cell, expanded),
|
|
1291
1294
|
foot: state && {
|
|
1292
1295
|
state,
|
|
@@ -1311,10 +1314,7 @@ export const evalToolRenderer = {
|
|
|
1311
1314
|
const rawOutput = options.renderContext?.output ?? resultText(result).trimEnd();
|
|
1312
1315
|
const state: RunState = cancelled ? "cancelled" : isPartial ? "running" : result.isError ? "failed" : "done";
|
|
1313
1316
|
const last: Omit<EvalCellRun, "language" | "code" | "title"> = {
|
|
1314
|
-
outputs:
|
|
1315
|
-
...evalOutputNodes(stripOutputNotice(rawOutput, details?.meta).trimEnd(), false, previewLines),
|
|
1316
|
-
...jsonNodes,
|
|
1317
|
-
],
|
|
1317
|
+
outputs: evalOutputNodes(stripOutputNotice(rawOutput, details?.meta).trimEnd(), undefined, previewLines),
|
|
1318
1318
|
status: evalStatusSection(details ?? {}, expanded),
|
|
1319
1319
|
foot: { state, elapsedMs: options.elapsedMs, ...callFacts },
|
|
1320
1320
|
};
|
package/src/tools/find.ts
CHANGED
|
@@ -37,7 +37,6 @@ import type {
|
|
|
37
37
|
import type { TspTone } from "@oh-my-pi/pi-wire";
|
|
38
38
|
import { code, col, compact, keyed, node, span, text } from "../native/describe";
|
|
39
39
|
import type { NativeChild, NativeNode } from "../native/node";
|
|
40
|
-
import { getLanguageFromPath } from "../lang-from-path";
|
|
41
40
|
import { fileHref, fileRow, inlineErrorView } from "./native-view";
|
|
42
41
|
import { splitUrlScheme } from "./url-scheme-host";
|
|
43
42
|
|
|
@@ -214,10 +213,14 @@ function describeHit(hit: FindHit, rangeLimit: number, cwd: string | undefined,
|
|
|
214
213
|
href: isUrlHit || cwd === undefined ? undefined : fileHref(path.join(cwd, rawRel)),
|
|
215
214
|
key: "hit",
|
|
216
215
|
});
|
|
217
|
-
const lang = getLanguageFromPath(hit.rel);
|
|
218
216
|
const snippets = ranges.map(range =>
|
|
219
217
|
keyed(
|
|
220
|
-
code(range.snippet.trimEnd(), {
|
|
218
|
+
code(range.snippet.trimEnd(), {
|
|
219
|
+
path: hit.rel,
|
|
220
|
+
start: range.start,
|
|
221
|
+
numbers: true,
|
|
222
|
+
title: range.p.toFixed(2),
|
|
223
|
+
}),
|
|
221
224
|
`${range.start}-${range.end}`,
|
|
222
225
|
),
|
|
223
226
|
);
|
package/src/tools/grep.ts
CHANGED
|
@@ -9,7 +9,6 @@ import type {
|
|
|
9
9
|
RenderResultOptions,
|
|
10
10
|
ToolRenderer,
|
|
11
11
|
} from "./renderer";
|
|
12
|
-
import { getLanguageFromPath } from "../lang-from-path";
|
|
13
12
|
import { compact } from "../native/describe";
|
|
14
13
|
import type { NativeChild } from "../native/node";
|
|
15
14
|
import { footnoteText, inlineErrorView, resultText } from "./native-view";
|
|
@@ -557,7 +556,6 @@ export const grepToolRenderer = {
|
|
|
557
556
|
inline: true,
|
|
558
557
|
body: compact<NativeChild>([
|
|
559
558
|
...describeGroupedOutput(textContent.split("\n"), {
|
|
560
|
-
lang: getLanguageFromPath,
|
|
561
559
|
maxFiles: options.expanded ? undefined : NATIVE_COLLAPSED_FILES,
|
|
562
560
|
file: details?.files?.length === 1 ? details.files[0] : undefined,
|
|
563
561
|
}),
|
|
@@ -314,12 +314,13 @@ export interface NumberedLine {
|
|
|
314
314
|
* Numbered `code` for possibly non-contiguous source lines: one block per run
|
|
315
315
|
* of consecutive line numbers (`start` = the run's first line, match lines
|
|
316
316
|
* `mark`ed), with a `…` row wherever the numbers jump or an elision marker
|
|
317
|
-
* (`n: null`) sits.
|
|
317
|
+
* (`n: null`) sits. `path` is the file the lines come from: Tern picks their
|
|
318
|
+
* grammar from it and draws no header (the row or head above names the file).
|
|
318
319
|
* `wrap` soft-wraps long lines instead of scrolling the block sideways.
|
|
319
320
|
*/
|
|
320
321
|
export function numberedCode(
|
|
321
322
|
lines: readonly NumberedLine[],
|
|
322
|
-
opts: {
|
|
323
|
+
opts: { path?: string; wrap?: boolean } = {},
|
|
323
324
|
): NativeNode[] {
|
|
324
325
|
const out: NativeNode[] = [];
|
|
325
326
|
let run: { start: number; text: string[]; marks: { line: number; tone: TspTone }[] } | undefined;
|
|
@@ -332,7 +333,7 @@ export function numberedCode(
|
|
|
332
333
|
code(run.text.join("\n"), {
|
|
333
334
|
start: run.start,
|
|
334
335
|
numbers: true,
|
|
335
|
-
|
|
336
|
+
path: opts.path,
|
|
336
337
|
wrap: opts.wrap,
|
|
337
338
|
marks: run.marks.length > 0 ? run.marks : undefined,
|
|
338
339
|
}),
|
|
@@ -367,7 +368,7 @@ export function numberedCode(
|
|
|
367
368
|
*/
|
|
368
369
|
export function describeGroupedOutput(
|
|
369
370
|
lines: readonly string[],
|
|
370
|
-
options: {
|
|
371
|
+
options: { maxFiles?: number; file?: string } = {},
|
|
371
372
|
): NativeNode[] {
|
|
372
373
|
const files: NativeNode[] = [];
|
|
373
374
|
const loose: NativeNode[] = [];
|
|
@@ -380,7 +381,7 @@ export function describeGroupedOutput(
|
|
|
380
381
|
const flushLines = () => {
|
|
381
382
|
const target = current?.children ?? loose;
|
|
382
383
|
if (numbered.length > 0) {
|
|
383
|
-
target.push(...numberedCode(numbered, {
|
|
384
|
+
target.push(...numberedCode(numbered, { path: current?.path }));
|
|
384
385
|
numbered = [];
|
|
385
386
|
}
|
|
386
387
|
if (plain.length > 0) {
|
package/src/tools/read.ts
CHANGED
|
@@ -431,12 +431,12 @@ export function readContentCode(
|
|
|
431
431
|
content: string,
|
|
432
432
|
start: number,
|
|
433
433
|
numbers: readonly (number | null)[] | undefined,
|
|
434
|
-
|
|
434
|
+
path: string | undefined,
|
|
435
435
|
): NativeNode[] {
|
|
436
436
|
const lines = content.split("\n");
|
|
437
437
|
return numberedCode(
|
|
438
438
|
lines.map((text, i) => ({ n: numbers && numbers.length === lines.length ? numbers[i]! : start + i, text })),
|
|
439
|
-
{
|
|
439
|
+
{ path, wrap: true },
|
|
440
440
|
);
|
|
441
441
|
}
|
|
442
442
|
|
|
@@ -766,7 +766,7 @@ export const readToolRenderer = {
|
|
|
766
766
|
contentText,
|
|
767
767
|
details?.displayContent?.startLine ?? readRangeStart(rawPath, args),
|
|
768
768
|
details?.displayContent?.lineNumbers,
|
|
769
|
-
|
|
769
|
+
renderPath.path,
|
|
770
770
|
);
|
|
771
771
|
return { tool: head, inline: true, body: compact<NativeChild>([media, ...content, foot]) };
|
|
772
772
|
},
|
package/src/tools/write.ts
CHANGED
|
@@ -164,10 +164,12 @@ function describeFigure(figure: ToolFigure | undefined): NativeNode | undefined
|
|
|
164
164
|
return { ...md(text, { role: "omp.tool.write.figure", stream: !figure.closed }), key: "figure" };
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* The written content as numbered code, highlighted by Tern from the file's path,
|
|
169
|
+
* keyed so it keeps its node as the figure above it comes and goes.
|
|
170
|
+
*/
|
|
168
171
|
function describeSource(rawPath: string, content: string): NativeNode {
|
|
169
|
-
|
|
170
|
-
return { ...code(content, { lang, numbers: true, wrap: true }), key: "source" };
|
|
172
|
+
return { ...code(content, { path: rawPath || undefined, numbers: true, wrap: true }), key: "source" };
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
function countLines(text: string): number {
|
package/src/tui.ts
CHANGED
|
@@ -107,6 +107,11 @@ const PAINT_END_NO_SYNC = ENABLE_AUTOWRAP;
|
|
|
107
107
|
// native text selection.
|
|
108
108
|
const MOUSE_TRACKING_ON = "\x1b[?1000h\x1b[?1003h\x1b[?1006h";
|
|
109
109
|
const MOUSE_TRACKING_OFF = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
|
|
110
|
+
// Cell-size replies: CSI 6 ; height ; width t in pixels, and iTerm2's
|
|
111
|
+
// OSC 1337 ; ReportCellSize = height ; width [; scale] ST in points.
|
|
112
|
+
const CELL_SIZE_REPLY = /^\x1b\[6;(\d+);(\d+)t$/;
|
|
113
|
+
const ITERM2_CELL_SIZE_REPLY =
|
|
114
|
+
/^\x1b\]1337;ReportCellSize=(\d+(?:\.\d+)?);(\d+(?:\.\d+)?)(?:;(\d+(?:\.\d+)?))?(?:\x07|\x1b\\)$/;
|
|
110
115
|
|
|
111
116
|
type MouseTrackingState = "off" | "inline" | "full";
|
|
112
117
|
|
|
@@ -2562,6 +2567,9 @@ export class TUI extends Container {
|
|
|
2562
2567
|
// Query terminal for cell size in pixels: CSI 16 t
|
|
2563
2568
|
// Response format: CSI 6 ; height ; width t
|
|
2564
2569
|
this.terminal.write("\x1b[16t");
|
|
2570
|
+
// iTerm2 never answers CSI 16 t. Without a size, rows reserved for an image
|
|
2571
|
+
// and crops of a partly scrolled one assume 9x18 cells and land offset.
|
|
2572
|
+
if (TERMINAL.imageProtocol === ImageProtocol.Iterm2) this.terminal.write("\x1b]1337;ReportCellSize\x1b\\");
|
|
2565
2573
|
}
|
|
2566
2574
|
|
|
2567
2575
|
/**
|
|
@@ -3051,14 +3059,19 @@ export class TUI extends Container {
|
|
|
3051
3059
|
}
|
|
3052
3060
|
|
|
3053
3061
|
#consumeCellSizeResponse(data: string): boolean {
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3062
|
+
let heightPx: number;
|
|
3063
|
+
let widthPx: number;
|
|
3064
|
+
const pixels = CELL_SIZE_REPLY.exec(data);
|
|
3065
|
+
if (pixels) {
|
|
3066
|
+
heightPx = parseInt(pixels[1], 10);
|
|
3067
|
+
widthPx = parseInt(pixels[2], 10);
|
|
3068
|
+
} else {
|
|
3069
|
+
const points = ITERM2_CELL_SIZE_REPLY.exec(data);
|
|
3070
|
+
if (!points) return false;
|
|
3071
|
+
const scale = points[3] ? Number(points[3]) : 1;
|
|
3072
|
+
heightPx = Math.round(Number(points[1]) * scale);
|
|
3073
|
+
widthPx = Math.round(Number(points[2]) * scale);
|
|
3058
3074
|
}
|
|
3059
|
-
|
|
3060
|
-
const heightPx = parseInt(match[1], 10);
|
|
3061
|
-
const widthPx = parseInt(match[2], 10);
|
|
3062
3075
|
if (heightPx <= 0 || widthPx <= 0) {
|
|
3063
3076
|
return true;
|
|
3064
3077
|
}
|