@oh-my-pi/pi-tui 18.14.2 → 18.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -50
- package/README.md +1 -1
- package/dist/types/app-keybindings.d.ts +5 -0
- package/dist/types/apps/session-picker.d.ts +5 -3
- package/dist/types/chat/bash-execution.d.ts +4 -1
- package/dist/types/chat/eval-execution.d.ts +2 -0
- package/dist/types/chat/execution-shared.d.ts +14 -8
- package/dist/types/chat/transcript-render-helpers.d.ts +8 -0
- package/dist/types/chrome/dynamic-border.d.ts +6 -0
- package/dist/types/chrome/transcript-status.d.ts +2 -2
- package/dist/types/components/editor.d.ts +15 -6
- package/dist/types/components/loader.d.ts +2 -0
- package/dist/types/keybinding-matchers.d.ts +7 -0
- package/dist/types/overlays/btw-history-panel.d.ts +13 -0
- package/dist/types/overlays/btw-history.d.ts +24 -0
- package/dist/types/overlays/btw-inject.d.ts +22 -0
- package/dist/types/overlays/btw-panel.d.ts +7 -0
- package/dist/types/overlays/hook-editor.d.ts +2 -0
- package/dist/types/overlays/jobs-panel.d.ts +4 -0
- package/dist/types/overlays/session-selector.d.ts +24 -3
- package/dist/types/render/render-utils.d.ts +21 -1
- package/dist/types/status-line/startup.d.ts +0 -15
- package/dist/types/terminal-capabilities.d.ts +1 -1
- package/dist/types/theme/symbols.d.ts +3 -1
- package/dist/types/theme/theme-class.d.ts +1 -0
- package/dist/types/tools/grouped-file-output.d.ts +76 -8
- package/dist/types/tools/hashline-format.d.ts +5 -0
- package/dist/types/tools/native-view.d.ts +2 -0
- package/dist/types/tools/output-meta.d.ts +27 -0
- package/dist/types/tools/read.d.ts +10 -0
- package/dist/types/tools/wait.d.ts +2 -0
- package/dist/types/utils.d.ts +4 -0
- package/package.json +9 -9
- package/src/app-keybindings.ts +5 -0
- package/src/apps/session-picker.ts +7 -3
- package/src/chat/bash-execution.ts +20 -9
- package/src/chat/chat-transcript-builder.ts +5 -2
- package/src/chat/eval-execution.ts +14 -7
- package/src/chat/execution-shared.ts +39 -18
- package/src/chat/read-tool-group.ts +56 -36
- package/src/chat/transcript-render-helpers.ts +52 -6
- package/src/chrome/dynamic-border.ts +28 -2
- package/src/chrome/transcript-status.ts +5 -3
- package/src/components/editor.ts +123 -24
- package/src/components/key-value-list.ts +9 -9
- package/src/components/loader.ts +7 -0
- package/src/components/section.ts +1 -9
- package/src/components/table.ts +1 -9
- package/src/keybinding-matchers.ts +13 -0
- package/src/overlays/btw-history-panel.ts +98 -14
- package/src/overlays/btw-history.ts +65 -0
- package/src/overlays/btw-inject.ts +91 -0
- package/src/overlays/btw-panel.ts +53 -12
- package/src/overlays/hook-editor.ts +6 -0
- package/src/overlays/jobs-panel.ts +45 -19
- package/src/overlays/model-browser.ts +65 -8
- package/src/overlays/session-selector.ts +131 -26
- package/src/overlays/usage-dashboard.ts +2 -2
- package/src/render/code-cell.ts +1 -1
- package/src/render/file-list.ts +4 -7
- package/src/render/render-utils.ts +36 -6
- package/src/setup/scenes/glyph.ts +1 -1
- package/src/status-line/startup.ts +36 -70
- package/src/terminal-capabilities.ts +5 -0
- package/src/theme/symbols.ts +16 -9
- package/src/theme/theme-class.ts +3 -0
- package/src/tmux.ts +1 -0
- package/src/tools/agent-tree.ts +13 -11
- package/src/tools/ask.ts +13 -6
- package/src/tools/ast-edit.ts +48 -93
- package/src/tools/ast-grep.ts +22 -66
- package/src/tools/bash.ts +7 -0
- package/src/tools/default-renderer.ts +1 -1
- package/src/tools/edit.ts +50 -43
- package/src/tools/fetch.ts +2 -1
- package/src/tools/find.ts +6 -3
- package/src/tools/github.ts +14 -46
- package/src/tools/glob.ts +2 -1
- package/src/tools/grep.ts +43 -287
- package/src/tools/grouped-file-output.ts +250 -64
- package/src/tools/hashline-format.ts +5 -0
- package/src/tools/json-tree.ts +1 -5
- package/src/tools/lsp.ts +53 -25
- package/src/tools/native-view.ts +11 -5
- package/src/tools/output-meta.ts +68 -4
- package/src/tools/proc-render.ts +3 -3
- package/src/tools/read.ts +14 -0
- package/src/tools/task.ts +34 -77
- package/src/tools/vibe.ts +4 -4
- package/src/tools/wait.ts +8 -5
- package/src/tools/write.ts +17 -27
- package/src/tools/yield.ts +1 -1
- package/src/utils.ts +10 -0
|
@@ -10,9 +10,15 @@
|
|
|
10
10
|
import { Loader } from "../components/loader";
|
|
11
11
|
import { Text } from "../components/text";
|
|
12
12
|
import { Container, type TUI } from "../tui";
|
|
13
|
-
import {
|
|
13
|
+
import { theme } from "../theme/theme";
|
|
14
14
|
import type { OutputArtifactError } from "../tools/streaming-output";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
formatArtifactErrorNotice,
|
|
17
|
+
formatMinimizedMeta,
|
|
18
|
+
formatTruncationMetaNotice,
|
|
19
|
+
type MinimizedMeta,
|
|
20
|
+
type TruncationMeta,
|
|
21
|
+
} from "../tools/output-meta";
|
|
16
22
|
import { DynamicBorder } from "../chrome/dynamic-border";
|
|
17
23
|
import { Ellipsis, truncateToWidth, visibleWidth } from "../utils";
|
|
18
24
|
import { interruptKey } from "../chrome/keybinding-hints";
|
|
@@ -46,14 +52,15 @@ export type ExecutionColorKey = "dim" | "bashMode" | "pythonMode";
|
|
|
46
52
|
/**
|
|
47
53
|
* Build the spacer + top border + content container + bottom border scaffold
|
|
48
54
|
* that bash and eval execution components share. The caller appends the
|
|
49
|
-
* header (command vs `>>>` prompt) and the
|
|
50
|
-
* `
|
|
55
|
+
* header (command vs `>>>` prompt) to `contentContainer` and keeps the run's
|
|
56
|
+
* state in `bottomBorder` ({@link executionStatusLabel}); the returned loader
|
|
57
|
+
* animates that label rather than taking a row of its own.
|
|
51
58
|
*/
|
|
52
59
|
export function buildExecutionFrame(
|
|
53
60
|
parent: Container,
|
|
54
61
|
ui: TUI,
|
|
55
62
|
colorKey: ExecutionColorKey,
|
|
56
|
-
): { contentContainer: Container; loader: Loader } {
|
|
63
|
+
): { contentContainer: Container; loader: Loader; bottomBorder: DynamicBorder } {
|
|
57
64
|
const borderColor = (str: string) => theme.fg(colorKey, str);
|
|
58
65
|
|
|
59
66
|
parent.addChild(new DynamicBorder(borderColor));
|
|
@@ -66,23 +73,35 @@ export function buildExecutionFrame(
|
|
|
66
73
|
spinner => theme.fg(colorKey, spinner),
|
|
67
74
|
text => theme.fg("muted", text),
|
|
68
75
|
`Running… (${interruptKey()} to cancel)`,
|
|
69
|
-
|
|
76
|
+
// The status spinner, as on a running tool card's header.
|
|
77
|
+
theme.spinnerFrames,
|
|
70
78
|
);
|
|
71
79
|
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
const bottomBorder = new DynamicBorder(borderColor);
|
|
81
|
+
bottomBorder.setLabel(() => loader.inlineText());
|
|
82
|
+
parent.addChild(bottomBorder);
|
|
83
|
+
return { contentContainer, loader, bottomBorder };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The bottom-border marker of a settled run: `exit N` for a failure, `cancelled` for a cancel. */
|
|
87
|
+
export function executionStatusLabel(
|
|
88
|
+
status: Exclude<ExecutionStatus, "running">,
|
|
89
|
+
exitCode: number | undefined,
|
|
90
|
+
): string | undefined {
|
|
91
|
+
if (status === "cancelled") return theme.fg("warning", "cancelled");
|
|
92
|
+
if (status === "error") return theme.fg("error", `exit ${exitCode}`);
|
|
93
|
+
return undefined;
|
|
74
94
|
}
|
|
75
95
|
|
|
76
96
|
/**
|
|
77
|
-
* Build the post-run status block (hidden-line hint,
|
|
78
|
-
*
|
|
79
|
-
*
|
|
97
|
+
* Build the post-run status block (hidden-line hint, truncation notice).
|
|
98
|
+
* Returns undefined when there is nothing to display so callers can skip
|
|
99
|
+
* appending a stray Text child.
|
|
80
100
|
*/
|
|
81
101
|
export function buildStatusFooter(opts: {
|
|
82
|
-
status: ExecutionStatus;
|
|
83
|
-
exitCode: number | undefined;
|
|
84
102
|
truncation: TruncationMeta | undefined;
|
|
85
103
|
artifactError?: OutputArtifactError;
|
|
104
|
+
minimized?: MinimizedMeta;
|
|
86
105
|
hiddenLineCount: number;
|
|
87
106
|
/** Suppress the "… N more lines" hint (used when sixel passthrough renders the full output). */
|
|
88
107
|
suppressHiddenCount?: boolean;
|
|
@@ -92,17 +111,15 @@ export function buildStatusFooter(opts: {
|
|
|
92
111
|
if (opts.hiddenLineCount > 0 && !opts.suppressHiddenCount) {
|
|
93
112
|
parts.push(theme.fg("dim", `… ${opts.hiddenLineCount} more lines (${expandKeyHint()} to expand)`));
|
|
94
113
|
}
|
|
95
|
-
if (opts.status === "cancelled") {
|
|
96
|
-
parts.push(theme.fg("warning", "(cancelled)"));
|
|
97
|
-
} else if (opts.status === "error") {
|
|
98
|
-
parts.push(theme.fg("error", `(exit ${opts.exitCode})`));
|
|
99
|
-
}
|
|
100
114
|
if (opts.truncation) {
|
|
101
115
|
parts.push(theme.fg("warning", formatTruncationMetaNotice(opts.truncation)));
|
|
102
116
|
}
|
|
103
117
|
if (opts.artifactError) {
|
|
104
118
|
parts.push(theme.fg("warning", formatArtifactErrorNotice(opts.artifactError)));
|
|
105
119
|
}
|
|
120
|
+
if (opts.minimized) {
|
|
121
|
+
parts.push(theme.fg("dim", `Minimized: ${formatMinimizedMeta(opts.minimized)}`));
|
|
122
|
+
}
|
|
106
123
|
|
|
107
124
|
if (parts.length === 0) return undefined;
|
|
108
125
|
return new Text(`\n${parts.join("\n")}`, 1, 0);
|
|
@@ -133,6 +150,7 @@ export interface ExecutionCardInput {
|
|
|
133
150
|
readonly exitCode: number | undefined;
|
|
134
151
|
readonly truncation: TruncationMeta | undefined;
|
|
135
152
|
readonly artifactError?: OutputArtifactError;
|
|
153
|
+
readonly minimized?: MinimizedMeta;
|
|
136
154
|
}
|
|
137
155
|
|
|
138
156
|
/**
|
|
@@ -170,6 +188,7 @@ export function describeExecutionCard(input: ExecutionCardInput): NativeNode {
|
|
|
170
188
|
if (input.status === "cancelled") note("(cancelled)", "warning");
|
|
171
189
|
else if (input.status === "error") note(`(exit ${input.exitCode})`, "error");
|
|
172
190
|
if (input.truncation) note(formatTruncationMetaNotice(input.truncation), "warning");
|
|
191
|
+
if (input.minimized) note(`Minimized: ${formatMinimizedMeta(input.minimized)}`, "muted");
|
|
173
192
|
if (input.artifactError) note(formatArtifactErrorNotice(input.artifactError), "warning");
|
|
174
193
|
if (notes.length > 0) children.push(text(notes, { wrap: "word", key: "notes" }));
|
|
175
194
|
return card(
|
|
@@ -207,6 +226,7 @@ export interface ExecutionToolInput {
|
|
|
207
226
|
readonly exitCode: number | undefined;
|
|
208
227
|
readonly truncation: TruncationMeta | undefined;
|
|
209
228
|
readonly artifactError?: OutputArtifactError;
|
|
229
|
+
readonly minimized?: MinimizedMeta;
|
|
210
230
|
}
|
|
211
231
|
|
|
212
232
|
const EXECUTION_RUN_STATE: Record<ExecutionStatus, RunState> = {
|
|
@@ -244,6 +264,7 @@ export function describeExecutionTool(input: ExecutionToolInput): NativeNode {
|
|
|
244
264
|
state,
|
|
245
265
|
exitCode: input.exitCode,
|
|
246
266
|
elapsedMs: ms,
|
|
267
|
+
facts: input.minimized ? [`Minimized ${formatMinimizedMeta(input.minimized)}`] : undefined,
|
|
247
268
|
meta: { truncation: input.truncation, artifactError: input.artifactError },
|
|
248
269
|
}),
|
|
249
270
|
],
|
|
@@ -3,7 +3,7 @@ import type { AssistantMessage, Usage } from "@oh-my-pi/pi-ai";
|
|
|
3
3
|
import { type Component } from "../tui";
|
|
4
4
|
import { Container } from "../tui";
|
|
5
5
|
import { Text } from "../components/text";
|
|
6
|
-
import { getLanguageFromPath, theme } from "../theme";
|
|
6
|
+
import { getLanguageFromPath, theme, type ThemeColor } from "../theme";
|
|
7
7
|
import { parseLineRanges, selectorLineRanges } from "../tools/line-ranges";
|
|
8
8
|
import {
|
|
9
9
|
type ReadRenderArgs,
|
|
@@ -12,8 +12,15 @@ import {
|
|
|
12
12
|
readResultIsMarkdown,
|
|
13
13
|
readSourceFsPath,
|
|
14
14
|
splitPathAndSel,
|
|
15
|
+
splitReadQuery,
|
|
15
16
|
} from "../tools/read";
|
|
16
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
FILE_NAME_COLOR,
|
|
19
|
+
formatStatusIcon,
|
|
20
|
+
formatStyledPath,
|
|
21
|
+
PREVIEW_LIMITS,
|
|
22
|
+
shortenPath,
|
|
23
|
+
} from "../render/render-utils";
|
|
17
24
|
import { fileHyperlink, renderCodeCell, renderMarkdownCell } from "../render";
|
|
18
25
|
import { wrapTreeLine } from "../render/hanging-wrap";
|
|
19
26
|
import { wrapTextWithAnsi } from "../utils";
|
|
@@ -21,7 +28,7 @@ import { canonicalizeMessage } from "./thinking-display";
|
|
|
21
28
|
import { internalUrlSchemeSpec, splitUrlScheme } from "../tools/url-scheme-host";
|
|
22
29
|
import type { ToolExecutionHandle } from "./tool-execution";
|
|
23
30
|
import { formatUsageRow } from "../overlays/usage-row";
|
|
24
|
-
import { formatCount, sanitizeText, sanitizeTextDeep } from "@oh-my-pi/pi-utils";
|
|
31
|
+
import { formatCount, isUrlLikePath, sanitizeText, sanitizeTextDeep } from "@oh-my-pi/pi-utils";
|
|
25
32
|
import type { TspCardStatus, TspSpan, TspText } from "@oh-my-pi/pi-wire";
|
|
26
33
|
import type { NativeToolHead } from "../tools/renderer";
|
|
27
34
|
import { card, code, keyed, md, node, span, text, withHidden } from "../native/describe";
|
|
@@ -34,7 +41,7 @@ import {
|
|
|
34
41
|
} from "../native/node";
|
|
35
42
|
import { plainText } from "../native/spans";
|
|
36
43
|
import { Memo } from "../native/memo";
|
|
37
|
-
import { fileHref, fileRow, inlineErrorView } from "../tools/native-view";
|
|
44
|
+
import { fileHref, fileRow, inlineErrorView, urlHref } from "../tools/native-view";
|
|
38
45
|
|
|
39
46
|
/**
|
|
40
47
|
* Extract the read call's target path. `path` is the canonical arg; `file_path`
|
|
@@ -158,6 +165,21 @@ const COLLAPSED_PREVIEW_LINES = PREVIEW_LIMITS.OUTPUT_COLLAPSED;
|
|
|
158
165
|
*/
|
|
159
166
|
const TRIMMED_BODY_PREVIEW = { lines: 1000 } as const;
|
|
160
167
|
|
|
168
|
+
/** How a read path is painted: its path (`isUrl` for a whole URL) and its selectors' color. */
|
|
169
|
+
interface PathPaint {
|
|
170
|
+
path(filePath: string, isUrl: boolean): string;
|
|
171
|
+
selectors: ThemeColor;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** A `Read <path>` header line keeps the accent subject color. */
|
|
175
|
+
const HEADER_PATH_PAINT: PathPaint = { path: filePath => theme.fg("accent", filePath), selectors: "accent" };
|
|
176
|
+
|
|
177
|
+
/** Group rows share the tool-body path style with muted selectors. */
|
|
178
|
+
const ROW_PATH_PAINT: PathPaint = {
|
|
179
|
+
path: (filePath, isUrl) => (isUrl ? theme.fg(FILE_NAME_COLOR, filePath) : formatStyledPath(filePath, theme)),
|
|
180
|
+
selectors: "muted",
|
|
181
|
+
};
|
|
182
|
+
|
|
161
183
|
/** Native item tone per read status; successful reads stay neutral. */
|
|
162
184
|
const READ_STATUS_TONE: Record<ReadEntry["status"], "pending" | "warning" | "error" | undefined> = {
|
|
163
185
|
success: undefined,
|
|
@@ -695,7 +717,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
695
717
|
return {
|
|
696
718
|
target: filePath ? `${filePath}${split.sel ? `:${split.sel}` : ""}` : "…",
|
|
697
719
|
targetKind: "path",
|
|
698
|
-
href: fileHref(linkPathForTargets(row.targets)),
|
|
720
|
+
href: fileHref(linkPathForTargets(row.targets)) ?? urlHref(split.path),
|
|
699
721
|
meta: correctedFrom ? [`corrected from ${shortenPath(correctedFrom)}`] : undefined,
|
|
700
722
|
badges: conflicts
|
|
701
723
|
? [{ text: `${conflicts} conflict${conflicts === 1 ? "" : "s"}`, tone: "warning" }]
|
|
@@ -718,7 +740,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
718
740
|
: undefined;
|
|
719
741
|
return fileRow(shortenPath(split.sel ? split.path : row.targetPath), {
|
|
720
742
|
range: split.sel ? `:${split.sel}` : undefined,
|
|
721
|
-
href: fileHref(linkPathForTargets(row.targets)),
|
|
743
|
+
href: fileHref(linkPathForTargets(row.targets)) ?? urlHref(split.path),
|
|
722
744
|
icon: status === "error" ? "x-circle" : undefined,
|
|
723
745
|
tone: READ_STATUS_TONE[status],
|
|
724
746
|
chip: conflicts ? { text: `${conflicts} conflict${conflicts === 1 ? "" : "s"}` } : undefined,
|
|
@@ -914,8 +936,8 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
914
936
|
if (displayRows.length === 1) {
|
|
915
937
|
const row = displayRows[0]!;
|
|
916
938
|
if (!this.#shouldRenderPreviewRow(row)) {
|
|
917
|
-
const statusSymbol =
|
|
918
|
-
const pathDisplay = this.#formatRowPath(row);
|
|
939
|
+
const statusSymbol = formatStatusIcon(this.#statusForTargets(row.targets), theme);
|
|
940
|
+
const pathDisplay = this.#formatRowPath(row, HEADER_PATH_PAINT);
|
|
919
941
|
const lines = [` ${statusSymbol} ${theme.fg("toolTitle", theme.bold("Read"))} ${pathDisplay}`.trimEnd()];
|
|
920
942
|
const usageRows = this.#usageRowsBySummaryRow(displayRows).get(0) ?? [];
|
|
921
943
|
this.#appendUsageRows(lines, usageRows, " ");
|
|
@@ -1080,16 +1102,17 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
1080
1102
|
|
|
1081
1103
|
#formatRow(row: ReadSummaryRow): string {
|
|
1082
1104
|
const status = this.#statusForTargets(row.targets);
|
|
1083
|
-
const statusPrefix = status === "success" ? "" : `${
|
|
1105
|
+
const statusPrefix = status === "success" ? "" : `${formatStatusIcon(status, theme)} `;
|
|
1084
1106
|
return `${statusPrefix}${this.#formatRowPath(row)}`;
|
|
1085
1107
|
}
|
|
1086
1108
|
|
|
1087
|
-
#formatRowPath(row: ReadSummaryRow): string {
|
|
1088
|
-
return this.#formatPathValue(row.targetPath, {
|
|
1109
|
+
#formatRowPath(row: ReadSummaryRow, paint = ROW_PATH_PAINT): string {
|
|
1110
|
+
return this.#formatPathValue(row.targetPath, paint, {
|
|
1089
1111
|
correctedFrom: this.#correctedFromForTargets(row.targets),
|
|
1090
1112
|
conflictCount: this.#conflictCountForTargets(row.targets),
|
|
1091
1113
|
line: firstSelectorLineForTargets(row.targets),
|
|
1092
1114
|
linkPath: linkPathForTargets(row.targets),
|
|
1115
|
+
basePath: row.basePath,
|
|
1093
1116
|
});
|
|
1094
1117
|
}
|
|
1095
1118
|
|
|
@@ -1137,25 +1160,35 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
1137
1160
|
|
|
1138
1161
|
#formatPathValue(
|
|
1139
1162
|
value: string,
|
|
1140
|
-
|
|
1163
|
+
paint: PathPaint,
|
|
1164
|
+
options: {
|
|
1165
|
+
correctedFrom?: string;
|
|
1166
|
+
conflictCount?: number;
|
|
1167
|
+
line?: number;
|
|
1168
|
+
linkPath?: string;
|
|
1169
|
+
/** The path `value` selects from; merged selectors (`1-5,…,9`) do not parse back out of it. */
|
|
1170
|
+
basePath?: string;
|
|
1171
|
+
} = {},
|
|
1141
1172
|
): string {
|
|
1142
|
-
const
|
|
1143
|
-
const
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1173
|
+
const { basePath } = options;
|
|
1174
|
+
const split =
|
|
1175
|
+
basePath !== undefined && value.startsWith(`${basePath}:`)
|
|
1176
|
+
? { path: basePath, sel: value.slice(basePath.length + 1) }
|
|
1177
|
+
: splitPathAndSel(value);
|
|
1178
|
+
// `?q=…` queries read like selectors: they follow the path, outside its link.
|
|
1179
|
+
const target = splitReadQuery(split.sel ? split.path : value);
|
|
1180
|
+
const filePath = shortenPath(target.path);
|
|
1181
|
+
const selectors = split.sel ? `${target.query ?? ""}:${split.sel}` : (target.query ?? "");
|
|
1182
|
+
let pathDisplay = filePath ? paint.path(filePath, isUrlLikePath(target.path)) : theme.fg("toolOutput", "…");
|
|
1147
1183
|
if (filePath && options.linkPath) {
|
|
1148
1184
|
const linkOptions = options.line !== undefined ? { line: options.line } : undefined;
|
|
1149
1185
|
pathDisplay = fileHyperlink(options.linkPath, pathDisplay, linkOptions);
|
|
1150
1186
|
}
|
|
1151
|
-
if (
|
|
1152
|
-
pathDisplay += theme.fg("accent", selectorSuffix);
|
|
1153
|
-
}
|
|
1187
|
+
if (selectors) pathDisplay += theme.fg(paint.selectors, selectors);
|
|
1154
1188
|
if (options.correctedFrom) {
|
|
1155
1189
|
pathDisplay += theme.fg("dim", ` (corrected from ${shortenPath(options.correctedFrom)})`);
|
|
1156
1190
|
}
|
|
1157
|
-
pathDisplay
|
|
1158
|
-
return pathDisplay;
|
|
1191
|
+
return pathDisplay + this.#formatConflictBadge(options.conflictCount);
|
|
1159
1192
|
}
|
|
1160
1193
|
|
|
1161
1194
|
#formatConflictBadge(conflictCount: number | undefined): string {
|
|
@@ -1174,7 +1207,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
1174
1207
|
const lang = getLanguageFromPath(split.path);
|
|
1175
1208
|
const pathValue = shortenPath(entry.path);
|
|
1176
1209
|
const pathDisplay = pathValue
|
|
1177
|
-
? this.#formatPathValue(entry.path, {
|
|
1210
|
+
? this.#formatPathValue(entry.path, HEADER_PATH_PAINT, {
|
|
1178
1211
|
correctedFrom: entry.correctedFrom,
|
|
1179
1212
|
conflictCount: entry.conflictCount,
|
|
1180
1213
|
line: firstSelectorLine(split.sel),
|
|
@@ -1252,17 +1285,4 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
1252
1285
|
#shouldRenderPreview(entry: ReadEntry): boolean {
|
|
1253
1286
|
return this.#showContentPreview && entry.contentText !== undefined;
|
|
1254
1287
|
}
|
|
1255
|
-
|
|
1256
|
-
#formatStatus(status: ReadEntry["status"]): string {
|
|
1257
|
-
if (status === "success") {
|
|
1258
|
-
return theme.fg("text", theme.status.enabled);
|
|
1259
|
-
}
|
|
1260
|
-
if (status === "warning") {
|
|
1261
|
-
return theme.fg("warning", theme.status.warning);
|
|
1262
|
-
}
|
|
1263
|
-
if (status === "error") {
|
|
1264
|
-
return theme.fg("error", theme.status.error);
|
|
1265
|
-
}
|
|
1266
|
-
return theme.fg("dim", theme.status.pending);
|
|
1267
|
-
}
|
|
1268
1288
|
}
|
|
@@ -13,11 +13,18 @@ import type { JobSnapshot } from "../tools/wait";
|
|
|
13
13
|
import type { DaemonSnapshot } from "../tools/daemon";
|
|
14
14
|
import { type CustomMessage, type FileMentionMessage, resolveAbortLabel, shouldRenderAbortReason } from "./messages";
|
|
15
15
|
import { createIrcMessageCard } from "../tools/wait";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
formatArtifactErrorNotice,
|
|
18
|
+
formatMinimizedMeta,
|
|
19
|
+
type MinimizedMeta,
|
|
20
|
+
type OutputMeta,
|
|
21
|
+
} from "../tools/output-meta";
|
|
17
22
|
import type { OutputArtifactError } from "../tools/streaming-output";
|
|
18
|
-
import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../render/render-utils";
|
|
23
|
+
import { formatStyledPath, replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../render/render-utils";
|
|
19
24
|
import { canonicalizeMessage } from "./thinking-display";
|
|
20
25
|
import { ToolActivityContainer } from "../chrome/tool-activity";
|
|
26
|
+
import { CustomMessageComponent } from "./custom-message";
|
|
27
|
+
import type { MessageRenderer } from "./extension-types";
|
|
21
28
|
import { type TranscriptBlock } from "../chrome/transcript-container";
|
|
22
29
|
import { TranscriptStatusBlock, type TranscriptStatusRow } from "../chrome/transcript-status";
|
|
23
30
|
import { theme } from "../theme";
|
|
@@ -65,7 +72,7 @@ export function buildAsyncResultBlock(message: CustomOrHookMessage): ToolActivit
|
|
|
65
72
|
const duration = typeof job.durationMs === "number" ? formatDuration(job.durationMs) : undefined;
|
|
66
73
|
rows.push({
|
|
67
74
|
parts: [
|
|
68
|
-
{ text: `${theme.status.
|
|
75
|
+
{ text: `${theme.status.success} Background job completed`, color: "success" },
|
|
69
76
|
{ text: `[${type}]`, color: "dim" },
|
|
70
77
|
{ text: jobId, color: "accent" },
|
|
71
78
|
duration ? { text: `(${duration})`, color: "dim" } : undefined,
|
|
@@ -76,17 +83,56 @@ export function buildAsyncResultBlock(message: CustomOrHookMessage): ToolActivit
|
|
|
76
83
|
},
|
|
77
84
|
});
|
|
78
85
|
if (job.meta?.artifactError) rows.push(artifactErrorRow(job.meta.artifactError));
|
|
86
|
+
if (job.meta?.minimized) rows.push(minimizedRow(job.meta.minimized));
|
|
79
87
|
}
|
|
80
88
|
if (details?.meta?.artifactError) rows.push(artifactErrorRow(details.meta.artifactError));
|
|
81
89
|
return new ToolActivityContainer(new TranscriptStatusBlock(rows));
|
|
82
90
|
}
|
|
83
91
|
|
|
92
|
+
/** Hosts an extension renderer; relays `display.hideToolActivity` to the nested built-in fallback block. */
|
|
93
|
+
class AsyncResultCard extends CustomMessageComponent {
|
|
94
|
+
readonly #block: ToolActivityContainer;
|
|
95
|
+
|
|
96
|
+
constructor(message: CustomOrHookMessage, renderer: MessageRenderer, block: ToolActivityContainer) {
|
|
97
|
+
super(message as CustomMessage<unknown>, (delegate, options, rendererTheme) => {
|
|
98
|
+
try {
|
|
99
|
+
return renderer(delegate, options, rendererTheme) ?? block;
|
|
100
|
+
} catch {
|
|
101
|
+
return block;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
this.#block = block;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
setToolActivityVisible(visible: boolean): void {
|
|
108
|
+
this.#block.setToolActivityVisible(visible);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The `async-result` card for both transcript surfaces: a registered renderer owns it (#14131);
|
|
114
|
+
* the built-in block covers no renderer, a declining renderer, and a throwing one.
|
|
115
|
+
*/
|
|
116
|
+
export function buildAsyncResultComponent(
|
|
117
|
+
message: CustomOrHookMessage,
|
|
118
|
+
renderer: MessageRenderer | undefined,
|
|
119
|
+
): Component & { setExpanded(expanded: boolean): void } {
|
|
120
|
+
const block = buildAsyncResultBlock(message);
|
|
121
|
+
return renderer ? new AsyncResultCard(message, renderer, block) : block;
|
|
122
|
+
}
|
|
123
|
+
|
|
84
124
|
/** The warning row under a background job whose output artifact could not be saved. */
|
|
85
125
|
function artifactErrorRow(error: OutputArtifactError): TranscriptStatusRow {
|
|
86
126
|
const notice = formatArtifactErrorNotice(error);
|
|
87
127
|
return { parts: [{ text: notice, color: "warning" }], native: { tone: "warning", message: [span(notice)] } };
|
|
88
128
|
}
|
|
89
129
|
|
|
130
|
+
/** The dim row under a background job whose output the minimizer shortened. */
|
|
131
|
+
function minimizedRow(minimized: MinimizedMeta): TranscriptStatusRow {
|
|
132
|
+
const fact = `Minimized: ${formatMinimizedMeta(minimized)}`;
|
|
133
|
+
return { parts: [{ text: fact, color: "dim" }], native: { tone: "info", message: [span(fact, "dim")] } };
|
|
134
|
+
}
|
|
135
|
+
|
|
90
136
|
/**
|
|
91
137
|
* Native notice spans for a row's trailing facts (kind, id, duration): each
|
|
92
138
|
* after a dim ` · `, absent ones skipped.
|
|
@@ -106,7 +152,7 @@ export function buildLaunchCompletionBlock(message: CustomOrHookMessage): ToolAc
|
|
|
106
152
|
const daemons = details?.daemons ?? [];
|
|
107
153
|
if (daemons.length === 0 && typeof message.content === "string") {
|
|
108
154
|
rows.push({
|
|
109
|
-
parts: [{ text: `${theme.status.
|
|
155
|
+
parts: [{ text: `${theme.status.success} ${message.content}`, color: "dim" }],
|
|
110
156
|
native: { tone: "success", message: [span(message.content)] },
|
|
111
157
|
});
|
|
112
158
|
}
|
|
@@ -122,7 +168,7 @@ export function buildLaunchCompletionBlock(message: CustomOrHookMessage): ToolAc
|
|
|
122
168
|
parts: [
|
|
123
169
|
failed
|
|
124
170
|
? { text: `${theme.status.error} ${label}`, color: "error" }
|
|
125
|
-
: { text: `${theme.status.
|
|
171
|
+
: { text: `${theme.status.success} ${label}`, color: "success" },
|
|
126
172
|
{ text: daemon.name, color: "accent" },
|
|
127
173
|
exit ? { text: `(${exit})`, color: "dim" } : undefined,
|
|
128
174
|
duration ? { text: `(${duration})`, color: "dim" } : undefined,
|
|
@@ -203,7 +249,7 @@ export function buildFileMentionBlock(files: FileMentionMessage["files"], indent
|
|
|
203
249
|
parts: [
|
|
204
250
|
{ text: theme.tree.last, color: "dim" },
|
|
205
251
|
{ text: "Read", color: "muted" },
|
|
206
|
-
{ text: file.path,
|
|
252
|
+
{ text: formatStyledPath(file.path, theme) },
|
|
207
253
|
{ text: `(${detail})`, color: "dim" },
|
|
208
254
|
],
|
|
209
255
|
indent,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Component } from "../tui";
|
|
2
|
+
import { truncateToWidth, visibleWidth } from "../utils";
|
|
2
3
|
import { fgOrPlain, theme } from "../theme/index";
|
|
3
4
|
import type { NativeNode } from "../native/node";
|
|
4
5
|
import { EMPTY_NODE } from "../native/describe";
|
|
@@ -13,7 +14,9 @@ import { EMPTY_NODE } from "../native/describe";
|
|
|
13
14
|
*/
|
|
14
15
|
export class DynamicBorder implements Component {
|
|
15
16
|
#color: (str: string) => string;
|
|
17
|
+
#label: string | (() => string) | undefined;
|
|
16
18
|
#cachedWidth = -1;
|
|
19
|
+
#cachedLabel: string | undefined;
|
|
17
20
|
#cachedLines: string[] | undefined;
|
|
18
21
|
|
|
19
22
|
constructor(color: (str: string) => string = str => fgOrPlain("border", str)) {
|
|
@@ -25,19 +28,42 @@ export class DynamicBorder implements Component {
|
|
|
25
28
|
this.#cachedLines = undefined;
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Pre-styled text set into the rule after a short lead (`─── label ────`);
|
|
33
|
+
* undefined draws a plain rule. A function label is re-read every render,
|
|
34
|
+
* for live text such as a spinner.
|
|
35
|
+
*/
|
|
36
|
+
setLabel(label: string | (() => string) | undefined): void {
|
|
37
|
+
if (label === this.#label) return;
|
|
38
|
+
this.#label = label;
|
|
39
|
+
this.invalidate();
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
/** Native terminals draw their own chrome; a border rule describes nothing. */
|
|
29
43
|
describe(): NativeNode {
|
|
30
44
|
return EMPTY_NODE;
|
|
31
45
|
}
|
|
32
46
|
|
|
33
47
|
render(width: number): readonly string[] {
|
|
34
|
-
|
|
48
|
+
const label = typeof this.#label === "function" ? this.#label() : this.#label;
|
|
49
|
+
if (this.#cachedLines && this.#cachedWidth === width && this.#cachedLabel === label) {
|
|
35
50
|
return this.#cachedLines;
|
|
36
51
|
}
|
|
37
52
|
const horizontal = typeof theme === "undefined" ? "─" : theme.boxRound.horizontal;
|
|
38
|
-
const lines = [this.#
|
|
53
|
+
const lines = [this.#rule(horizontal, Math.max(1, width), label)];
|
|
39
54
|
this.#cachedWidth = width;
|
|
55
|
+
this.#cachedLabel = label;
|
|
40
56
|
this.#cachedLines = lines;
|
|
41
57
|
return lines;
|
|
42
58
|
}
|
|
59
|
+
|
|
60
|
+
#rule(horizontal: string, width: number, label: string | undefined): string {
|
|
61
|
+
const lead = horizontal.repeat(3);
|
|
62
|
+
// The label sits between single spaces and leaves at least one rule cell after it.
|
|
63
|
+
const labelRoom = width - visibleWidth(lead) - 3;
|
|
64
|
+
if (!label || labelRoom < 1) return this.#color(horizontal.repeat(width));
|
|
65
|
+
const fitted = ` ${truncateToWidth(label, labelRoom)} `;
|
|
66
|
+
const rest = width - visibleWidth(lead) - visibleWidth(fitted);
|
|
67
|
+
return `${this.#color(lead)}${fitted}${this.#color(horizontal.repeat(rest))}`;
|
|
68
|
+
}
|
|
43
69
|
}
|
|
@@ -6,10 +6,10 @@ import { type ThemeColor, theme } from "../theme";
|
|
|
6
6
|
import { TranscriptBlock } from "./transcript-container";
|
|
7
7
|
import { type NoticeTone, noticeRow } from "./transcript-notice";
|
|
8
8
|
|
|
9
|
-
/** One
|
|
9
|
+
/** One run of a status row's terminal line, in `color` unless the text is already styled. */
|
|
10
10
|
export interface TranscriptStatusPart {
|
|
11
11
|
readonly text: string;
|
|
12
|
-
readonly color
|
|
12
|
+
readonly color?: ThemeColor;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/** A status row in a native terminal: its {@link noticeRow}'s tone, icon and message. */
|
|
@@ -41,7 +41,9 @@ export class TranscriptStatusBlock extends TranscriptBlock {
|
|
|
41
41
|
constructor(rows: readonly TranscriptStatusRow[]) {
|
|
42
42
|
super();
|
|
43
43
|
for (const row of rows) {
|
|
44
|
-
const styled = row.parts.flatMap(part =>
|
|
44
|
+
const styled = row.parts.flatMap(part =>
|
|
45
|
+
part ? [part.color ? theme.fg(part.color, part.text) : part.text] : [],
|
|
46
|
+
);
|
|
45
47
|
this.addChild(new Text(styled.join(" "), row.indent ?? 1, 0));
|
|
46
48
|
}
|
|
47
49
|
this.#native = col(
|