@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/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
- pi.registerMarkdownTransformer?.((markdown, context) => {
80
- if (context.messageType !== "user" || markdown.trim() === "") return markdown;
81
- return ` ${USER_MESSAGE_ICON} ${markdown}`;
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(hostCustomEditor, (icon) => {
218
- const theme = ctx.ui.theme;
219
- return typeof theme.getThinkingBorderColor === "function"
220
- ? theme.getThinkingBorderColor(ctx.thinkingLevel ?? "off")(icon)
221
- : theme.fg("thinkingText", icon);
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(pi, cwd, null, createReadTool(cwd), TextComp);
560
+ registerReadTool(toolPi, cwd, null, createReadTool(cwd), TextComp);
541
561
  }
542
562
  if (isToolEnabled("bash") && createBashTool) {
543
- registerBashTool(pi, cwd, null, createBashTool(cwd), TextComp);
563
+ registerBashTool(toolPi, cwd, null, createBashTool(cwd), TextComp);
544
564
  }
545
565
  if (isToolEnabled("ls") && createLsTool) {
546
- registerLsTool(pi, cwd, null, createLsTool(cwd), TextComp);
566
+ registerLsTool(toolPi, cwd, null, createLsTool(cwd), TextComp);
547
567
  }
548
568
  if (isToolEnabled("find") && createFindTool) {
549
- registerFindTool(pi, cwd, fffService, createFindTool(cwd), TextComp);
569
+ registerFindTool(toolPi, cwd, fffService, createFindTool(cwd), TextComp);
550
570
  }
551
571
  if (isToolEnabled("grep") && createGrepTool) {
552
- registerGrepTool(pi, cwd, fffService, createGrepTool(cwd), TextComp);
572
+ registerGrepTool(toolPi, cwd, fffService, createGrepTool(cwd), TextComp);
553
573
  }
554
574
  }
@@ -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 PROMPT_WIDTH = 3; // one space on either side of the icon
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.userPaddingX + this.promptWidth());
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 PROMPT_WIDTH + (supportsBorderHooks && this.userPaddingX === 0 ? 1 : 0);
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.userPaddingX + this.promptWidth());
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
- return this.roundBorder(super.renderTopBorder(width, hiddenLineCount), width, "╭", "╮");
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
- this.bottomBorderLine = this.roundBorder(super.renderBottomBorder(width, hiddenLineCount), width, "╰", "╯");
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.userPaddingX + this.promptWidth(), maxPadding);
79
- if (promptPadding >= PROMPT_WIDTH && firstContentLine.startsWith(" ".repeat(promptPadding))) {
80
- const promptStart = promptPadding - PROMPT_WIDTH;
81
- const prompt = ` ${colorPrompt(PROMPT_ICON)} `;
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;
@@ -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 ? THINKING_LABEL : `${THOUGHT_LABEL} ${formatThinkingDuration(ms)}`;
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
- const THINKING_LABEL = " Thinking...";
592
- const THOUGHT_LABEL = " Thought for";
593
- const padThinkingLabel = (label: string): string => (label.startsWith(" ") ? label : ` ${label}`);
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(`${THINKING_LABEL} ${duration()}`);
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(`${THOUGHT_LABEL} ${formatThinkingDuration(ms)}`);
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 = THINKING_LABEL;
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 = THINKING_LABEL): void {
726
+ tick(label = thinkingLabel()): void {
715
727
  const paddedLabel = padThinkingLabel(label);
716
728
  if (paddedLabel !== currentLabel) {
717
729
  frames = buildLabelFrames(paddedLabel);