@oh-my-pi/pi-tui 18.12.0 → 18.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -11
- 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/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/package.json +9 -9
- 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/terminal-capabilities.ts +7 -1
- 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/write.ts +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.12.1] - 2026-10-17
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- `numberedCode`, `readContentCode` and `nativeDiff` take the file's path (`path`) instead of a language, and `describeGroupedOutput` drops its `lang` option: Tern picks the grammar of file contents from their path
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Advisor notes in Tern read as a callout tinted by their worst severity: its icon and a tally (`Concern`, `1 blocker · 2 concerns`) head the card with the advisor's name at the end, and several notes each lead with their own severity icon
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Fixed images rendering blank inside tmux on Kitty-capable terminals when tmux's `allow-passthrough` is off (the default); omp now falls back to SIXEL or the text placeholder instead of sending Kitty graphics tmux drops
|
|
18
|
+
- Fixed Tern drawing tool cards' file contents (write, read, edit, grep, find, ast-grep, ast-edit) plain or in the wrong language when omp did not know the file's extension (`.ptx`) or mapped it to a neighbor (`.cu` as C++); omp now sends the file's path and Tern picks the grammar
|
|
19
|
+
|
|
5
20
|
## [18.12.0] - 2026-10-17
|
|
6
21
|
|
|
7
22
|
### Breaking Changes
|
|
@@ -2512,14 +2527,4 @@
|
|
|
2512
2527
|
|
|
2513
2528
|
- Changed truncation debug logging to run only when `debugRedraw` is enabled
|
|
2514
2529
|
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
### Changed
|
|
2518
|
-
|
|
2519
|
-
- Updated hash computation to use `Bun.hash()` instead of `Bun.hash.xxHash64()`, which may return `number` in addition to `bigint`
|
|
2520
|
-
- Simplified cache key computation in Box component by removing intermediate hash updates and consolidating hash operations
|
|
2521
|
-
- Wrapped native text utility functions (`sliceWithWidth`, `truncateToWidth`, `wrapTextWithAnsi`, `extractSegments`) to automatically pass the current default tab width, simplifying the API for consumers
|
|
2522
|
-
- Added `getIndentationNoescape` wrapper that uses `process.cwd()` as the project root for relative file paths
|
|
2523
|
-
- Re-export `getDefaultTabWidth`, `getIndentation`, and `setDefaultTabWidth` from `@oh-my-pi/pi-utils`; native text helpers still receive tab width via wrappers that read the JS default
|
|
2524
|
-
|
|
2525
|
-
Older entries are archived in [packages/tui/CHANGELOG.md@691b3d448f3d](https://github.com/can1357/oh-my-pi/blob/691b3d448f3da1f99010854dea4a562395fd190d/packages/tui/CHANGELOG.md).
|
|
2530
|
+
Older entries are archived in [packages/tui/CHANGELOG.md@200e7d2c6ebb](https://github.com/can1357/oh-my-pi/blob/200e7d2c6ebb2e187b167f807b9f68f779c0bf4b/packages/tui/CHANGELOG.md).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Component } from "../tui.js";
|
|
2
2
|
import type { AdvisorMessageDetails } from "./messages.js";
|
|
3
|
-
import {
|
|
3
|
+
import type { Theme } from "../theme/index.js";
|
|
4
4
|
import { type NativeUiEvent } from "../native/node.js";
|
|
5
5
|
/** The advisor card: shared by the controlled expansion getter and native toggles. */
|
|
6
6
|
export interface AdvisorMessageCard extends Component {
|
|
@@ -14,6 +14,9 @@ export interface AdvisorMessageCard extends Component {
|
|
|
14
14
|
* output (whose `thinkingText` color equals `toolOutput` in most themes):
|
|
15
15
|
* a bold `customMessageLabel` header tag (skill-card convention), a heavy
|
|
16
16
|
* rail tinted per-note severity, and the note body on the default text color.
|
|
17
|
+
* Natively it is a callout tinted by its worst note: that severity's icon and
|
|
18
|
+
* the tally head it, the advisor's name ends the head, and with several notes
|
|
19
|
+
* each leads with its own severity icon.
|
|
17
20
|
*
|
|
18
21
|
* `getExpanded` owns expansion; a native toggle overrides it for this card
|
|
19
22
|
* until the next transcript-wide change.
|
|
@@ -173,7 +173,9 @@ export declare function isUserInvokedSkillPrompt(message: CustomMessage): boolea
|
|
|
173
173
|
* reminders, and auto-continues are not turn starts.
|
|
174
174
|
*/
|
|
175
175
|
export declare function isUserTurnInitiator(message: CustomMessage): boolean;
|
|
176
|
-
|
|
176
|
+
/** Advisor note severities, weakest first; ranks severity thresholds such as `advisor.continueSeverity`. */
|
|
177
|
+
export declare const ADVISOR_SEVERITIES: readonly ["nit", "concern", "blocker"];
|
|
178
|
+
export type AdvisorSeverity = (typeof ADVISOR_SEVERITIES)[number];
|
|
177
179
|
/** One queued advice note. */
|
|
178
180
|
export interface AdvisorNote {
|
|
179
181
|
note: string;
|
|
@@ -7,10 +7,9 @@ import type { NativeNode } from "../native/node.js";
|
|
|
7
7
|
* becomes one hunk; the terminal draws gutters, word emphasis and highlighting.
|
|
8
8
|
* Removed rows carry old line numbers, added rows new ones and context rows
|
|
9
9
|
* old ones, so the other side is derived from the running line delta.
|
|
10
|
+
* Tern picks the grammar from `path`.
|
|
10
11
|
*/
|
|
11
|
-
export declare function nativeDiff(diffText: string,
|
|
12
|
-
filePath?: string;
|
|
13
|
-
} & TspProps<"diff">): NativeNode;
|
|
12
|
+
export declare function nativeDiff(diffText: string, props?: TspProps<"diff">): NativeNode;
|
|
14
13
|
export interface RenderDiffOptions {
|
|
15
14
|
/** File path used to resolve indentation (.editorconfig + defaults) */
|
|
16
15
|
filePath?: string;
|
package/dist/types/tmux.d.ts
CHANGED
|
@@ -45,3 +45,12 @@ export declare function resolveTmuxClientTerminalNames(env?: NodeJS.ProcessEnv):
|
|
|
45
45
|
* for features.
|
|
46
46
|
*/
|
|
47
47
|
export declare function tmuxClientRendersSixel(env?: NodeJS.ProcessEnv): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Whether tmux forwards this pane's `\x1bPtmux;…\x1b\\` passthrough envelopes
|
|
50
|
+
* to the attached client. tmux 3.3+ defaults `allow-passthrough` to `off` and
|
|
51
|
+
* then silently drops every wrapped sequence, so Kitty graphics sent through
|
|
52
|
+
* the envelope never reach a Kitty-capable outer terminal. Resolves the
|
|
53
|
+
* effective pane value (pane → window → global). Queried once per pane;
|
|
54
|
+
* `false` outside tmux or when the query fails.
|
|
55
|
+
*/
|
|
56
|
+
export declare function tmuxPaneAllowsPassthrough(env?: NodeJS.ProcessEnv): boolean;
|
|
@@ -13,30 +13,60 @@ export interface EvalStatusEvent {
|
|
|
13
13
|
resolvedThinkingLevel?: ConfiguredThinkingLevel;
|
|
14
14
|
[key: string]: unknown;
|
|
15
15
|
}
|
|
16
|
+
/** One column of a table view. */
|
|
17
|
+
export interface EvalTableColumn {
|
|
18
|
+
head: string;
|
|
19
|
+
/** Numeric values: renderers right-align the column. */
|
|
20
|
+
numeric?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A pretty rendering of something a cell printed, shown to the user in place
|
|
24
|
+
* of its text; the model only ever sees the text.
|
|
25
|
+
* - `table`: a data frame or `console.table`; leading index columns are plain
|
|
26
|
+
* columns, and a truncated frame's elided rows/columns are `…` cells.
|
|
27
|
+
* - `json`: a structured value (dict/list, object/array) as a collapsible tree.
|
|
28
|
+
* - `markdown`: an object's Markdown representation.
|
|
29
|
+
*/
|
|
30
|
+
export type EvalView = {
|
|
31
|
+
kind: "table";
|
|
32
|
+
columns: EvalTableColumn[];
|
|
33
|
+
rows: string[][];
|
|
34
|
+
} | {
|
|
35
|
+
kind: "json";
|
|
36
|
+
value: unknown;
|
|
37
|
+
} | {
|
|
38
|
+
kind: "markdown";
|
|
39
|
+
text: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* A view and the exact printed text it stands for. Renderers locate `text` in
|
|
43
|
+
* the cell's `output` (entries in print order) and show the view there; an
|
|
44
|
+
* entry whose text is no longer in the output (truncated) is skipped.
|
|
45
|
+
*/
|
|
46
|
+
export interface EvalViewEntry {
|
|
47
|
+
text: string;
|
|
48
|
+
view: EvalView;
|
|
49
|
+
}
|
|
16
50
|
/** Per-cell execution result for transcript rendering. */
|
|
17
51
|
export interface EvalCellResult {
|
|
18
52
|
index: number;
|
|
19
53
|
title?: string;
|
|
20
54
|
code: string;
|
|
21
55
|
language?: EvalLanguage;
|
|
56
|
+
/** What the model saw: the cell's printed text plus image notes. */
|
|
22
57
|
output: string;
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
* from `jsonOutputs` instead. Absent when the cell displayed no JSON.
|
|
26
|
-
*/
|
|
27
|
-
textOutput?: string;
|
|
58
|
+
/** Pretty views of spans of `output`, in print order. */
|
|
59
|
+
views?: EvalViewEntry[];
|
|
28
60
|
status: "pending" | "running" | "complete" | "error";
|
|
29
61
|
durationMs?: number;
|
|
30
62
|
exitCode?: number;
|
|
31
63
|
statusEvents?: EvalStatusEvent[];
|
|
32
64
|
/** Discrete events dropped from the front of `statusEvents` (see {@link recordStatusEvent}). */
|
|
33
65
|
statusEventsElided?: number;
|
|
34
|
-
hasMarkdown?: boolean;
|
|
35
66
|
}
|
|
36
67
|
/** Tool result detail object surfaced to the UI/transcript. */
|
|
37
68
|
export interface EvalToolDetails {
|
|
38
69
|
cells?: EvalCellResult[];
|
|
39
|
-
jsonOutputs?: unknown[];
|
|
40
70
|
images?: ImageContent[];
|
|
41
71
|
statusEvents?: EvalStatusEvent[];
|
|
42
72
|
/** Discrete events dropped from the front of `statusEvents` (see {@link recordStatusEvent}). */
|
|
@@ -105,6 +135,20 @@ export declare function upsertStatusEvent(events: EvalStatusEvent[], event: Eval
|
|
|
105
135
|
* events behind an "… N earlier" row.
|
|
106
136
|
*/
|
|
107
137
|
export declare function recordStatusEvent(log: StatusEventLog, event: EvalStatusEvent): void;
|
|
138
|
+
/** One ordered piece of a cell's output: printed text, or a view shown in place of the text it matched. */
|
|
139
|
+
export type EvalOutputPart = {
|
|
140
|
+
kind: "text";
|
|
141
|
+
text: string;
|
|
142
|
+
} | {
|
|
143
|
+
kind: "view";
|
|
144
|
+
view: EvalView;
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Split `output` around its views, in order: each view's `text` is searched
|
|
148
|
+
* from the end of the previous match, unmatched entries are skipped, and
|
|
149
|
+
* blank text between views is dropped (newlines bordering a view trimmed).
|
|
150
|
+
*/
|
|
151
|
+
export declare function splitEvalOutput(output: string, views: readonly EvalViewEntry[] | undefined): EvalOutputPart[];
|
|
108
152
|
/** Render eval code cells, structured display output, and progress events. */
|
|
109
153
|
export declare const evalToolRenderer: {
|
|
110
154
|
animatedPendingPreview: true;
|
|
@@ -110,11 +110,12 @@ export interface NumberedLine {
|
|
|
110
110
|
* Numbered `code` for possibly non-contiguous source lines: one block per run
|
|
111
111
|
* of consecutive line numbers (`start` = the run's first line, match lines
|
|
112
112
|
* `mark`ed), with a `…` row wherever the numbers jump or an elision marker
|
|
113
|
-
* (`n: null`) sits.
|
|
113
|
+
* (`n: null`) sits. `path` is the file the lines come from: Tern picks their
|
|
114
|
+
* grammar from it and draws no header (the row or head above names the file).
|
|
114
115
|
* `wrap` soft-wraps long lines instead of scrolling the block sideways.
|
|
115
116
|
*/
|
|
116
117
|
export declare function numberedCode(lines: readonly NumberedLine[], opts?: {
|
|
117
|
-
|
|
118
|
+
path?: string;
|
|
118
119
|
wrap?: boolean;
|
|
119
120
|
}): NativeNode[];
|
|
120
121
|
/**
|
|
@@ -128,7 +129,6 @@ export declare function numberedCode(lines: readonly NumberedLine[], opts?: {
|
|
|
128
129
|
* same file row as a directory search's.
|
|
129
130
|
*/
|
|
130
131
|
export declare function describeGroupedOutput(lines: readonly string[], options?: {
|
|
131
|
-
lang?: (path: string) => string | undefined;
|
|
132
132
|
maxFiles?: number;
|
|
133
133
|
file?: string;
|
|
134
134
|
}): NativeNode[];
|
|
@@ -136,7 +136,7 @@ export declare function readResultIsMarkdown(details: ReadToolDetails | undefine
|
|
|
136
136
|
* block; runs split with `…` between) when they cover every line, else count
|
|
137
137
|
* up from `start`.
|
|
138
138
|
*/
|
|
139
|
-
export declare function readContentCode(content: string, start: number, numbers: readonly (number | null)[] | undefined,
|
|
139
|
+
export declare function readContentCode(content: string, start: number, numbers: readonly (number | null)[] | undefined, path: string | undefined): NativeNode[];
|
|
140
140
|
/** Render file, image, and URL reads in the transcript. */
|
|
141
141
|
export declare const readToolRenderer: {
|
|
142
142
|
activitySummary(args: unknown, _context: ToolActivityContext): ToolActivitySummary;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "18.12.
|
|
4
|
+
"version": "18.12.1",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"gen:glyphs": "bun scripts/gen-glyphs.ts"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@oh-my-pi/omptype": "18.12.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "18.12.
|
|
46
|
-
"@oh-my-pi/pi-ai": "18.12.
|
|
47
|
-
"@oh-my-pi/pi-catalog": "18.12.
|
|
48
|
-
"@oh-my-pi/pi-natives": "18.12.
|
|
49
|
-
"@oh-my-pi/pi-utils": "18.12.
|
|
50
|
-
"@oh-my-pi/pi-wire": "18.12.
|
|
51
|
-
"@oh-my-pi/snapcompact": "18.12.
|
|
44
|
+
"@oh-my-pi/omptype": "18.12.1",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.12.1",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.12.1",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.12.1",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.12.1",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.12.1",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.12.1",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.12.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"kitty-vt-wasm": "^0.2.0"
|
|
@@ -4,9 +4,9 @@ import { visibleWidth } from "../utils";
|
|
|
4
4
|
import type { AdvisorMessageDetails, AdvisorNote, AdvisorSeverity } from "./messages";
|
|
5
5
|
import { formatBadge, replaceTabs, type ToolUIColor, wrapTextWithAnsi } from "../render/render-utils";
|
|
6
6
|
import { Ellipsis, truncateToWidth } from "../render";
|
|
7
|
-
import {
|
|
8
|
-
import type { TspSpan } from "@oh-my-pi/pi-wire";
|
|
9
|
-
import { card, span, text } from "../native/describe";
|
|
7
|
+
import type { Theme } from "../theme";
|
|
8
|
+
import type { TspSpan, TspTone } from "@oh-my-pi/pi-wire";
|
|
9
|
+
import { card, node, span, text } from "../native/describe";
|
|
10
10
|
import { type NativeNode, type NativeUiEvent, rootToggleExpanded } from "../native/node";
|
|
11
11
|
import { plainText } from "../native/spans";
|
|
12
12
|
import { Memo } from "../native/memo";
|
|
@@ -111,13 +111,16 @@ class AdvisorNotes implements Component {
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/** The advisor a note came from; undefined for the implicit single ("default") advisor, which goes unlabeled. */
|
|
115
|
+
function sourceOf(entry: AdvisorNote): string | undefined {
|
|
116
|
+
return entry.advisor && entry.advisor !== "default" ? entry.advisor : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
114
119
|
/** Wrapped, truncated rail rows for a single note; shared by both branches. */
|
|
115
120
|
function renderAdvisorNote(entry: AdvisorNote, width: number, uiTheme: Theme): string[] {
|
|
116
121
|
const badge = entry.severity ? `${formatBadge(entry.severity, severityColor(entry.severity), uiTheme)} ` : "";
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const who =
|
|
120
|
-
entry.advisor && entry.advisor !== "default" ? `${uiTheme.fg("dim", `[${replaceTabs(entry.advisor)}]`)} ` : "";
|
|
122
|
+
const source = sourceOf(entry);
|
|
123
|
+
const who = source ? `${uiTheme.fg("dim", `[${replaceTabs(source)}]`)} ` : "";
|
|
121
124
|
const age = entry.turnsAgo !== undefined ? `${uiTheme.fg("dim", `T-${entry.turnsAgo}`)} ` : "";
|
|
122
125
|
const railGlyph = uiTheme.symbol("advisor.rail");
|
|
123
126
|
const rail = uiTheme.fg(severityColor(entry.severity), railGlyph);
|
|
@@ -145,16 +148,47 @@ function renderAdvisorNote(entry: AdvisorNote, width: number, uiTheme: Theme): s
|
|
|
145
148
|
);
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
/**
|
|
149
|
-
|
|
151
|
+
/** A note's rank in the native callout: its severity, `note` when it has none. */
|
|
152
|
+
type AdvisorRank = AdvisorSeverity | "note";
|
|
153
|
+
|
|
154
|
+
/** Ranks worst first: the callout wears the worst rank among its notes. */
|
|
155
|
+
const RANKS: readonly AdvisorRank[] = ["blocker", "concern", "nit", "note"];
|
|
156
|
+
|
|
157
|
+
/** Each rank's native look, GitHub-alert style: card/icon tone, Tern icon name, label and its span style. */
|
|
158
|
+
const RANK_LOOK: Record<AdvisorRank, { tone: TspTone; icon: string; label: string; style: string }> = {
|
|
159
|
+
blocker: { tone: "error", icon: "octagon-alert", label: "Blocker", style: "error strong" },
|
|
160
|
+
concern: { tone: "warning", icon: "warn", label: "Concern", style: "warning strong" },
|
|
161
|
+
nit: { tone: "neutral", icon: "lightbulb", label: "Nit", style: "strong" },
|
|
162
|
+
note: { tone: "neutral", icon: "info", label: "Note", style: "strong" },
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
function rankOf(entry: AdvisorNote): AdvisorRank {
|
|
166
|
+
return entry.severity ?? "note";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The callout's title: one note's rank (`Concern`), else a tally worst first (`1 blocker · 2 concerns`). */
|
|
170
|
+
function tallySpans(notes: readonly AdvisorNote[]): TspSpan[] {
|
|
171
|
+
if (notes.length === 1) {
|
|
172
|
+
const { label, style } = RANK_LOOK[rankOf(notes[0]!)];
|
|
173
|
+
return [span(label, style)];
|
|
174
|
+
}
|
|
150
175
|
const spans: TspSpan[] = [];
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
176
|
+
for (const rank of RANKS) {
|
|
177
|
+
const count = notes.filter(entry => rankOf(entry) === rank).length;
|
|
178
|
+
if (count === 0) continue;
|
|
179
|
+
if (spans.length > 0) spans.push(span(" · ", "dim"));
|
|
180
|
+
spans.push(span(`${count} ${rank}${count === 1 ? "" : "s"}`, RANK_LOOK[rank].style));
|
|
181
|
+
}
|
|
155
182
|
return spans;
|
|
156
183
|
}
|
|
157
184
|
|
|
185
|
+
/** Native spans for one note: its advisor when the callout mixes several, then the text. */
|
|
186
|
+
function noteSpans(entry: AdvisorNote, attribute: boolean): TspSpan[] {
|
|
187
|
+
const who = attribute ? sourceOf(entry) : undefined;
|
|
188
|
+
const body = span(plainText(entry.note));
|
|
189
|
+
return who ? [span(`${who} · `, "dim"), body] : [body];
|
|
190
|
+
}
|
|
191
|
+
|
|
158
192
|
/** The advisor card: shared by the controlled expansion getter and native toggles. */
|
|
159
193
|
export interface AdvisorMessageCard extends Component {
|
|
160
194
|
/** Transcript-wide expansion (`Ctrl+O`); clears a toggle made in the terminal. */
|
|
@@ -168,6 +202,9 @@ export interface AdvisorMessageCard extends Component {
|
|
|
168
202
|
* output (whose `thinkingText` color equals `toolOutput` in most themes):
|
|
169
203
|
* a bold `customMessageLabel` header tag (skill-card convention), a heavy
|
|
170
204
|
* rail tinted per-note severity, and the note body on the default text color.
|
|
205
|
+
* Natively it is a callout tinted by its worst note: that severity's icon and
|
|
206
|
+
* the tally head it, the advisor's name ends the head, and with several notes
|
|
207
|
+
* each leads with its own severity icon.
|
|
171
208
|
*
|
|
172
209
|
* `getExpanded` owns expansion; a native toggle overrides it for this card
|
|
173
210
|
* until the next transcript-wide change.
|
|
@@ -193,24 +230,40 @@ export function createAdvisorMessageCard(
|
|
|
193
230
|
};
|
|
194
231
|
const nativeMemo = new Memo();
|
|
195
232
|
const describeCard = (isExpanded: boolean): NativeNode => {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
233
|
+
const look = RANK_LOOK[RANKS.find(rank => notes.some(entry => rankOf(entry) === rank)) ?? "note"];
|
|
234
|
+
const sources = new Set(notes.map(sourceOf));
|
|
235
|
+
const [sole] = sources;
|
|
236
|
+
const head = node(
|
|
237
|
+
"row",
|
|
238
|
+
{ gap: "sm", align: "center", role: "omp.advisor.head" },
|
|
239
|
+
[
|
|
240
|
+
node("icon", { name: look.icon, tone: look.tone }),
|
|
241
|
+
text(tallySpans(notes), { grow: 1 }),
|
|
242
|
+
text([span(sources.size === 1 && sole ? sole : "Advisor", "dim")], { role: "omp.advisor.source" }),
|
|
243
|
+
],
|
|
244
|
+
"head",
|
|
203
245
|
);
|
|
246
|
+
const attribute = sources.size > 1;
|
|
247
|
+
const body = notes.map((entry, index) => {
|
|
248
|
+
const spans = noteSpans(entry, attribute);
|
|
249
|
+
if (notes.length === 1) return text(spans, { wrap: "word", role: "omp.advisor.note", key: `n${index}` });
|
|
250
|
+
const { tone, icon, label } = RANK_LOOK[rankOf(entry)];
|
|
251
|
+
return node(
|
|
252
|
+
"row",
|
|
253
|
+
{ gap: "sm", align: "start", role: "omp.advisor.note", tone },
|
|
254
|
+
[node("icon", { name: icon, tone, aria: label }), text(spans, { wrap: "word", grow: 1, shrink: 1 })],
|
|
255
|
+
`n${index}`,
|
|
256
|
+
);
|
|
257
|
+
});
|
|
204
258
|
return card(
|
|
205
259
|
{
|
|
206
260
|
role: "omp.advisor",
|
|
207
|
-
tone:
|
|
208
|
-
head,
|
|
261
|
+
tone: look.tone,
|
|
209
262
|
collapsible: notes.length > COLLAPSED_NOTES,
|
|
210
263
|
collapsed: notes.length > COLLAPSED_NOTES ? !isExpanded : undefined,
|
|
211
264
|
preview: notes.length > COLLAPSED_NOTES ? "auto" : undefined,
|
|
212
265
|
},
|
|
213
|
-
body,
|
|
266
|
+
[head, ...body],
|
|
214
267
|
);
|
|
215
268
|
};
|
|
216
269
|
|
|
@@ -231,7 +284,7 @@ export function createAdvisorMessageCard(
|
|
|
231
284
|
},
|
|
232
285
|
describe(): NativeNode {
|
|
233
286
|
const isExpanded = expanded();
|
|
234
|
-
return nativeMemo.get([isExpanded
|
|
287
|
+
return nativeMemo.get([isExpanded], () => describeCard(isExpanded));
|
|
235
288
|
},
|
|
236
289
|
setExpanded(value: boolean): void {
|
|
237
290
|
lastGlobal = value;
|
package/src/chat/messages.ts
CHANGED
|
@@ -286,7 +286,10 @@ export function isUserTurnInitiator(message: CustomMessage): boolean {
|
|
|
286
286
|
);
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
|
|
289
|
+
/** Advisor note severities, weakest first; ranks severity thresholds such as `advisor.continueSeverity`. */
|
|
290
|
+
export const ADVISOR_SEVERITIES = ["nit", "concern", "blocker"] as const;
|
|
291
|
+
|
|
292
|
+
export type AdvisorSeverity = (typeof ADVISOR_SEVERITIES)[number];
|
|
290
293
|
|
|
291
294
|
/** One queued advice note. */
|
|
292
295
|
export interface AdvisorNote {
|
|
@@ -761,7 +761,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
761
761
|
lines.slice(0, shown).join("\n"),
|
|
762
762
|
entry.codeStartLine ?? firstSelectorLine(split.sel) ?? 1,
|
|
763
763
|
entry.codeLineNumbers?.slice(0, shown),
|
|
764
|
-
|
|
764
|
+
split.path,
|
|
765
765
|
);
|
|
766
766
|
const hidden = lines.length - shown;
|
|
767
767
|
if (hidden > 0) {
|
|
@@ -836,7 +836,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
836
836
|
previewsMarkdown(entry)
|
|
837
837
|
? md(entry.contentText ?? "")
|
|
838
838
|
: code(entry.contentText ?? "", {
|
|
839
|
-
|
|
839
|
+
path: split.path,
|
|
840
840
|
start: entry.codeStartLine,
|
|
841
841
|
numbers: true,
|
|
842
842
|
wrap: true,
|
package/src/chrome/diff.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { diffWords } from "@oh-my-pi/pi-natives";
|
|
|
2
2
|
import { DEFAULT_TAB_WIDTH, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import type { TspDiffHunk, TspProps } from "@oh-my-pi/pi-wire";
|
|
4
4
|
import { theme as activeTheme, getLanguageFromPath, highlightCode, type Theme } from "../theme/index";
|
|
5
|
-
import { type CodeFrameMarker, formatCodeFrameLine, replaceTabs
|
|
5
|
+
import { type CodeFrameMarker, formatCodeFrameLine, replaceTabs } from "../render/render-utils";
|
|
6
6
|
import { node } from "../native/describe";
|
|
7
7
|
import type { NativeNode } from "../native/node";
|
|
8
8
|
/** SGR dim on / normal intensity — additive, preserves fg/bg colors. */
|
|
@@ -106,9 +106,9 @@ function renderIntraLineDiff(
|
|
|
106
106
|
* becomes one hunk; the terminal draws gutters, word emphasis and highlighting.
|
|
107
107
|
* Removed rows carry old line numbers, added rows new ones and context rows
|
|
108
108
|
* old ones, so the other side is derived from the running line delta.
|
|
109
|
+
* Tern picks the grammar from `path`.
|
|
109
110
|
*/
|
|
110
|
-
export function nativeDiff(diffText: string,
|
|
111
|
-
const { filePath, ...props } = options;
|
|
111
|
+
export function nativeDiff(diffText: string, props: TspProps<"diff"> = {}): NativeNode {
|
|
112
112
|
const hunks: TspDiffHunk[] = [];
|
|
113
113
|
let lines: string[] = [];
|
|
114
114
|
let oldStart = 0;
|
|
@@ -148,12 +148,7 @@ export function nativeDiff(diffText: string, options: { filePath?: string } & Ts
|
|
|
148
148
|
lines.push(`${parsed.prefix}${parsed.content}`);
|
|
149
149
|
}
|
|
150
150
|
close();
|
|
151
|
-
return node("diff", {
|
|
152
|
-
...props,
|
|
153
|
-
hunks,
|
|
154
|
-
path: props.path ?? (filePath ? shortenPath(filePath) : undefined),
|
|
155
|
-
lang: props.lang ?? (filePath ? getLanguageFromPath(filePath) : undefined),
|
|
156
|
-
});
|
|
151
|
+
return node("diff", { ...props, hunks });
|
|
157
152
|
}
|
|
158
153
|
|
|
159
154
|
export interface RenderDiffOptions {
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
terminalMultiplexerForTerm,
|
|
18
18
|
terminalMultiplexerSessions,
|
|
19
19
|
} from "./terminal-multiplexer";
|
|
20
|
-
import { resolveTmuxClientTerminalNames, wrapTmuxPassthroughIfNeeded } from "./tmux";
|
|
20
|
+
import { resolveTmuxClientTerminalNames, tmuxPaneAllowsPassthrough, wrapTmuxPassthroughIfNeeded } from "./tmux";
|
|
21
21
|
import { detectTerminalBackground } from "./theme/theme";
|
|
22
22
|
import type { HangulCompatibilityJamoWidth } from "./utils";
|
|
23
23
|
|
|
@@ -560,6 +560,12 @@ export function resolveImageProtocol(
|
|
|
560
560
|
) {
|
|
561
561
|
return null;
|
|
562
562
|
}
|
|
563
|
+
// Kitty graphics reach a tmux client only through the DCS passthrough
|
|
564
|
+
// envelope, which tmux drops unless `allow-passthrough` is enabled. A Kitty
|
|
565
|
+
// client identity alone would otherwise send images into the void.
|
|
566
|
+
if (imageProtocol === ImageProtocol.Kitty && env.TMUX && !tmuxPaneAllowsPassthrough(env)) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
563
569
|
return imageProtocol;
|
|
564
570
|
}
|
|
565
571
|
|
package/src/tmux.ts
CHANGED
|
@@ -150,3 +150,33 @@ export function tmuxClientRendersSixel(env: NodeJS.ProcessEnv = Bun.env): boolea
|
|
|
150
150
|
if (cachedClientSixel?.tmux !== tmux) cachedClientSixel = { tmux, renders: queryTmuxClientRendersSixel(env) };
|
|
151
151
|
return cachedClientSixel.renders;
|
|
152
152
|
}
|
|
153
|
+
|
|
154
|
+
let cachedPanePassthrough: { key: string; allowed: boolean } | undefined;
|
|
155
|
+
|
|
156
|
+
function queryTmuxPaneAllowsPassthrough(env: NodeJS.ProcessEnv): boolean {
|
|
157
|
+
const pane = env.TMUX_PANE;
|
|
158
|
+
const target = pane ? ["-t", pane] : [];
|
|
159
|
+
const value = runTmux(["show-options", "-Apqv", ...target, "allow-passthrough"], env)?.trim();
|
|
160
|
+
if (value === undefined) return false;
|
|
161
|
+
// tmux before 3.3 has no `allow-passthrough` option (`-q` prints nothing)
|
|
162
|
+
// and always forwards DCS passthrough.
|
|
163
|
+
return value === "" || value === "on" || value === "all";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Whether tmux forwards this pane's `\x1bPtmux;…\x1b\\` passthrough envelopes
|
|
168
|
+
* to the attached client. tmux 3.3+ defaults `allow-passthrough` to `off` and
|
|
169
|
+
* then silently drops every wrapped sequence, so Kitty graphics sent through
|
|
170
|
+
* the envelope never reach a Kitty-capable outer terminal. Resolves the
|
|
171
|
+
* effective pane value (pane → window → global). Queried once per pane;
|
|
172
|
+
* `false` outside tmux or when the query fails.
|
|
173
|
+
*/
|
|
174
|
+
export function tmuxPaneAllowsPassthrough(env: NodeJS.ProcessEnv = Bun.env): boolean {
|
|
175
|
+
const tmux = env.TMUX;
|
|
176
|
+
if (!tmux) return false;
|
|
177
|
+
const key = `${tmux}\0${env.TMUX_PANE ?? ""}`;
|
|
178
|
+
if (cachedPanePassthrough?.key !== key) {
|
|
179
|
+
cachedPanePassthrough = { key, allowed: queryTmuxPaneAllowsPassthrough(env) };
|
|
180
|
+
}
|
|
181
|
+
return cachedPanePassthrough.allowed;
|
|
182
|
+
}
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -25,7 +25,6 @@ import type {
|
|
|
25
25
|
import { code, compact, diff, node, span, text } from "../native/describe";
|
|
26
26
|
import type { NativeChild, NativeNode } from "../native/node";
|
|
27
27
|
import { diffStatsMeta, displayPath, errorText, fileDiffSection, noteText, resultText, statsText } from "./native-view";
|
|
28
|
-
import { getLanguageFromPath } from "../lang-from-path";
|
|
29
28
|
|
|
30
29
|
/** Display metadata returned by ast-edit. */
|
|
31
30
|
export interface AstEditToolDetails {
|
|
@@ -155,9 +154,9 @@ function parseAstEditChanges(lines: readonly string[]): AstEditFileDiff[] {
|
|
|
155
154
|
return files;
|
|
156
155
|
}
|
|
157
156
|
|
|
158
|
-
/** A file's proposed rewrite as a `diff`:
|
|
157
|
+
/** A file's proposed rewrite as a `diff`: Tern highlights it by the file's grammar and draws no path header. */
|
|
159
158
|
function astEditDiff(file: AstEditFileDiff): NativeNode {
|
|
160
|
-
return diff(file.lines.join("\n"), {
|
|
159
|
+
return diff(file.lines.join("\n"), { path: file.path });
|
|
161
160
|
}
|
|
162
161
|
|
|
163
162
|
/** The single rewrite pattern (`target:"pattern"`), or the rewrite count, for the head. */
|
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 {
|