@heyhuynhgiabuu/pi-pretty 0.6.31 → 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 +15 -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/types.d.ts +5 -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 +1 -1
- 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/types.ts +5 -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/types.ts
CHANGED
|
@@ -34,6 +34,11 @@ export interface RenderCtxLike {
|
|
|
34
34
|
isError?: boolean;
|
|
35
35
|
state: Record<string, unknown>;
|
|
36
36
|
expanded?: boolean;
|
|
37
|
+
/** Tool call arguments as streamed so far. */
|
|
38
|
+
args?: Record<string, unknown>;
|
|
39
|
+
isPartial?: boolean;
|
|
40
|
+
/** False while the tool call is still streaming its arguments. */
|
|
41
|
+
executionStarted?: boolean;
|
|
37
42
|
invalidate?: () => void;
|
|
38
43
|
/** Host horizontal padding (pi >= 1.1). Absent on older hosts; renderers fall back to 1. */
|
|
39
44
|
outputPad?: number;
|
package/src/working-indicator.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* phase. The extension adds no interrupt hint or token-count suffix.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { keyText } from "@earendil-works/pi-coding-agent";
|
|
17
18
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
18
19
|
import { FG_BLUE, FG_DIM, FG_MUTED, type ThinkingIndicatorConfig, type WorkingIndicatorConfig } from "./config.js";
|
|
19
20
|
import { dimAccentHex, hexToAnsiFg, sessionAccentHex } from "./session-color.js";
|
|
@@ -539,7 +540,7 @@ export function installPerRowThinkingLabels(componentClass: unknown): PerRowThin
|
|
|
539
540
|
if (typeof ts !== "number") return incoming;
|
|
540
541
|
if (ts === state.activeTs) return incoming;
|
|
541
542
|
const ms = state.completed.get(ts);
|
|
542
|
-
return ms === undefined ?
|
|
543
|
+
return ms === undefined ? thinkingLabel() : thoughtLabel(ms);
|
|
543
544
|
} catch {
|
|
544
545
|
return incoming;
|
|
545
546
|
}
|
|
@@ -588,9 +589,20 @@ export interface ThinkingTimer {
|
|
|
588
589
|
restore(): void;
|
|
589
590
|
}
|
|
590
591
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
592
|
+
let claudeThinkingLabels = false;
|
|
593
|
+
/** claudecode tool style: flush-left `thinking` / `thought 3s` instead of ` Thinking...` / ` Thought for 3s`. */
|
|
594
|
+
export function setClaudeThinkingLabels(enabled: boolean): void {
|
|
595
|
+
claudeThinkingLabels = enabled;
|
|
596
|
+
}
|
|
597
|
+
const thinkingLabel = (): string => (claudeThinkingLabels ? "thinking" : " Thinking...");
|
|
598
|
+
const padThinkingLabel = (label: string): string =>
|
|
599
|
+
claudeThinkingLabels || label.startsWith(" ") ? label : ` ${label}`;
|
|
600
|
+
/** Settled label for a finished thinking run; claudecode style appends the toggle hint when bound. */
|
|
601
|
+
const thoughtLabel = (ms: number): string => {
|
|
602
|
+
if (!claudeThinkingLabels) return ` Thought for ${formatThinkingDuration(ms)}`;
|
|
603
|
+
const key = keyText("app.thinking.toggle");
|
|
604
|
+
return `thought ${formatThinkingDuration(ms)}${key ? ` (${key} to expand)` : ""}`;
|
|
605
|
+
};
|
|
594
606
|
|
|
595
607
|
/** Format sub-second elapsed time as milliseconds, then use compact whole seconds. */
|
|
596
608
|
export function formatThinkingDuration(elapsedMs: number): string {
|
|
@@ -626,13 +638,13 @@ export function createThinkingTimer(
|
|
|
626
638
|
return {
|
|
627
639
|
tick(): void {
|
|
628
640
|
if (terminal) return;
|
|
629
|
-
animator.tick(`${
|
|
641
|
+
animator.tick(`${thinkingLabel()} ${duration()}`);
|
|
630
642
|
},
|
|
631
643
|
complete(): number | undefined {
|
|
632
644
|
if (terminal) return undefined;
|
|
633
645
|
terminal = true;
|
|
634
646
|
const ms = elapsed();
|
|
635
|
-
animator.show(
|
|
647
|
+
animator.show(thoughtLabel(ms));
|
|
636
648
|
return ms;
|
|
637
649
|
},
|
|
638
650
|
restore(): void {
|
|
@@ -702,7 +714,7 @@ export function createThinkingLabelAnimator(
|
|
|
702
714
|
}).frames.map((frame) => `${leftPadding}${frame}`);
|
|
703
715
|
};
|
|
704
716
|
|
|
705
|
-
let currentLabel =
|
|
717
|
+
let currentLabel = thinkingLabel();
|
|
706
718
|
let frames = buildLabelFrames(currentLabel);
|
|
707
719
|
|
|
708
720
|
let index = 0;
|
|
@@ -711,7 +723,7 @@ export function createThinkingLabelAnimator(
|
|
|
711
723
|
get frames(): readonly string[] {
|
|
712
724
|
return frames;
|
|
713
725
|
},
|
|
714
|
-
tick(label =
|
|
726
|
+
tick(label = thinkingLabel()): void {
|
|
715
727
|
const paddedLabel = padThinkingLabel(label);
|
|
716
728
|
if (paddedLabel !== currentLabel) {
|
|
717
729
|
frames = buildLabelFrames(paddedLabel);
|