@quandev104/pi-style 0.1.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 +35 -0
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/dist/extensions/pi-style.js +7897 -0
- package/dist/extensions/pi-style.js.map +1 -0
- package/extension-src/pi-style/app/command-service.ts +244 -0
- package/extension-src/pi-style/app/commands.ts +12 -0
- package/extension-src/pi-style/app/config-storage.ts +98 -0
- package/extension-src/pi-style/app/doctor.ts +53 -0
- package/extension-src/pi-style/app/index.ts +322 -0
- package/extension-src/pi-style/app/providers.ts +268 -0
- package/extension-src/pi-style/app/render-scheduler.ts +48 -0
- package/extension-src/pi-style/app/runtime.ts +404 -0
- package/extension-src/pi-style/app/snapshot.ts +15 -0
- package/extension-src/pi-style/domain/capabilities.ts +34 -0
- package/extension-src/pi-style/domain/config-authorization.ts +20 -0
- package/extension-src/pi-style/domain/config-diagnostics.ts +20 -0
- package/extension-src/pi-style/domain/config-diff.ts +30 -0
- package/extension-src/pi-style/domain/config-migrations.ts +52 -0
- package/extension-src/pi-style/domain/config-normalization.ts +567 -0
- package/extension-src/pi-style/domain/config-presets.ts +51 -0
- package/extension-src/pi-style/domain/config-types.ts +115 -0
- package/extension-src/pi-style/domain/providers.ts +37 -0
- package/extension-src/pi-style/domain/status-presets.ts +60 -0
- package/extension-src/pi-style/domain/status-renderer.ts +142 -0
- package/extension-src/pi-style/domain/status.ts +430 -0
- package/extension-src/pi-style/domain/theme.ts +232 -0
- package/extension-src/pi-style/features/.gitkeep +0 -0
- package/extension-src/pi-style/features/editor/index.ts +304 -0
- package/extension-src/pi-style/features/messages/boxed-block.ts +74 -0
- package/extension-src/pi-style/features/messages/index.ts +145 -0
- package/extension-src/pi-style/features/messages/special-blocks.ts +281 -0
- package/extension-src/pi-style/features/startup/index.ts +564 -0
- package/extension-src/pi-style/features/startup/logo.ts +151 -0
- package/extension-src/pi-style/features/status-line/index.ts +319 -0
- package/extension-src/pi-style/features/tools/boxed/bash.ts +347 -0
- package/extension-src/pi-style/features/tools/boxed/edit.ts +113 -0
- package/extension-src/pi-style/features/tools/boxed/fallback.ts +67 -0
- package/extension-src/pi-style/features/tools/boxed/find.ts +65 -0
- package/extension-src/pi-style/features/tools/boxed/grep.ts +108 -0
- package/extension-src/pi-style/features/tools/boxed/index.ts +64 -0
- package/extension-src/pi-style/features/tools/boxed/ls.ts +65 -0
- package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +190 -0
- package/extension-src/pi-style/features/tools/boxed/read.ts +204 -0
- package/extension-src/pi-style/features/tools/boxed/session-config.ts +45 -0
- package/extension-src/pi-style/features/tools/boxed/shared.ts +157 -0
- package/extension-src/pi-style/features/tools/boxed/write.ts +89 -0
- package/extension-src/pi-style/features/tools/index.ts +480 -0
- package/extension-src/pi-style/pi/commands.ts +17 -0
- package/extension-src/pi-style/pi/compatibility-coordinator.ts +195 -0
- package/extension-src/pi-style/pi/compatibility-probe.ts +645 -0
- package/extension-src/pi-style/pi/compatibility-registry.ts +329 -0
- package/extension-src/pi-style/pi/config-host.ts +52 -0
- package/extension-src/pi-style/pi/config-session.ts +105 -0
- package/extension-src/pi-style/pi/index.ts +77 -0
- package/extension-src/pi-style/pi/operational-state.ts +29 -0
- package/extension-src/pi-style/pi/session-coordinator.ts +187 -0
- package/extension-src/pi-style/pi/session-usage.ts +62 -0
- package/extension-src/pi-style/pi/startup-resources.ts +70 -0
- package/extension-src/pi-style/shared/.gitkeep +0 -0
- package/extension-src/pi-style/shared/ansi.ts +386 -0
- package/extension-src/pi-style/shared/box.ts +805 -0
- package/extension-src/pi-style/shared/disposable-store.ts +28 -0
- package/extension-src/pi-style/shared/elapsed.ts +88 -0
- package/extension-src/pi-style/shared/render-budget.ts +367 -0
- package/extension-src/pi-style/shared/split-diff.ts +692 -0
- package/extension-src/pi-style/shared/theme-extras.ts +292 -0
- package/package.json +68 -0
- package/themes/.gitkeep +0 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// Boxed bash tool renderer
|
|
2
|
+
// (renderCall/renderResult only).
|
|
3
|
+
|
|
4
|
+
import { highlightCode } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
6
|
+
import { stripAnsi } from "../../../shared/ansi.js";
|
|
7
|
+
import type { BoxTheme } from "../../../shared/box.js";
|
|
8
|
+
import {
|
|
9
|
+
boxedToolWidthKey,
|
|
10
|
+
formatBoxedFooter,
|
|
11
|
+
formatToolOutputLine,
|
|
12
|
+
getTextOutput,
|
|
13
|
+
renderBoxedToolCall,
|
|
14
|
+
renderBoxedToolResult,
|
|
15
|
+
replaceTabs,
|
|
16
|
+
} from "../../../shared/box.js";
|
|
17
|
+
import { safeTruncateToWidth, truncateAtCodePointBoundary } from "../../../shared/render-budget.js";
|
|
18
|
+
import { getStateElapsedMs, getToolsRenderConfig } from "./session-config.js";
|
|
19
|
+
import { type BoxedToolContext, type BoxedToolDefinition, noteExecutionStart } from "./shared.js";
|
|
20
|
+
|
|
21
|
+
const MAX_BASH_PREVIEW_LINES = 5;
|
|
22
|
+
const MAX_LINE_CHARS = 2000;
|
|
23
|
+
const ESC = "\x1b";
|
|
24
|
+
const BASH_TOOL_NOTICE_PATTERN = /^\[Showing (?:last|lines)\b.*\. Full output: .+\]$/;
|
|
25
|
+
const BG_ANSI_PATTERN = new RegExp(`${ESC}\\[4[0-9;]*m`, "g");
|
|
26
|
+
const SHELL_VAR_PATTERN = /\$\{?[A-Za-z_][A-Za-z0-9_]*\}?/;
|
|
27
|
+
const SHELL_OP_PATTERN = /^(?:&&|\|\||>>|>&|\|&|[|&;()<>])$/;
|
|
28
|
+
|
|
29
|
+
function highlightBashFallback(line: string): string {
|
|
30
|
+
try {
|
|
31
|
+
const highlighted = highlightCode(line, "bash")[0] ?? line;
|
|
32
|
+
// Strip background colors to avoid clashing with badge/parens styling
|
|
33
|
+
return highlighted.replace(BG_ANSI_PATTERN, "");
|
|
34
|
+
} catch {
|
|
35
|
+
return line;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeShellWord(word: string): string {
|
|
40
|
+
return word.replace(/^(['"])(.*)\1$/, "$2");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function colorShellWord(theme: BoxTheme, word: string, commandExpected: boolean): string {
|
|
44
|
+
const normalized = normalizeShellWord(word);
|
|
45
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=.*/.test(normalized)) return theme.fg("syntaxVariable", word);
|
|
46
|
+
if (normalized.startsWith("-")) return theme.fg("syntaxKeyword", word);
|
|
47
|
+
if (normalized.includes("/") || /^\.{1,2}(?:\/|$)/.test(normalized)) return theme.fg("syntaxVariable", word);
|
|
48
|
+
if (SHELL_VAR_PATTERN.test(normalized)) return theme.fg("syntaxVariable", word);
|
|
49
|
+
return commandExpected ? theme.fg("syntaxFunction", word) : theme.fg("syntaxString", word);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tokenizeShellLinePreservingText(line: string): string[] | undefined {
|
|
53
|
+
const tokens: string[] = [];
|
|
54
|
+
let current = "";
|
|
55
|
+
let quote: string | null = null;
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < line.length; i++) {
|
|
58
|
+
const char = line[i] ?? "";
|
|
59
|
+
const next = line[i + 1] ?? "";
|
|
60
|
+
|
|
61
|
+
if (quote) {
|
|
62
|
+
current += char;
|
|
63
|
+
if (char === "\\" && next) current += line[++i] ?? "";
|
|
64
|
+
else if (char === quote) quote = null;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (char === "'" || char === '"') {
|
|
69
|
+
quote = char;
|
|
70
|
+
current += char;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (/\s/.test(char)) {
|
|
75
|
+
if (current) tokens.push(current);
|
|
76
|
+
current = "";
|
|
77
|
+
tokens.push(char);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (char === "#" && !current) {
|
|
82
|
+
if (current) tokens.push(current);
|
|
83
|
+
tokens.push(line.slice(i));
|
|
84
|
+
return tokens;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const two = `${char}${next}`;
|
|
88
|
+
if (SHELL_OP_PATTERN.test(two) || SHELL_OP_PATTERN.test(char)) {
|
|
89
|
+
if (current) tokens.push(current);
|
|
90
|
+
current = "";
|
|
91
|
+
if (SHELL_OP_PATTERN.test(two)) {
|
|
92
|
+
tokens.push(two);
|
|
93
|
+
i++;
|
|
94
|
+
} else {
|
|
95
|
+
tokens.push(char);
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
current += char;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (quote) return undefined;
|
|
104
|
+
if (current) tokens.push(current);
|
|
105
|
+
return tokens;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function highlightBashLine(line: string, theme: BoxTheme): string {
|
|
109
|
+
const tokens = tokenizeShellLinePreservingText(line);
|
|
110
|
+
if (!tokens) return highlightBashFallback(line);
|
|
111
|
+
let commandExpected = true;
|
|
112
|
+
return tokens
|
|
113
|
+
.map((token) => {
|
|
114
|
+
if (/^\s+$/.test(token)) return token;
|
|
115
|
+
if (token.startsWith("#")) return theme.fg("syntaxComment", token);
|
|
116
|
+
if (SHELL_OP_PATTERN.test(token)) {
|
|
117
|
+
commandExpected = token === "|" || token === "||" || token === "&&" || token === ";" || token === "&";
|
|
118
|
+
return theme.fg("syntaxOperator", token);
|
|
119
|
+
}
|
|
120
|
+
const styled = colorShellWord(theme, token, commandExpected);
|
|
121
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*=.*/.test(normalizeShellWord(token))) commandExpected = false;
|
|
122
|
+
return styled;
|
|
123
|
+
})
|
|
124
|
+
.join("");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function clampLineLength(line: string, max: number = MAX_LINE_CHARS): string {
|
|
128
|
+
if (line.length <= max) return line;
|
|
129
|
+
return `${truncateAtCodePointBoundary(line, max)}… (truncated)`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function countNewlines(text: string, from: number, to: number): number {
|
|
133
|
+
let count = 0;
|
|
134
|
+
for (let i = from; i < to; i++) {
|
|
135
|
+
if (text.charCodeAt(i) === 10) count++;
|
|
136
|
+
}
|
|
137
|
+
return count;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function stripBashToolNoticeLines(text: string): string {
|
|
141
|
+
const filteredLines = text
|
|
142
|
+
.replace(/\r/g, "")
|
|
143
|
+
.split("\n")
|
|
144
|
+
.filter((line) => !BASH_TOOL_NOTICE_PATTERN.test(line.trim()));
|
|
145
|
+
return filteredLines
|
|
146
|
+
.join("\n")
|
|
147
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
148
|
+
.trimEnd();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function bashWidthKey(rawCommand: string, timeout: unknown): string {
|
|
152
|
+
return boxedToolWidthKey("Bash", `${rawCommand}|${timeout ?? ""}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function renderBoxedBashCall(
|
|
156
|
+
theme: BoxTheme,
|
|
157
|
+
commandLines: string[],
|
|
158
|
+
context: BoxedToolContext,
|
|
159
|
+
widthKey: string,
|
|
160
|
+
): Component {
|
|
161
|
+
const maxCommandLines = 5;
|
|
162
|
+
const shownCount = Math.min(commandLines.length, maxCommandLines + 1);
|
|
163
|
+
const detailLines: string[] = [];
|
|
164
|
+
for (let i = 0; i < shownCount; i++) {
|
|
165
|
+
const prefix = i === 0 ? theme.fg("dim", "$ ") : theme.fg("dim", "> ");
|
|
166
|
+
detailLines.push(`${prefix}${highlightBashLine(commandLines[i] ?? "", theme)}`);
|
|
167
|
+
}
|
|
168
|
+
if (commandLines.length > maxCommandLines + 1) {
|
|
169
|
+
detailLines.push(theme.fg("muted", `... ${commandLines.length - maxCommandLines - 1} more lines`));
|
|
170
|
+
}
|
|
171
|
+
return renderBoxedToolCall(theme, "Bash", detailLines, {
|
|
172
|
+
widthKey,
|
|
173
|
+
isError: Boolean(context.isError),
|
|
174
|
+
isPartial: Boolean(context.isPartial),
|
|
175
|
+
isPending: Boolean(context.isPartial),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function formatTimeout(context: BoxedToolContext): string {
|
|
180
|
+
const timeout = context?.args?.timeout ?? 300;
|
|
181
|
+
return `${timeout}s`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function renderBoxedBashResult(
|
|
185
|
+
theme: BoxTheme,
|
|
186
|
+
inner: Component,
|
|
187
|
+
result: unknown,
|
|
188
|
+
context: BoxedToolContext,
|
|
189
|
+
): Component {
|
|
190
|
+
const rawCommand = String(context?.args?.command ?? "...");
|
|
191
|
+
const referenceLines = rawCommand.split("\n").map((line, index) => `${index === 0 ? "$ " : "> "}${line}`);
|
|
192
|
+
return renderBoxedToolResult(theme, inner, {
|
|
193
|
+
widthKey: bashWidthKey(rawCommand, context?.args?.timeout),
|
|
194
|
+
referenceLines,
|
|
195
|
+
footerLines: [formatBoxedFooter(theme, result as never, [`⏹ ${formatTimeout(context)}`], getElapsed(context))],
|
|
196
|
+
isError: context.isError,
|
|
197
|
+
isPartial: Boolean(context.isPartial),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function getElapsed(context: BoxedToolContext): number | undefined {
|
|
202
|
+
return getStateElapsedMs(context.state);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function createBashResultPreview(
|
|
206
|
+
theme: BoxTheme,
|
|
207
|
+
text: string,
|
|
208
|
+
options: { expanded: boolean },
|
|
209
|
+
color: "toolOutput" | "error",
|
|
210
|
+
extraLinesBefore: number = 0,
|
|
211
|
+
): Component {
|
|
212
|
+
let cacheKey = "";
|
|
213
|
+
let cacheLines: string[] | null = null;
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
invalidate() {
|
|
217
|
+
cacheKey = "";
|
|
218
|
+
cacheLines = null;
|
|
219
|
+
},
|
|
220
|
+
render(width: number): string[] {
|
|
221
|
+
const bodyWidth = Math.max(1, width);
|
|
222
|
+
const cfg = getToolsRenderConfig();
|
|
223
|
+
const expanded = Boolean(options.expanded);
|
|
224
|
+
const cacheId = `${bodyWidth}|${expanded ? 1 : 0}|${cfg.maxExpandedLines}|${cfg.dimOutput ? 1 : 0}`;
|
|
225
|
+
if (cacheLines && cacheKey === cacheId) return cacheLines;
|
|
226
|
+
|
|
227
|
+
if (!expanded) {
|
|
228
|
+
// Collapsed: only process the tail of the output
|
|
229
|
+
const needed = MAX_BASH_PREVIEW_LINES;
|
|
230
|
+
let totalNewlines = 0;
|
|
231
|
+
let scanFrom = 0; // default: take full text if fewer than needed newlines
|
|
232
|
+
for (let i = text.length - 1; i >= 0; i--) {
|
|
233
|
+
if (text.charCodeAt(i) === 10) {
|
|
234
|
+
totalNewlines++;
|
|
235
|
+
if (totalNewlines === needed) {
|
|
236
|
+
scanFrom = i + 1;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (text.length === 0) {
|
|
243
|
+
cacheKey = cacheId;
|
|
244
|
+
cacheLines = [];
|
|
245
|
+
return cacheLines;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const tail = replaceTabs(text.slice(scanFrom)).replace(/\r/g, "");
|
|
249
|
+
const shownLines = tail ? tail.split("\n").map((l) => clampLineLength(l)) : [];
|
|
250
|
+
|
|
251
|
+
if (shownLines.length === 0) {
|
|
252
|
+
cacheKey = cacheId;
|
|
253
|
+
cacheLines = [];
|
|
254
|
+
return cacheLines;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const truncatedShown = shownLines.map((line) => {
|
|
258
|
+
const truncated = safeTruncateToWidth(line, bodyWidth, "…");
|
|
259
|
+
if (color === "error") return formatToolOutputLine(theme, truncated, "error");
|
|
260
|
+
return cfg.dimOutput
|
|
261
|
+
? formatToolOutputLine(theme, truncated)
|
|
262
|
+
: formatToolOutputLine(theme, truncated, "text");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Count remaining lines (lines before scanFrom)
|
|
266
|
+
const remaining = extraLinesBefore + (scanFrom > 0 ? countNewlines(text, 0, scanFrom) : 0);
|
|
267
|
+
|
|
268
|
+
if (remaining <= 0) {
|
|
269
|
+
cacheKey = cacheId;
|
|
270
|
+
cacheLines = truncatedShown;
|
|
271
|
+
return cacheLines;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const hint = safeTruncateToWidth(`... ${remaining} more lines, press Ctrl+o to expand`, bodyWidth, "…");
|
|
275
|
+
cacheKey = cacheId;
|
|
276
|
+
cacheLines = [...truncatedShown, "", theme.fg("muted", hint)];
|
|
277
|
+
return cacheLines;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Expanded: process all lines
|
|
281
|
+
const normalized = replaceTabs(text);
|
|
282
|
+
const logicalLines = normalized.split("\n").map((l) => clampLineLength(l));
|
|
283
|
+
const hasOutput = !(logicalLines.length === 1 && logicalLines[0] === "");
|
|
284
|
+
|
|
285
|
+
if (!hasOutput) {
|
|
286
|
+
cacheKey = cacheId;
|
|
287
|
+
cacheLines = [];
|
|
288
|
+
return cacheLines;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const truncatedLines = logicalLines.map((line) => safeTruncateToWidth(line, bodyWidth, "…"));
|
|
292
|
+
const expandedLines = truncatedLines.length === 1 && truncatedLines[0] === "" ? [] : truncatedLines;
|
|
293
|
+
const applyColor = (l: string) =>
|
|
294
|
+
color === "error"
|
|
295
|
+
? formatToolOutputLine(theme, l, "error")
|
|
296
|
+
: cfg.dimOutput
|
|
297
|
+
? formatToolOutputLine(theme, l)
|
|
298
|
+
: formatToolOutputLine(theme, l, "text");
|
|
299
|
+
if (cfg.maxExpandedLines > 0 && expandedLines.length > cfg.maxExpandedLines) {
|
|
300
|
+
const truncated = expandedLines.slice(-cfg.maxExpandedLines).map(applyColor);
|
|
301
|
+
const remaining = expandedLines.length - cfg.maxExpandedLines;
|
|
302
|
+
truncated.unshift(theme.fg("dim", `… ${remaining} earlier lines`));
|
|
303
|
+
cacheKey = cacheId;
|
|
304
|
+
cacheLines = truncated;
|
|
305
|
+
return cacheLines;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
cacheKey = cacheId;
|
|
309
|
+
cacheLines = expandedLines.map(applyColor);
|
|
310
|
+
return cacheLines;
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export const bashTool: BoxedToolDefinition = {
|
|
316
|
+
call(args, theme, context) {
|
|
317
|
+
noteExecutionStart(context);
|
|
318
|
+
const rawCommand = String(args?.command ?? "...");
|
|
319
|
+
return renderBoxedBashCall(theme, rawCommand.split("\n"), context, bashWidthKey(rawCommand, args?.timeout));
|
|
320
|
+
},
|
|
321
|
+
result(result, options, theme, context) {
|
|
322
|
+
const raw = getTextOutput(result);
|
|
323
|
+
const outputColor = context.isError ? "error" : "toolOutput";
|
|
324
|
+
|
|
325
|
+
if (!options.expanded) {
|
|
326
|
+
const scanLines = MAX_BASH_PREVIEW_LINES + 10;
|
|
327
|
+
let nlCount = 0;
|
|
328
|
+
let tailStart = 0;
|
|
329
|
+
for (let i = raw.length - 1; i >= 0; i--) {
|
|
330
|
+
if (raw.charCodeAt(i) === 10) {
|
|
331
|
+
nlCount++;
|
|
332
|
+
if (nlCount >= scanLines) {
|
|
333
|
+
tailStart = i + 1;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const tail = stripBashToolNoticeLines(stripAnsi(raw.slice(tailStart)));
|
|
339
|
+
const totalLinesBefore = tailStart > 0 ? countNewlines(raw, 0, tailStart) : 0;
|
|
340
|
+
const inner = createBashResultPreview(theme, tail, options, outputColor, totalLinesBefore);
|
|
341
|
+
return renderBoxedBashResult(theme, inner, result, context);
|
|
342
|
+
}
|
|
343
|
+
const output = stripBashToolNoticeLines(stripAnsi(raw));
|
|
344
|
+
const inner = createBashResultPreview(theme, output, options, outputColor, 0);
|
|
345
|
+
return renderBoxedBashResult(theme, inner, result, context);
|
|
346
|
+
},
|
|
347
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Boxed edit tool renderer
|
|
2
|
+
// (renderCall/renderResult only; no edit-core re-registration).
|
|
3
|
+
|
|
4
|
+
import { getLanguageFromPath } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
6
|
+
import { stripAnsi } from "../../../shared/ansi.js";
|
|
7
|
+
import { getTextOutput, renderBoxedToolCall, renderBoxedToolResult } from "../../../shared/box.js";
|
|
8
|
+
import {
|
|
9
|
+
buildSplitRows,
|
|
10
|
+
countDiffStats,
|
|
11
|
+
extractEditedPath,
|
|
12
|
+
firstText,
|
|
13
|
+
renderDiffMeter,
|
|
14
|
+
SplitDiffComponent,
|
|
15
|
+
} from "../../../shared/split-diff.js";
|
|
16
|
+
import {
|
|
17
|
+
type BoxedToolContext,
|
|
18
|
+
type BoxedToolDefinition,
|
|
19
|
+
displayPath,
|
|
20
|
+
noteExecutionStart,
|
|
21
|
+
resultFooterLines,
|
|
22
|
+
} from "./shared.js";
|
|
23
|
+
|
|
24
|
+
const MAX_HIGHLIGHT_DIFF_CHARS = 12000;
|
|
25
|
+
const MAX_HIGHLIGHT_DIFF_ROWS = 120;
|
|
26
|
+
|
|
27
|
+
type EditResultDetails = { diff?: string; path?: string } | undefined;
|
|
28
|
+
|
|
29
|
+
export const editTool: BoxedToolDefinition = {
|
|
30
|
+
call(args, theme, context) {
|
|
31
|
+
noteExecutionStart(context);
|
|
32
|
+
const detail = displayPath(String(args?.path ?? args?.file_path ?? ""), context);
|
|
33
|
+
return renderBoxedToolCall(theme, "Edit", [`${theme.fg("dim", "Path: ")}${detail}`], {
|
|
34
|
+
isError: Boolean(context.isError),
|
|
35
|
+
isPartial: Boolean(context.isPartial),
|
|
36
|
+
isPending: Boolean(context.isPartial),
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
result(result, options, theme, context) {
|
|
40
|
+
// Handle partial/streaming state
|
|
41
|
+
if (options.isPartial) {
|
|
42
|
+
return renderBoxedToolResult(theme, () => [`${theme.fg("dim", "↳")} ${theme.fg("muted", "Applying edit...")}`], {
|
|
43
|
+
isPartial: true,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Handle errors
|
|
48
|
+
if (context.isError) {
|
|
49
|
+
const output = getTextOutput(result);
|
|
50
|
+
return renderBoxedToolResult(theme, () => [theme.fg("error", stripAnsi(output).trim() || "Error")], {
|
|
51
|
+
footerLines: resultFooterLines(theme, result, context),
|
|
52
|
+
isError: true,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Extract diff from result details
|
|
57
|
+
const details = result.details as EditResultDetails;
|
|
58
|
+
const diff = details?.diff as string | undefined;
|
|
59
|
+
|
|
60
|
+
if (!diff) {
|
|
61
|
+
const output = stripAnsi(getTextOutput(result)).trim();
|
|
62
|
+
const fallback = `↳ ${output || "Edit applied"}`;
|
|
63
|
+
return renderBoxedToolResult(theme, () => [theme.fg("dim", fallback)], {
|
|
64
|
+
footerLines: resultFooterLines(theme, result, context),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Resolve language for syntax highlighting
|
|
69
|
+
const message = firstText(result.content as Array<{ type: string; text?: string }>);
|
|
70
|
+
const argPath = String(context?.args?.path ?? context?.args?.file_path ?? "");
|
|
71
|
+
const sourcePath = details?.path ?? (argPath || extractEditedPath(message));
|
|
72
|
+
const language = sourcePath ? getLanguageFromPath(sourcePath) : undefined;
|
|
73
|
+
|
|
74
|
+
// Build split-diff rows
|
|
75
|
+
const rows = buildSplitRows(diff);
|
|
76
|
+
const expanded = options.expanded;
|
|
77
|
+
const shouldHighlight =
|
|
78
|
+
Boolean(language) && diff.length <= MAX_HIGHLIGHT_DIFF_CHARS && rows.length <= MAX_HIGHLIGHT_DIFF_ROWS;
|
|
79
|
+
|
|
80
|
+
// Build summary header with diff stats and meter
|
|
81
|
+
const { additions, removals } = countDiffStats(diff);
|
|
82
|
+
const meter = renderDiffMeter(theme, additions, removals);
|
|
83
|
+
const summary =
|
|
84
|
+
`${theme.fg("dim", "↳")} ${theme.fg("muted", "diff")}` +
|
|
85
|
+
` ${theme.fg("toolDiffAdded", `+${additions}`)}` +
|
|
86
|
+
` ${theme.fg("toolDiffRemoved", `-${removals}`)}` +
|
|
87
|
+
` ${theme.fg("muted", "split")}` +
|
|
88
|
+
(meter ? ` ${meter}` : "");
|
|
89
|
+
|
|
90
|
+
// Render split-diff with syntax colors for small outputs.
|
|
91
|
+
const maxRows = expanded ? 160 : 36;
|
|
92
|
+
const split = new SplitDiffComponent(theme, rows, maxRows, shouldHighlight ? language : undefined);
|
|
93
|
+
|
|
94
|
+
return renderBoxedToolResult(
|
|
95
|
+
theme,
|
|
96
|
+
{
|
|
97
|
+
render(width: number): string[] {
|
|
98
|
+
const safeWidth = Math.max(20, width);
|
|
99
|
+
const headerLines = new Text(summary, 0, 0).render(safeWidth);
|
|
100
|
+
return [...headerLines, ...split.render(safeWidth)];
|
|
101
|
+
},
|
|
102
|
+
invalidate(): void {
|
|
103
|
+
split.invalidate();
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
footerLines: resultFooterLines(theme, result, context),
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export type { BoxedToolContext };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Boxed fallback for tools without a dedicated renderer.
|
|
2
|
+
// (component side only; no ToolExecutionComponent prototype patching).
|
|
3
|
+
|
|
4
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
5
|
+
import type { BoxTheme, MetricResultLike } from "../../../shared/box.js";
|
|
6
|
+
import {
|
|
7
|
+
formatBoxedFooter,
|
|
8
|
+
formatToolName,
|
|
9
|
+
formatToolParamLines,
|
|
10
|
+
getTextOutput,
|
|
11
|
+
renderBoxedToolCall,
|
|
12
|
+
renderBoxedToolResult,
|
|
13
|
+
renderLines,
|
|
14
|
+
} from "../../../shared/box.js";
|
|
15
|
+
import { getStateElapsedMs, getToolsRenderConfig } from "./session-config.js";
|
|
16
|
+
import { type BoxedToolContext, noteExecutionStart } from "./shared.js";
|
|
17
|
+
|
|
18
|
+
const MAX_FALLBACK_PREVIEW_LINES = 10;
|
|
19
|
+
|
|
20
|
+
export function renderFallbackCall(
|
|
21
|
+
toolName: unknown,
|
|
22
|
+
args: Record<string, unknown>,
|
|
23
|
+
theme: BoxTheme,
|
|
24
|
+
context: BoxedToolContext,
|
|
25
|
+
): Component {
|
|
26
|
+
noteExecutionStart(context);
|
|
27
|
+
return renderBoxedToolCall(theme, formatToolName(String(toolName ?? "Tool")), formatToolParamLines(args, theme), {
|
|
28
|
+
isError: Boolean(context.isError),
|
|
29
|
+
isPartial: Boolean(context.isPartial),
|
|
30
|
+
isPending: Boolean(context.isPartial),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function renderFallbackResult(
|
|
35
|
+
_toolName: unknown,
|
|
36
|
+
result: MetricResultLike,
|
|
37
|
+
options: { expanded: boolean; isPartial: boolean },
|
|
38
|
+
theme: BoxTheme,
|
|
39
|
+
context: BoxedToolContext,
|
|
40
|
+
): Component {
|
|
41
|
+
const isError = Boolean(context.isError);
|
|
42
|
+
const expanded = Boolean(options.expanded);
|
|
43
|
+
const maxLines = expanded ? getToolsRenderConfig().maxExpandedLines : MAX_FALLBACK_PREVIEW_LINES;
|
|
44
|
+
const output = getTextOutput(result);
|
|
45
|
+
const elapsedMs = getStateElapsedMs(context.state);
|
|
46
|
+
|
|
47
|
+
return renderBoxedToolResult(
|
|
48
|
+
theme,
|
|
49
|
+
(contentWidth) => {
|
|
50
|
+
const body = renderLines(theme, output, options, {
|
|
51
|
+
maxLines,
|
|
52
|
+
color: isError ? "error" : "toolOutput",
|
|
53
|
+
width: contentWidth,
|
|
54
|
+
});
|
|
55
|
+
return body ? body.split("\n") : [];
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
footerLines: [formatBoxedFooter(theme, result, [], elapsedMs)],
|
|
59
|
+
renderLineBudget: maxLines,
|
|
60
|
+
isError,
|
|
61
|
+
isPartial: Boolean(options.isPartial),
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Re-exported for callers that need the tool-name label normalization.
|
|
67
|
+
export { formatToolName };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Boxed find tool renderer.
|
|
2
|
+
|
|
3
|
+
import { stripAnsi } from "../../../shared/ansi.js";
|
|
4
|
+
import {
|
|
5
|
+
boxedToolWidthKey,
|
|
6
|
+
countLines,
|
|
7
|
+
getTextOutput,
|
|
8
|
+
renderBoxedToolResult,
|
|
9
|
+
shortenPath,
|
|
10
|
+
stripTrailingNotice,
|
|
11
|
+
} from "../../../shared/box.js";
|
|
12
|
+
import {
|
|
13
|
+
type BoxedToolDefinition,
|
|
14
|
+
clearFooterState,
|
|
15
|
+
compactCall,
|
|
16
|
+
compactFooterWithState,
|
|
17
|
+
noteExecutionStart,
|
|
18
|
+
resultFooterLines,
|
|
19
|
+
truncationOutputLines,
|
|
20
|
+
} from "./shared.js";
|
|
21
|
+
|
|
22
|
+
function queryDetail(pattern: string, rawPath: string): string {
|
|
23
|
+
const displayPath = String(rawPath ?? ".");
|
|
24
|
+
const path = displayPath === "." || displayPath === "" ? "current directory" : shortenPath(displayPath);
|
|
25
|
+
return pattern ? `${pattern} in ${path}` : path;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const findTool: BoxedToolDefinition = {
|
|
29
|
+
call(args, theme, context) {
|
|
30
|
+
noteExecutionStart(context);
|
|
31
|
+
const detail = queryDetail(String(args?.pattern ?? ""), String(args?.path ?? "."));
|
|
32
|
+
return compactCall(theme, "Find", `${theme.fg("dim", "Query: ")}${detail}`, {
|
|
33
|
+
detailKey: detail,
|
|
34
|
+
context,
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
result(result, options, theme, context) {
|
|
38
|
+
clearFooterState(context);
|
|
39
|
+
const output = stripAnsi(getTextOutput(result)).trimEnd();
|
|
40
|
+
const detail = queryDetail(String(context?.args?.pattern ?? ""), String(context?.args?.path ?? "."));
|
|
41
|
+
const widthKey = boxedToolWidthKey("Find", detail);
|
|
42
|
+
|
|
43
|
+
if (context.isError) {
|
|
44
|
+
return renderBoxedToolResult(theme, () => [theme.fg("error", output || "Error")], {
|
|
45
|
+
widthKey,
|
|
46
|
+
footerLines: resultFooterLines(theme, result, context),
|
|
47
|
+
isError: true,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!options.expanded) return compactFooterWithState(theme, result, context);
|
|
52
|
+
|
|
53
|
+
let fileCount = 0;
|
|
54
|
+
if (output && output !== "No files found matching pattern") {
|
|
55
|
+
const stripped = stripTrailingNotice(output);
|
|
56
|
+
fileCount = truncationOutputLines(result) ?? countLines(stripped);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const summary = `↳ Found ${fileCount} ${fileCount === 1 ? "file" : "files"}.`;
|
|
60
|
+
return renderBoxedToolResult(theme, () => [theme.fg("dim", summary)], {
|
|
61
|
+
widthKey,
|
|
62
|
+
footerLines: resultFooterLines(theme, result, context),
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Boxed grep/search tool renderer.
|
|
2
|
+
|
|
3
|
+
import { stripAnsi } from "../../../shared/ansi.js";
|
|
4
|
+
import {
|
|
5
|
+
boxedToolWidthKey,
|
|
6
|
+
countLines,
|
|
7
|
+
getTextOutput,
|
|
8
|
+
renderBoxedToolResult,
|
|
9
|
+
renderLines,
|
|
10
|
+
shortenPath,
|
|
11
|
+
stripTrailingNotice,
|
|
12
|
+
} from "../../../shared/box.js";
|
|
13
|
+
import { getToolsRenderConfig } from "./session-config.js";
|
|
14
|
+
import {
|
|
15
|
+
type BoxedToolDefinition,
|
|
16
|
+
clearFooterState,
|
|
17
|
+
compactCall,
|
|
18
|
+
compactFooterWithState,
|
|
19
|
+
matchLimitReached,
|
|
20
|
+
noteExecutionStart,
|
|
21
|
+
resultFooterLines,
|
|
22
|
+
} from "./shared.js";
|
|
23
|
+
|
|
24
|
+
const MAX_GREP_PREVIEW_LINES = 10;
|
|
25
|
+
|
|
26
|
+
function queryDetail(pattern: string, rawPath: string): string {
|
|
27
|
+
const displayPath = String(rawPath ?? ".");
|
|
28
|
+
const path = displayPath === "." || displayPath === "" ? "current directory" : shortenPath(displayPath);
|
|
29
|
+
return pattern ? `/${pattern}/ in ${path}` : path;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const grepTool: BoxedToolDefinition = {
|
|
33
|
+
call(args, theme, context) {
|
|
34
|
+
noteExecutionStart(context);
|
|
35
|
+
const detail = queryDetail(String(args?.pattern ?? ""), String(args?.path ?? "."));
|
|
36
|
+
return compactCall(theme, "Search", `${theme.fg("dim", "Query: ")}${detail}`, {
|
|
37
|
+
detailKey: detail,
|
|
38
|
+
context,
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
result(result, options, theme, context) {
|
|
42
|
+
clearFooterState(context);
|
|
43
|
+
const output = stripAnsi(getTextOutput(result)).trimEnd();
|
|
44
|
+
const stripped = stripTrailingNotice(output);
|
|
45
|
+
const detail = queryDetail(String(context?.args?.pattern ?? ""), String(context?.args?.path ?? "."));
|
|
46
|
+
const widthKey = boxedToolWidthKey("Search", detail);
|
|
47
|
+
const cfg = getToolsRenderConfig();
|
|
48
|
+
|
|
49
|
+
if (context.isError) {
|
|
50
|
+
return renderBoxedToolResult(
|
|
51
|
+
theme,
|
|
52
|
+
(width) => {
|
|
53
|
+
const body = renderLines(theme, stripped || output || "Error", options, {
|
|
54
|
+
maxLines: MAX_GREP_PREVIEW_LINES,
|
|
55
|
+
color: "error",
|
|
56
|
+
width,
|
|
57
|
+
});
|
|
58
|
+
return body ? body.split("\n") : [];
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
widthKey,
|
|
62
|
+
footerLines: resultFooterLines(theme, result, context),
|
|
63
|
+
isError: true,
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!options.expanded) return compactFooterWithState(theme, result, context);
|
|
69
|
+
|
|
70
|
+
let matchCount = 0;
|
|
71
|
+
if (stripped && stripped !== "No matches found") {
|
|
72
|
+
const lines = stripped.split("\n");
|
|
73
|
+
matchCount = lines.filter((line) => /:\d+:/.test(line)).length;
|
|
74
|
+
|
|
75
|
+
if (matchCount === 0) {
|
|
76
|
+
matchCount = countLines(stripped);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (matchLimitReached(result) !== undefined) {
|
|
80
|
+
matchCount = Math.max(matchCount, matchLimitReached(result) as number);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const summary = theme.fg("dim", `↳ Found ${matchCount} ${matchCount === 1 ? "match" : "matches"}.`);
|
|
85
|
+
if (!stripped || stripped === "No matches found") {
|
|
86
|
+
return renderBoxedToolResult(theme, () => [summary], {
|
|
87
|
+
widthKey,
|
|
88
|
+
footerLines: resultFooterLines(theme, result, context),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return renderBoxedToolResult(
|
|
93
|
+
theme,
|
|
94
|
+
(width) => {
|
|
95
|
+
const body = renderLines(theme, stripped, options, {
|
|
96
|
+
maxLines: options.expanded ? cfg.maxExpandedLines : MAX_GREP_PREVIEW_LINES,
|
|
97
|
+
color: "toolOutput",
|
|
98
|
+
width,
|
|
99
|
+
});
|
|
100
|
+
return [summary, ...(body ? body.split("\n") : [])];
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
widthKey,
|
|
104
|
+
footerLines: resultFooterLines(theme, result, context),
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
};
|