@heyhuynhgiabuu/pi-pretty 0.6.30 → 0.7.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 +33 -0
- package/README.md +21 -0
- package/dist/claude-style.d.ts +45 -0
- package/dist/claude-style.d.ts.map +1 -0
- package/dist/claude-style.js +582 -0
- package/dist/claude-style.js.map +1 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -12
- package/dist/index.js.map +1 -1
- package/dist/prompt-editor.d.ts +4 -1
- package/dist/prompt-editor.d.ts.map +1 -1
- package/dist/prompt-editor.js +27 -12
- package/dist/prompt-editor.js.map +1 -1
- package/dist/render.d.ts +10 -7
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +41 -25
- package/dist/render.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +17 -13
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +15 -17
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +10 -8
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/ls.d.ts.map +1 -1
- package/dist/tools/ls.js +11 -9
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +25 -21
- package/dist/tools/read.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/working-indicator.d.ts +2 -0
- package/dist/working-indicator.d.ts.map +1 -1
- package/dist/working-indicator.js +20 -8
- package/dist/working-indicator.js.map +1 -1
- package/package.json +5 -5
- package/src/claude-style.ts +650 -0
- package/src/config.ts +3 -0
- package/src/index.ts +35 -15
- package/src/prompt-editor.ts +26 -11
- package/src/render.ts +54 -24
- package/src/tools/bash.ts +28 -23
- package/src/tools/find.ts +20 -16
- package/src/tools/grep.ts +10 -6
- package/src/tools/ls.ts +17 -8
- package/src/tools/read.ts +29 -30
- package/src/types.ts +9 -0
- package/src/working-indicator.ts +20 -8
package/src/index.ts
CHANGED
|
@@ -21,6 +21,12 @@ import type {
|
|
|
21
21
|
} from "@earendil-works/pi-coding-agent";
|
|
22
22
|
import * as hostSdk from "@earendil-works/pi-coding-agent";
|
|
23
23
|
import { createFffAutocompleteProvider } from "./autocomplete.js";
|
|
24
|
+
import {
|
|
25
|
+
installClaudeChatGrouping,
|
|
26
|
+
installClaudeExternalToolRenderers,
|
|
27
|
+
setClaudeStyleTheme,
|
|
28
|
+
withClaudeToolStyleApi,
|
|
29
|
+
} from "./claude-style.js";
|
|
24
30
|
import {
|
|
25
31
|
applyConfig,
|
|
26
32
|
getDefaultAgentDir,
|
|
@@ -45,6 +51,7 @@ import {
|
|
|
45
51
|
type PerRowThinkingLabels,
|
|
46
52
|
resolveThinkingIndicatorSettings,
|
|
47
53
|
resolveWorkingIndicatorSettings,
|
|
54
|
+
setClaudeThinkingLabels,
|
|
48
55
|
type ThinkingTimer,
|
|
49
56
|
thinkingBlockActive,
|
|
50
57
|
WORKING_INTERVAL_MS,
|
|
@@ -76,10 +83,15 @@ export type { PiPrettyDeps };
|
|
|
76
83
|
export default async function piPrettyExtension(pi: ExtensionAPI, deps?: PiPrettyDeps): Promise<void> {
|
|
77
84
|
const config = loadConfig();
|
|
78
85
|
applyConfig(config);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
const claudeStyle = config.toolStyle === "claudecode";
|
|
87
|
+
setClaudeThinkingLabels(claudeStyle);
|
|
88
|
+
// claudecode style keeps user messages plain; the default style prefixes them with a prompt icon.
|
|
89
|
+
if (!claudeStyle) {
|
|
90
|
+
pi.registerMarkdownTransformer?.((markdown, context) => {
|
|
91
|
+
if (context.messageType !== "user" || markdown.trim() === "") return markdown;
|
|
92
|
+
return ` ${USER_MESSAGE_ICON} ${markdown}`;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
83
95
|
pi.registerFlag?.("pretty-fff-home-scan", {
|
|
84
96
|
description: "Allow FFF to index the home directory when Pi starts there",
|
|
85
97
|
type: "boolean",
|
|
@@ -214,12 +226,16 @@ export default async function piPrettyExtension(pi: ExtensionAPI, deps?: PiPrett
|
|
|
214
226
|
const editorUi = ctx.ui as unknown as EditorUiCompatibility;
|
|
215
227
|
if (typeof editorUi.setEditorComponent !== "function") return;
|
|
216
228
|
previousEditorFactory = editorUi.getEditorComponent?.();
|
|
217
|
-
const PromptEditor = createPromptEditorClass(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
229
|
+
const PromptEditor = createPromptEditorClass(
|
|
230
|
+
hostCustomEditor,
|
|
231
|
+
(icon) => {
|
|
232
|
+
const theme = ctx.ui.theme;
|
|
233
|
+
return typeof theme.getThinkingBorderColor === "function"
|
|
234
|
+
? theme.getThinkingBorderColor(ctx.thinkingLevel ?? "off")(icon)
|
|
235
|
+
: theme.fg("thinkingText", icon);
|
|
236
|
+
},
|
|
237
|
+
!claudeStyle,
|
|
238
|
+
);
|
|
223
239
|
editorUi.setEditorComponent((tui, theme, keybindings) => new PromptEditor(tui, theme, keybindings));
|
|
224
240
|
promptEditorInstalled = true;
|
|
225
241
|
};
|
|
@@ -294,6 +310,9 @@ export default async function piPrettyExtension(pi: ExtensionAPI, deps?: PiPrett
|
|
|
294
310
|
|
|
295
311
|
pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
|
|
296
312
|
if (ctx.mode === "tui") {
|
|
313
|
+
setClaudeStyleTheme(ctx.ui.theme);
|
|
314
|
+
installClaudeChatGrouping(claudeStyle);
|
|
315
|
+
installClaudeExternalToolRenderers(hostSdk.ToolExecutionComponent, claudeStyle);
|
|
297
316
|
installPromptEditor(ctx);
|
|
298
317
|
ctx.ui.setToolsExpanded(false);
|
|
299
318
|
// Per-row hidden-thinking labels: intercept the host's label fan-out so
|
|
@@ -536,19 +555,20 @@ export default async function piPrettyExtension(pi: ExtensionAPI, deps?: PiPrett
|
|
|
536
555
|
// Tool registration
|
|
537
556
|
// ------------------------------------------------------------------
|
|
538
557
|
|
|
558
|
+
const toolPi = claudeStyle ? withClaudeToolStyleApi(pi) : pi;
|
|
539
559
|
if (isToolEnabled("read") && createReadTool) {
|
|
540
|
-
registerReadTool(
|
|
560
|
+
registerReadTool(toolPi, cwd, null, createReadTool(cwd), TextComp);
|
|
541
561
|
}
|
|
542
562
|
if (isToolEnabled("bash") && createBashTool) {
|
|
543
|
-
registerBashTool(
|
|
563
|
+
registerBashTool(toolPi, cwd, null, createBashTool(cwd), TextComp);
|
|
544
564
|
}
|
|
545
565
|
if (isToolEnabled("ls") && createLsTool) {
|
|
546
|
-
registerLsTool(
|
|
566
|
+
registerLsTool(toolPi, cwd, null, createLsTool(cwd), TextComp);
|
|
547
567
|
}
|
|
548
568
|
if (isToolEnabled("find") && createFindTool) {
|
|
549
|
-
registerFindTool(
|
|
569
|
+
registerFindTool(toolPi, cwd, fffService, createFindTool(cwd), TextComp);
|
|
550
570
|
}
|
|
551
571
|
if (isToolEnabled("grep") && createGrepTool) {
|
|
552
|
-
registerGrepTool(
|
|
572
|
+
registerGrepTool(toolPi, cwd, fffService, createGrepTool(cwd), TextComp);
|
|
553
573
|
}
|
|
554
574
|
}
|
package/src/prompt-editor.ts
CHANGED
|
@@ -7,15 +7,20 @@ export type CustomEditorConstructor = new (...args: ConstructorParameters<typeof
|
|
|
7
7
|
type PromptColor = (text: string) => string;
|
|
8
8
|
|
|
9
9
|
const PROMPT_ICON = "❯";
|
|
10
|
-
const
|
|
10
|
+
const BOXED_PROMPT_WIDTH = 3; // one space on either side of the icon
|
|
11
|
+
const UNBOXED_PROMPT_WIDTH = 2; // icon flush left, one space after
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Build a CustomEditor subclass that reserves room for, and renders, the
|
|
14
15
|
* user-message prompt. The host still owns all editing and app keybindings.
|
|
16
|
+
* With `boxed: false` the host's plain horizontal rules are kept as-is (no rounded
|
|
17
|
+
* corners, no side bars) and the prompt sits flush left: `❯ text`, ignoring the
|
|
18
|
+
* host's horizontal padding.
|
|
15
19
|
*/
|
|
16
20
|
export function createPromptEditorClass(
|
|
17
21
|
Base: CustomEditorConstructor,
|
|
18
22
|
colorPrompt: PromptColor,
|
|
23
|
+
boxed = true,
|
|
19
24
|
): CustomEditorConstructor {
|
|
20
25
|
const basePrototype = Base.prototype as unknown as {
|
|
21
26
|
renderTopBorder?: unknown;
|
|
@@ -33,13 +38,19 @@ export function createPromptEditorClass(
|
|
|
33
38
|
const [tui, theme, keybindings, options] = args;
|
|
34
39
|
super(tui, theme, keybindings, { ...(options ?? {}), embedWorkingStatus: true });
|
|
35
40
|
this.userPaddingX = super.getPaddingX();
|
|
36
|
-
super.setPaddingX(this.
|
|
41
|
+
super.setPaddingX(this.reservedPadding());
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
private promptWidth(): number {
|
|
45
|
+
if (!boxed) return UNBOXED_PROMPT_WIDTH;
|
|
40
46
|
// At zero user padding, reserve one extra column so the border leaves
|
|
41
47
|
// the same visible gap before the icon as padded editors.
|
|
42
|
-
return
|
|
48
|
+
return BOXED_PROMPT_WIDTH + (supportsBorderHooks && this.userPaddingX === 0 ? 1 : 0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Columns the host must reserve left of the input: user padding plus the prompt slot. */
|
|
52
|
+
private reservedPadding(): number {
|
|
53
|
+
return (boxed ? this.userPaddingX : 0) + this.promptWidth();
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
override getPaddingX(): number {
|
|
@@ -48,16 +59,19 @@ export function createPromptEditorClass(
|
|
|
48
59
|
|
|
49
60
|
override setPaddingX(padding: number): void {
|
|
50
61
|
this.userPaddingX = Math.max(0, padding);
|
|
51
|
-
super.setPaddingX(this.
|
|
62
|
+
super.setPaddingX(this.reservedPadding());
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
protected override renderTopBorder(width: number, hiddenLineCount: number): string {
|
|
55
66
|
this.isScrolled = hiddenLineCount > 0;
|
|
56
|
-
|
|
67
|
+
const border = super.renderTopBorder(width, hiddenLineCount);
|
|
68
|
+
return boxed ? this.roundBorder(border, width, "╭", "╮") : border;
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
protected override renderBottomBorder(width: number, hiddenLineCount: number): string {
|
|
60
|
-
|
|
72
|
+
const border = super.renderBottomBorder(width, hiddenLineCount);
|
|
73
|
+
if (!boxed) return border;
|
|
74
|
+
this.bottomBorderLine = this.roundBorder(border, width, "╰", "╯");
|
|
61
75
|
return this.bottomBorderLine;
|
|
62
76
|
}
|
|
63
77
|
|
|
@@ -75,16 +89,17 @@ export function createPromptEditorClass(
|
|
|
75
89
|
const firstContentLine = lines[1];
|
|
76
90
|
if (firstContentLine) {
|
|
77
91
|
const maxPadding = Math.max(0, Math.floor((width - 1) / 2));
|
|
78
|
-
const promptPadding = Math.min(this.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
92
|
+
const promptPadding = Math.min(this.reservedPadding(), maxPadding);
|
|
93
|
+
const slot = boxed ? BOXED_PROMPT_WIDTH : UNBOXED_PROMPT_WIDTH;
|
|
94
|
+
if (promptPadding >= slot && firstContentLine.startsWith(" ".repeat(promptPadding))) {
|
|
95
|
+
const promptStart = promptPadding - slot;
|
|
96
|
+
const prompt = boxed ? ` ${colorPrompt(PROMPT_ICON)} ` : `${colorPrompt(PROMPT_ICON)} `;
|
|
82
97
|
lines[1] = `${firstContentLine.slice(0, promptStart)}${prompt}${firstContentLine.slice(promptPadding)}`;
|
|
83
98
|
}
|
|
84
99
|
}
|
|
85
100
|
}
|
|
86
101
|
|
|
87
|
-
if (!supportsBorderHooks || !this.bottomBorderLine) return lines;
|
|
102
|
+
if (!boxed || !supportsBorderHooks || !this.bottomBorderLine) return lines;
|
|
88
103
|
|
|
89
104
|
const bottomBorderIndex = lines.lastIndexOf(this.bottomBorderLine);
|
|
90
105
|
const contentEnd = bottomBorderIndex > 0 ? bottomBorderIndex : lines.length - 1;
|
package/src/render.ts
CHANGED
|
@@ -200,7 +200,16 @@ export function preserveBoxBackground(ansi: string): string {
|
|
|
200
200
|
});
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
/** Left indent for self-rendered rows: host `ctx.outputPad` spaces, falling back to 1 on older/invalid hosts. */
|
|
204
|
+
export function toolIndent(ctx?: Pick<RenderContext, "outputPad">): string {
|
|
205
|
+
const pad = ctx?.outputPad;
|
|
206
|
+
// Reject unusable values, including pads wider than the terminal (avoids giant allocations).
|
|
207
|
+
return typeof pad === "number" && Number.isInteger(pad) && pad >= 0 && pad <= termWidth()
|
|
208
|
+
? " ".repeat(pad)
|
|
209
|
+
: TOOL_RESULT_INDENT;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function fillToolBackground(text: string, bg = BG_BASE, width?: number, indent = TOOL_RESULT_INDENT): string {
|
|
204
213
|
return text
|
|
205
214
|
.split("\n")
|
|
206
215
|
.map((line) => {
|
|
@@ -209,7 +218,8 @@ export function fillToolBackground(text: string, bg = BG_BASE, width?: number):
|
|
|
209
218
|
return bg ? bg + stripped : stripped;
|
|
210
219
|
}
|
|
211
220
|
const plainLead = line.replace(ANSI_CAPTURE_RE, "");
|
|
212
|
-
|
|
221
|
+
// Indented rows keep their natural width; with no indent every row is padded.
|
|
222
|
+
const skipPad = indent !== "" && (line.startsWith(indent) || plainLead.startsWith(indent));
|
|
213
223
|
const fitted = truncateToWidth(line, width, "", !skipPad);
|
|
214
224
|
const stripped = preserveBoxBackground(fitted);
|
|
215
225
|
return bg ? bg + stripped : stripped;
|
|
@@ -218,8 +228,8 @@ export function fillToolBackground(text: string, bg = BG_BASE, width?: number):
|
|
|
218
228
|
}
|
|
219
229
|
|
|
220
230
|
/** Add exactly the missing terminal row after a rendered tool body. */
|
|
221
|
-
export function fillToolBody(text: string, bg = BG_BASE, width?: number): string {
|
|
222
|
-
return fillToolBackground(text.endsWith("\n") ? text : `${text}\n`, bg, width);
|
|
231
|
+
export function fillToolBody(text: string, bg = BG_BASE, width?: number, indent = TOOL_RESULT_INDENT): string {
|
|
232
|
+
return fillToolBackground(text.endsWith("\n") ? text : `${text}\n`, bg, width, indent);
|
|
223
233
|
}
|
|
224
234
|
|
|
225
235
|
const TOOL_TITLE_STATE_KEY = "__piPrettyToolTitle";
|
|
@@ -250,29 +260,44 @@ function lnum(n: number, w: number): string {
|
|
|
250
260
|
// Tool metrics line
|
|
251
261
|
// ---------------------------------------------------------------------------
|
|
252
262
|
|
|
253
|
-
|
|
263
|
+
/** Prefer the host's final duration (0 is valid); fall back to wrapper-recorded metadata. */
|
|
264
|
+
function resolveDurationMs(
|
|
265
|
+
result: AgentToolResult<Record<string, unknown>>,
|
|
266
|
+
ctx?: Pick<RenderContext, "durationMs">,
|
|
267
|
+
): number | undefined {
|
|
268
|
+
const host = ctx?.durationMs;
|
|
269
|
+
if (typeof host === "number" && Number.isFinite(host) && host >= 0) return host;
|
|
270
|
+
const details = result.details as Record<string, unknown> | undefined;
|
|
271
|
+
return details?.[ELAPSED_KEY] as number | undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function renderToolMetrics(
|
|
275
|
+
result: AgentToolResult<Record<string, unknown>>,
|
|
276
|
+
ctx?: Pick<RenderContext, "durationMs">,
|
|
277
|
+
): string {
|
|
254
278
|
const details = result.details as Record<string, unknown> | undefined;
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
const chars = formatCharCount(details[CHARS_KEY] as number | undefined);
|
|
279
|
+
const elapsed = formatElapsedMs(resolveDurationMs(result, ctx));
|
|
280
|
+
const chars = formatCharCount(details?.[CHARS_KEY] as number | undefined);
|
|
258
281
|
if (!elapsed && !chars) return "";
|
|
259
282
|
return `${FG_DIM}· ${[elapsed, chars].filter(Boolean).join(" · ")}${RST}`;
|
|
260
283
|
}
|
|
261
284
|
|
|
262
|
-
export function renderToolDuration(
|
|
263
|
-
|
|
264
|
-
|
|
285
|
+
export function renderToolDuration(
|
|
286
|
+
result: AgentToolResult<Record<string, unknown>>,
|
|
287
|
+
ctx?: Pick<RenderContext, "durationMs">,
|
|
288
|
+
): string {
|
|
289
|
+
return formatElapsedMs(resolveDurationMs(result, ctx));
|
|
265
290
|
}
|
|
266
291
|
|
|
267
292
|
// ---------------------------------------------------------------------------
|
|
268
293
|
// Error renderer
|
|
269
294
|
// ---------------------------------------------------------------------------
|
|
270
295
|
|
|
271
|
-
export function renderToolError(error: string, theme: ThemeLike): string {
|
|
296
|
+
export function renderToolError(error: string, theme: ThemeLike, indent = TOOL_RESULT_INDENT): string {
|
|
272
297
|
const body = compactErrorLines(error)
|
|
273
|
-
.map((line) => `${
|
|
298
|
+
.map((line) => `${indent}${line ? theme.fg("error", line) : ""}`)
|
|
274
299
|
.join("\n");
|
|
275
|
-
return fillToolBody(body, BG_ERROR);
|
|
300
|
+
return fillToolBody(body, BG_ERROR, undefined, indent);
|
|
276
301
|
}
|
|
277
302
|
|
|
278
303
|
// ---------------------------------------------------------------------------
|
|
@@ -305,14 +330,14 @@ export async function renderFileContent(
|
|
|
305
330
|
// Bash — colored exit status
|
|
306
331
|
// ---------------------------------------------------------------------------
|
|
307
332
|
|
|
308
|
-
export function renderBashOutput(text: string): string {
|
|
333
|
+
export function renderBashOutput(text: string, indent = TOOL_RESULT_INDENT): string {
|
|
309
334
|
const lines = text.split("\n");
|
|
310
335
|
const maxShow = MAX_PREVIEW_LINES;
|
|
311
336
|
const show = lines.slice(0, maxShow);
|
|
312
337
|
const remaining = lines.length - maxShow;
|
|
313
338
|
|
|
314
339
|
let body = show.join("\n");
|
|
315
|
-
if (remaining > 0) body += `\n${
|
|
340
|
+
if (remaining > 0) body += `\n${indent}${FG_DIM}… ${remaining} more lines${RST}`;
|
|
316
341
|
|
|
317
342
|
return body;
|
|
318
343
|
}
|
|
@@ -351,6 +376,7 @@ export function renderTree(text: string, _basePath: string): string {
|
|
|
351
376
|
// Find — grouped file list (plain, no tree characters or icons)
|
|
352
377
|
// ---------------------------------------------------------------------------
|
|
353
378
|
|
|
379
|
+
/** File rows keep the structural one-space indent; the tool applies host outer padding. */
|
|
354
380
|
export function renderFindResults(text: string, theme?: ThemeLike): string {
|
|
355
381
|
const lines = text.trim().split("\n").filter(Boolean);
|
|
356
382
|
if (!lines.length) return theme ? theme.fg("dim", "(no matches)") : `${FG_DIM}(no matches)${RST}`;
|
|
@@ -392,7 +418,7 @@ export function renderFindResults(text: string, theme?: ThemeLike): string {
|
|
|
392
418
|
// Grep — highlighted matches with line numbers
|
|
393
419
|
// ---------------------------------------------------------------------------
|
|
394
420
|
|
|
395
|
-
export async function renderGrepResults(text: string, pattern: string): Promise<string> {
|
|
421
|
+
export async function renderGrepResults(text: string, pattern: string, indent = TOOL_RESULT_INDENT): Promise<string> {
|
|
396
422
|
const lines = normalizeLineEndings(text).split("\n");
|
|
397
423
|
if (!lines.length || (lines.length === 1 && !lines[0].trim())) return `${FG_DIM}(no matches)${RST}`;
|
|
398
424
|
|
|
@@ -423,10 +449,10 @@ export async function renderGrepResults(text: string, pattern: string): Promise<
|
|
|
423
449
|
const nw = Math.max(3, lineNo.length);
|
|
424
450
|
let display = content;
|
|
425
451
|
if (re) display = content.replace(re, `${RST}${FG_YELLOW}\x1b[1m$1${RST}`);
|
|
426
|
-
out.push(`${
|
|
452
|
+
out.push(`${indent}${lnum(Number(lineNo), nw)} ${FG_RULE}│${RST} ${display}${RST}`);
|
|
427
453
|
count++;
|
|
428
454
|
} else if (line.trim() === "--") {
|
|
429
|
-
out.push(`${
|
|
455
|
+
out.push(`${indent}${FG_DIM} ···${RST}`);
|
|
430
456
|
} else if (line.trim()) {
|
|
431
457
|
out.push(line);
|
|
432
458
|
count++;
|
|
@@ -445,7 +471,7 @@ export function makeRenderCall(toolName: string) {
|
|
|
445
471
|
resolveBaseBackground(theme);
|
|
446
472
|
const text = ctx.lastComponent ?? new TuiText("", 0, 0);
|
|
447
473
|
const bg = ctx.isError ? BG_ERROR : undefined;
|
|
448
|
-
text.setText(fillToolBackground(`${theme.fg("toolTitle", theme.bold(toolName))}`, bg));
|
|
474
|
+
text.setText(fillToolBackground(`${theme.fg("toolTitle", theme.bold(toolName))}`, bg, undefined, toolIndent(ctx)));
|
|
449
475
|
return text;
|
|
450
476
|
};
|
|
451
477
|
}
|
|
@@ -455,26 +481,30 @@ export function makeRenderResult() {
|
|
|
455
481
|
resolveBaseBackground(theme);
|
|
456
482
|
const text = ctx.lastComponent ?? new TuiText("", 0, 0);
|
|
457
483
|
if (ctx.isError) {
|
|
458
|
-
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
484
|
+
text.setText(renderToolError(getTextContent(result) || "Error", theme, toolIndent(ctx)));
|
|
459
485
|
return text;
|
|
460
486
|
}
|
|
461
487
|
const content = getTextContent(result);
|
|
462
488
|
if (content) {
|
|
463
489
|
const renderWidth = termWidth();
|
|
490
|
+
const indent = toolIndent(ctx);
|
|
464
491
|
const lines = content.split("\n");
|
|
465
492
|
const maxShow = ctx.expanded ? lines.length : Math.min(lines.length, MAX_PREVIEW_LINES);
|
|
466
493
|
const preview = lines.slice(0, maxShow).join("\n");
|
|
467
494
|
const more = lines.length > maxShow ? `\n${FG_DIM}... ${lines.length - maxShow} more lines${RST}` : "";
|
|
468
|
-
const metrics = renderToolMetrics(result);
|
|
495
|
+
const metrics = renderToolMetrics(result, ctx);
|
|
469
496
|
text.setText(
|
|
470
497
|
fillToolBody(
|
|
471
|
-
`${
|
|
498
|
+
`${indent}${preview}${more}${metrics ? `\n${indent}${metrics}` : ""}`,
|
|
472
499
|
undefined,
|
|
473
500
|
renderWidth,
|
|
501
|
+
indent,
|
|
474
502
|
),
|
|
475
503
|
);
|
|
476
504
|
} else {
|
|
477
|
-
text.setText(
|
|
505
|
+
text.setText(
|
|
506
|
+
fillToolBody(`${toolIndent(ctx)}${theme.fg("dim", "(no text output)")}`, undefined, undefined, toolIndent(ctx)),
|
|
507
|
+
);
|
|
478
508
|
}
|
|
479
509
|
return text;
|
|
480
510
|
};
|
package/src/tools/bash.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* pi-pretty: bash tool -- command execution with styled output. */
|
|
2
2
|
|
|
3
3
|
import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { resolveBaseBackground,
|
|
4
|
+
import { resolveBaseBackground, termWidth } from "../config.js";
|
|
5
5
|
import {
|
|
6
6
|
CHARS_KEY,
|
|
7
7
|
compactErrorLines,
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
renderToolDuration,
|
|
18
18
|
renderToolError,
|
|
19
19
|
setCollapsedToolTitle,
|
|
20
|
+
toolIndent,
|
|
20
21
|
} from "../render.js";
|
|
21
22
|
import { resolveTextCtor } from "../tui-text.js";
|
|
22
23
|
import type { BashDetails, ComponentLike, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
|
|
@@ -106,6 +107,7 @@ export function registerBashTool(
|
|
|
106
107
|
resolveBaseBackground(theme);
|
|
107
108
|
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
108
109
|
const t = typeof args.timeout === "number" ? ` ${theme.fg("muted", `(timeout ${args.timeout}s)`)}` : "";
|
|
110
|
+
const ind = toolIndent(ctx);
|
|
109
111
|
const tw = termWidth() || 80;
|
|
110
112
|
const rawCmd = String(args.command ?? "");
|
|
111
113
|
const headerBudget = ctx.expanded ? tw : Math.max(8, tw - 20);
|
|
@@ -117,11 +119,7 @@ export function registerBashTool(
|
|
|
117
119
|
: rawCmd;
|
|
118
120
|
const commandLabel = theme.fg(ctx.isError ? "error" : "toolTitle", theme.bold(`$ ${cmd}`));
|
|
119
121
|
const renderTitle = (suffix = ""): string =>
|
|
120
|
-
fillToolBackground(
|
|
121
|
-
`\n${TOOL_RESULT_INDENT}${commandLabel}${t}${suffix}\n`,
|
|
122
|
-
undefined,
|
|
123
|
-
ctx.expanded ? undefined : tw,
|
|
124
|
-
);
|
|
122
|
+
fillToolBackground(`\n${ind}${commandLabel}${t}${suffix}\n`, undefined, ctx.expanded ? undefined : tw, ind);
|
|
125
123
|
rememberToolTitle(ctx, text, renderTitle);
|
|
126
124
|
text.setText(renderTitle());
|
|
127
125
|
return text;
|
|
@@ -154,26 +152,29 @@ export function registerBashTool(
|
|
|
154
152
|
const cleaned = stripBashExitStatusLine(d.text);
|
|
155
153
|
const output = isErr ? compactErrorLines(cleaned).join("\n") : cleaned;
|
|
156
154
|
const lineCount = output.split("\n").length;
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
155
|
+
const buildInfo = (): string =>
|
|
156
|
+
[
|
|
157
|
+
`${lineCount} lines`,
|
|
158
|
+
renderToolDuration(displayResult, ctx),
|
|
159
|
+
rejectedMetrics ? formatCharCount(rejectedMetrics.chars) : "",
|
|
160
|
+
!ctx.expanded ? "ctrl+o to expand" : "",
|
|
161
|
+
]
|
|
162
|
+
.filter(Boolean)
|
|
163
|
+
.map((part) => theme.fg("dim", part))
|
|
164
|
+
.join(theme.fg("dim", " · "));
|
|
165
|
+
const info = buildInfo();
|
|
167
166
|
const rw = termWidth();
|
|
168
167
|
|
|
169
168
|
if (setCollapsedToolTitle(ctx, text, ` ${info}`)) return text;
|
|
170
169
|
|
|
171
170
|
const renderFn = (w: number) => {
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
const ind = toolIndent(ctx);
|
|
172
|
+
const header = `${ind}${buildInfo()}`;
|
|
173
|
+
if (!ctx.expanded) return fillToolBody(header, undefined, w, ind);
|
|
174
|
+
if (!output.trim()) return fillToolBody(header, undefined, w, ind);
|
|
174
175
|
const show = output.split("\n");
|
|
175
|
-
const out = [header, "", ...show.map((line: string) => `${
|
|
176
|
-
return fillToolBody(out.join("\n"), undefined, w);
|
|
176
|
+
const out = [header, "", ...show.map((line: string) => `${ind}${line}`)];
|
|
177
|
+
return fillToolBody(out.join("\n"), undefined, w, ind);
|
|
177
178
|
};
|
|
178
179
|
|
|
179
180
|
text.setText(renderFn(rw));
|
|
@@ -184,7 +185,7 @@ export function registerBashTool(
|
|
|
184
185
|
let key: string | undefined;
|
|
185
186
|
(text as unknown as Record<string, unknown>).render = (w: number) => {
|
|
186
187
|
const width = Math.max(1, Math.floor(w || termWidth()));
|
|
187
|
-
const k = `bash:${ctx.expanded ? "1" : "0"}:${width}:${d.exitCode ?? "killed"}:${output.length}:${renderToolDuration(displayResult)}`;
|
|
188
|
+
const k = `bash:${ctx.expanded ? "1" : "0"}:${width}:${d.exitCode ?? "killed"}:${output.length}:${renderToolDuration(displayResult, ctx)}:${toolIndent(ctx).length}`;
|
|
188
189
|
if (key !== k) {
|
|
189
190
|
text.setText(renderFn(width));
|
|
190
191
|
key = k;
|
|
@@ -196,13 +197,17 @@ export function registerBashTool(
|
|
|
196
197
|
}
|
|
197
198
|
|
|
198
199
|
if (ctx.isError) {
|
|
199
|
-
text.setText(renderToolError(tc || "Error", theme));
|
|
200
|
+
text.setText(renderToolError(tc || "Error", theme, toolIndent(ctx)));
|
|
200
201
|
return text;
|
|
201
202
|
}
|
|
202
203
|
const fc = displayResult.content?.[0];
|
|
204
|
+
const ind = toolIndent(ctx);
|
|
203
205
|
text.setText(
|
|
204
206
|
fillToolBody(
|
|
205
|
-
`${
|
|
207
|
+
`${ind}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
|
|
208
|
+
undefined,
|
|
209
|
+
undefined,
|
|
210
|
+
ind,
|
|
206
211
|
),
|
|
207
212
|
);
|
|
208
213
|
return text;
|
package/src/tools/find.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { isAbsolute, relative } from "node:path";
|
|
4
4
|
import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { BG_ERROR, FG_DIM, RST, resolveBaseBackground
|
|
5
|
+
import { BG_ERROR, FG_DIM, RST, resolveBaseBackground } from "../config.js";
|
|
6
6
|
import { isLikelyGlobPattern, normalizeFindGlobPattern } from "../find-glob.js";
|
|
7
7
|
import { shortPath } from "../helpers.js";
|
|
8
8
|
import { NOTICE_PARTIAL_FILE_INDEX } from "../notices.js";
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
renderToolDuration,
|
|
15
15
|
renderToolError,
|
|
16
16
|
setCollapsedToolTitle,
|
|
17
|
+
toolIndent,
|
|
17
18
|
} from "../render.js";
|
|
18
19
|
import { resolveTextCtor } from "../tui-text.js";
|
|
19
20
|
import type { FffServiceWithCursor, FindDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
|
|
@@ -171,8 +172,9 @@ export function registerFindTool(
|
|
|
171
172
|
const pathPart = theme.fg("toolOutput", pathArg);
|
|
172
173
|
const limitPart = limit !== undefined && limit !== null ? theme.fg("dim", ` limit ${limit}`) : "";
|
|
173
174
|
const out = `${findLabel} ${patternPart}${inPart}${pathPart}${limitPart}`;
|
|
175
|
+
const ind = toolIndent(ctx);
|
|
174
176
|
const renderTitle = (suffix = ""): string =>
|
|
175
|
-
fillToolBackground(`\n${
|
|
177
|
+
fillToolBackground(`\n${ind}${out}${suffix}\n`, ctx.isError ? BG_ERROR : undefined, undefined, ind);
|
|
176
178
|
rememberToolTitle(ctx, text, renderTitle);
|
|
177
179
|
text.setText(renderTitle());
|
|
178
180
|
return text;
|
|
@@ -182,8 +184,9 @@ export function registerFindTool(
|
|
|
182
184
|
resolveBaseBackground(theme);
|
|
183
185
|
const r = result;
|
|
184
186
|
const text = (ctx as RenderCtxLike).lastComponent ?? new TC("", 0, 0);
|
|
187
|
+
const ind = toolIndent(ctx);
|
|
185
188
|
if (ctx.isError) {
|
|
186
|
-
text.setText(renderToolError(getText(r) || "Error", theme));
|
|
189
|
+
text.setText(renderToolError(getText(r) || "Error", theme, ind));
|
|
187
190
|
return text;
|
|
188
191
|
}
|
|
189
192
|
const d = r.details as FindDetails | undefined;
|
|
@@ -191,36 +194,37 @@ export function registerFindTool(
|
|
|
191
194
|
if (!d.text.trim()) {
|
|
192
195
|
const notice = d.notices?.length ? ` ${theme.fg("warning", `[${d.notices.join(". ")}]`)}` : "";
|
|
193
196
|
if (setCollapsedToolTitle(ctx, text, ` ${FG_DIM}0 files${RST}${notice}`)) return text;
|
|
194
|
-
const noticeStr = d.notices?.length
|
|
195
|
-
|
|
196
|
-
: "";
|
|
197
|
-
text.setText(fillToolBody(`${TOOL_RESULT_INDENT}${theme.fg("dim", "0 files")}${noticeStr}`));
|
|
197
|
+
const noticeStr = d.notices?.length ? `\n${ind}${theme.fg("warning", `[${d.notices.join(". ")}]`)}` : "";
|
|
198
|
+
text.setText(fillToolBody(`${ind}${theme.fg("dim", "0 files")}${noticeStr}`, undefined, undefined, ind));
|
|
198
199
|
return text;
|
|
199
200
|
}
|
|
200
201
|
if (!ctx.expanded) {
|
|
201
|
-
const duration = renderToolDuration(r);
|
|
202
|
+
const duration = renderToolDuration(r, ctx);
|
|
202
203
|
const summary = `${FG_DIM}${d.matchCount} files — ctrl+o to expand${RST}${duration ? `${FG_DIM}· ${duration}${RST}` : ""}`;
|
|
203
204
|
if (setCollapsedToolTitle(ctx, text, ` ${summary}`)) return text;
|
|
204
|
-
text.setText(fillToolBody(`${
|
|
205
|
+
text.setText(fillToolBody(`${ind}${summary}`, undefined, undefined, ind));
|
|
205
206
|
return text;
|
|
206
207
|
}
|
|
207
208
|
const rendered = renderFindResults(d.text, theme)
|
|
208
209
|
.split("\n")
|
|
209
|
-
.map((l) => `${
|
|
210
|
+
.map((l) => `${ind}${l}`)
|
|
210
211
|
.join("\n");
|
|
211
|
-
const noticeStr = d.notices?.length
|
|
212
|
-
|
|
213
|
-
: "";
|
|
214
|
-
const duration = renderToolDuration(r);
|
|
212
|
+
const noticeStr = d.notices?.length ? `\n${ind}${theme.fg("warning", `[${d.notices.join(". ")}]`)}` : "";
|
|
213
|
+
const duration = renderToolDuration(r, ctx);
|
|
215
214
|
text.setText(
|
|
216
215
|
fillToolBody(
|
|
217
|
-
`${
|
|
216
|
+
`${ind}${theme.fg("dim", `${d.matchCount} files`)}${duration ? `${FG_DIM}· ${duration}${RST}` : ""}\n${rendered}${noticeStr}`,
|
|
217
|
+
undefined,
|
|
218
|
+
undefined,
|
|
219
|
+
ind,
|
|
218
220
|
),
|
|
219
221
|
);
|
|
220
222
|
return text;
|
|
221
223
|
}
|
|
222
224
|
const fc = r.content?.[0] as TextContent | undefined;
|
|
223
|
-
text.setText(
|
|
225
|
+
text.setText(
|
|
226
|
+
fillToolBody(`${ind}${theme.fg("dim", fc?.text?.slice(0, 120) ?? "0 files")}`, undefined, undefined, ind),
|
|
227
|
+
);
|
|
224
228
|
return text;
|
|
225
229
|
},
|
|
226
230
|
} as unknown as ToolDefinition);
|
package/src/tools/grep.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* pi-pretty: grep tool -- FFF-backed text search with SDK fallback. */
|
|
2
2
|
|
|
3
3
|
import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { BG_ERROR, FG_DIM, RST, resolveBaseBackground
|
|
4
|
+
import { BG_ERROR, FG_DIM, RST, resolveBaseBackground } from "../config.js";
|
|
5
5
|
import { fffFormatGrepText } from "../fff-helpers.js";
|
|
6
6
|
import { normalizeLineEndings, shortPath } from "../helpers.js";
|
|
7
7
|
import { NOTICE_PARTIAL_FILE_INDEX } from "../notices.js";
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
rememberToolTitle,
|
|
12
12
|
renderToolError,
|
|
13
13
|
setCollapsedToolTitle,
|
|
14
|
+
toolIndent,
|
|
14
15
|
} from "../render.js";
|
|
15
16
|
import { resolveTextCtor } from "../tui-text.js";
|
|
16
17
|
import type { FffServiceWithCursor, GrepDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
|
|
@@ -122,8 +123,9 @@ export function registerGrepTool(
|
|
|
122
123
|
if (limit !== undefined && limit !== null) out += theme.fg("dim", ` limit ${limit}`);
|
|
123
124
|
if (literal) out += theme.fg("dim", ` (literal)`);
|
|
124
125
|
if (caseInsensitive) out += theme.fg("dim", ` (case-insensitive)`);
|
|
126
|
+
const ind = toolIndent(ctx);
|
|
125
127
|
const renderTitle = (suffix = ""): string =>
|
|
126
|
-
fillToolBackground(`\n${
|
|
128
|
+
fillToolBackground(`\n${ind}${out}${suffix}\n`, ctx.isError ? BG_ERROR : undefined, undefined, ind);
|
|
127
129
|
rememberToolTitle(ctx, text, renderTitle);
|
|
128
130
|
text.setText(renderTitle());
|
|
129
131
|
return text;
|
|
@@ -132,6 +134,7 @@ export function registerGrepTool(
|
|
|
132
134
|
renderResult(result: Result, _opt: unknown, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
133
135
|
resolveBaseBackground(theme);
|
|
134
136
|
const text = ctx.lastComponent ?? new T("", 0, 0);
|
|
137
|
+
const ind = toolIndent(ctx);
|
|
135
138
|
if (ctx.isError) {
|
|
136
139
|
text.setText(
|
|
137
140
|
renderToolError(
|
|
@@ -140,6 +143,7 @@ export function registerGrepTool(
|
|
|
140
143
|
.map((c) => c.text)
|
|
141
144
|
.join("\n") || "Error",
|
|
142
145
|
theme,
|
|
146
|
+
ind,
|
|
143
147
|
),
|
|
144
148
|
);
|
|
145
149
|
return text;
|
|
@@ -150,7 +154,7 @@ export function registerGrepTool(
|
|
|
150
154
|
if (!ctx.expanded) {
|
|
151
155
|
const summary = `${FG_DIM}${lines.length} lines — ctrl+o to expand${RST}`;
|
|
152
156
|
if (setCollapsedToolTitle(ctx, text, ` ${summary}`)) return text;
|
|
153
|
-
text.setText(fillToolBody(`${
|
|
157
|
+
text.setText(fillToolBody(`${ind}${summary}`, ctx.isError ? BG_ERROR : undefined, undefined, ind));
|
|
154
158
|
return text;
|
|
155
159
|
}
|
|
156
160
|
const maxShow = lines.length;
|
|
@@ -164,14 +168,14 @@ export function registerGrepTool(
|
|
|
164
168
|
if (remaining > 0) {
|
|
165
169
|
out.push(theme.fg("muted", `… (${remaining} more ${remaining === 1 ? "line" : "lines"}, to expand)`));
|
|
166
170
|
}
|
|
167
|
-
const body = out.map((l) => `${
|
|
168
|
-
text.setText(fillToolBody(body, ctx.isError ? BG_ERROR : undefined));
|
|
171
|
+
const body = out.map((l) => `${ind}${l}`).join("\n");
|
|
172
|
+
text.setText(fillToolBody(body, ctx.isError ? BG_ERROR : undefined, undefined, ind));
|
|
169
173
|
return text;
|
|
170
174
|
}
|
|
171
175
|
const fc = result.content?.[0];
|
|
172
176
|
const fallback = fc && "text" in fc ? String(fc.text).slice(0, 120) : "no matches";
|
|
173
177
|
text.setText(
|
|
174
|
-
fillToolBody(`${
|
|
178
|
+
fillToolBody(`${ind}${theme.fg("dim", fallback)}`, ctx.isError ? BG_ERROR : undefined, undefined, ind),
|
|
175
179
|
);
|
|
176
180
|
return text;
|
|
177
181
|
},
|