@quandev104/pi-style 0.1.2 → 0.1.4

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +1 -2
  3. package/dist/extensions/pi-style.js +5919 -4966
  4. package/dist/extensions/pi-style.js.map +1 -1
  5. package/extension-src/pi-style/app/command-service.ts +2 -2
  6. package/extension-src/pi-style/app/index.ts +0 -1
  7. package/extension-src/pi-style/domain/config-authorization.ts +1 -2
  8. package/extension-src/pi-style/domain/config-normalization.ts +3 -3
  9. package/extension-src/pi-style/domain/config-types.ts +2 -2
  10. package/extension-src/pi-style/domain/status.ts +1 -1
  11. package/extension-src/pi-style/domain/theme.ts +3 -0
  12. package/extension-src/pi-style/features/messages/index.ts +2 -8
  13. package/extension-src/pi-style/features/tools/boxed/bash.ts +663 -45
  14. package/extension-src/pi-style/features/tools/boxed/batch.ts +459 -0
  15. package/extension-src/pi-style/features/tools/boxed/edit.ts +28 -2
  16. package/extension-src/pi-style/features/tools/boxed/fallback.ts +47 -4
  17. package/extension-src/pi-style/features/tools/boxed/find.ts +48 -48
  18. package/extension-src/pi-style/features/tools/boxed/grep.ts +161 -89
  19. package/extension-src/pi-style/features/tools/boxed/index.ts +4 -0
  20. package/extension-src/pi-style/features/tools/boxed/ls.ts +39 -47
  21. package/extension-src/pi-style/features/tools/boxed/output-tree.ts +368 -0
  22. package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +25 -4
  23. package/extension-src/pi-style/features/tools/boxed/read.ts +32 -189
  24. package/extension-src/pi-style/features/tools/boxed/session-config.ts +70 -4
  25. package/extension-src/pi-style/features/tools/boxed/shared.ts +41 -1
  26. package/extension-src/pi-style/features/tools/boxed/write.ts +105 -47
  27. package/extension-src/pi-style/features/tools/index.ts +14 -0
  28. package/extension-src/pi-style/pi/compatibility-coordinator.ts +4 -26
  29. package/extension-src/pi-style/pi/compatibility-probe.ts +3 -33
  30. package/extension-src/pi-style/pi/compatibility-registry.ts +0 -1
  31. package/extension-src/pi-style/pi/config-session.ts +0 -2
  32. package/extension-src/pi-style/pi/index.ts +35 -1
  33. package/extension-src/pi-style/pi/session-coordinator.ts +47 -3
  34. package/extension-src/pi-style/shared/box.ts +117 -26
  35. package/extension-src/pi-style/shared/theme-extras.ts +0 -2
  36. package/package.json +1 -1
@@ -2,8 +2,16 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
2
2
  import { type KeyId, matchesKey } from "@earendil-works/pi-tui";
3
3
  import type { ConfigFilePort } from "../app/config-storage.js";
4
4
  import { createPiStyleApp, type PiStyleApp } from "../app/index.js";
5
+ import { resolveTheme } from "../domain/theme.js";
5
6
  import { setSpecialBlockTheme } from "../features/messages/special-blocks.js";
6
- import { setToolsRenderConfig } from "../features/tools/boxed/session-config.js";
7
+ import { resetBashTreeRegistry } from "../features/tools/boxed/bash.js";
8
+ import { resetBatchRegistry } from "../features/tools/boxed/batch.js";
9
+ import { resetGrepRegistry } from "../features/tools/boxed/grep.js";
10
+ import {
11
+ setToolsRenderConfig,
12
+ stopAllElapsedTickers,
13
+ type ToolsRenderConfig,
14
+ } from "../features/tools/boxed/session-config.js";
7
15
  import { createCompatibilityCoordinator } from "./compatibility-coordinator.js";
8
16
  import {
9
17
  type CompatibilityCleanupResult,
@@ -45,6 +53,8 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
45
53
  let active = false;
46
54
  let tuiSession = false;
47
55
  let terminalInputUnsubscribe: (() => void) | undefined;
56
+ let sessionTheme: unknown;
57
+ let sessionUi: import("@earendil-works/pi-coding-agent").ExtensionUIContext | undefined;
48
58
  const source = createConfigSourceAdapter(
49
59
  pi,
50
60
  filePort,
@@ -68,6 +78,22 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
68
78
  ),
69
79
  );
70
80
  };
81
+ /** Render-scoped tool config: line budgets + the resolved open-tree glyph. */
82
+ const applyToolsRenderConfig = (config: import("../domain/config-types.js").NormalizedPiStyleConfig) => {
83
+ setToolsRenderConfig({
84
+ ...config.tools,
85
+ batchOpenGlyph: resolveTheme(sessionTheme as never, config, process.env).glyph("batchOpen"),
86
+ nerdFonts: resolveTheme(sessionTheme as never, config, process.env).mode === "nerd",
87
+ } satisfies ToolsRenderConfig);
88
+ };
89
+ /**
90
+ * Hide Pi's "Thinking..." placeholder label: an empty label renders zero
91
+ * lines, so the thinking block leaves no trace while content stays hidden.
92
+ * Passing undefined restores the default label.
93
+ */
94
+ const applyMessagesConfig = (config: import("../domain/config-types.js").NormalizedPiStyleConfig) => {
95
+ sessionUi?.setHiddenThinkingLabel?.(config.messages.hideThinkingLabel ? "" : undefined);
96
+ };
71
97
  const app: PiStyleApp = createPiStyleApp(
72
98
  undefined,
73
99
  {
@@ -79,6 +105,10 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
79
105
  (config) => {
80
106
  productGate = app.productPolicy.corePatchGate;
81
107
  if (!active) return;
108
+ // Apply render-scoped tool config live so `/pi-style set tools.*` takes
109
+ // effect immediately (line budgets, dimOutput, open-tree glyph, …).
110
+ applyToolsRenderConfig(config);
111
+ applyMessagesConfig(config);
82
112
  if (compatibility.report) {
83
113
  const cleanup = compatibility.dispose();
84
114
  if (!cleanup.complete) {
@@ -100,7 +130,6 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
100
130
  authorization = readSessionAuthorization(pi);
101
131
  compatibility.captureAuthorization(
102
132
  authorization.core,
103
- authorization.user,
104
133
  authorization.assistant,
105
134
  authorization.specialBlocks,
106
135
  authorization.tools,
@@ -120,6 +149,14 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
120
149
  tuiSession = ctx.mode === "tui";
121
150
  app.setProjectTrusted(ctx.isProjectTrusted());
122
151
  source.setSession(cwd, ctx.isProjectTrusted());
152
+ // Drop any batch state carried over from the previous session (Pi renders
153
+ // the restored chat between session_shutdown and the next session_start).
154
+ resetBatchRegistry();
155
+ resetGrepRegistry();
156
+ resetBashTreeRegistry();
157
+ // Stop any 1s elapsed re-render ticker left by a tool that was still
158
+ // running when the session ended.
159
+ stopAllElapsedTickers();
123
160
  active = false;
124
161
  await app.reload();
125
162
  productGate = app.productPolicy.corePatchGate;
@@ -127,7 +164,10 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
127
164
  compatibility.install(app.config, ctx.mode === "tui", productGate);
128
165
  // Session-scoped render configuration for the boxed tool/message surfaces.
129
166
  // Populated once per session (never inside render).
130
- setToolsRenderConfig(app.config.tools);
167
+ sessionTheme = ctx.ui?.theme as never;
168
+ sessionUi = ctx.ui as import("@earendil-works/pi-coding-agent").ExtensionUIContext | undefined;
169
+ applyToolsRenderConfig(app.config);
170
+ applyMessagesConfig(app.config);
131
171
  if (ctx.ui?.theme) setSpecialBlockTheme(ctx.ui.theme as never);
132
172
  const toolDetails = collectToolDetails(pi.getActiveTools?.(), pi.getAllTools?.());
133
173
  app.sessionStart(
@@ -180,6 +220,10 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
180
220
  tuiSession = false;
181
221
  terminalInputUnsubscribe?.();
182
222
  terminalInputUnsubscribe = undefined;
223
+ resetBatchRegistry();
224
+ resetGrepRegistry();
225
+ resetBashTreeRegistry();
226
+ stopAllElapsedTickers();
183
227
  app.sessionShutdown();
184
228
  // Tier C prototype patches stay installed across session switches. Pi renders
185
229
  // the restored chat (renderBeforeBind) AFTER session_shutdown but BEFORE the
@@ -43,10 +43,26 @@ export interface BoxedRenderOptions {
43
43
  isError?: boolean;
44
44
  isPartial?: boolean;
45
45
  isPending?: boolean;
46
+ /** Execution has started but the tool is still running (title `◌` instead of `✓`). */
47
+ running?: boolean;
48
+ /** A result renderer already produced a continuation for this call, so the call
49
+ * leaves the box open instead of closing it with a pending label. */
50
+ resultSeen?: boolean;
46
51
  pendingText?: string;
52
+ /** Verbatim bottom-border label for the pending/running card (overrides the
53
+ * `… ${pendingText}` default, e.g. a live `◌ Running · 12.4s` status). */
54
+ pendingLabel?: string;
47
55
  state?: Record<string, unknown>;
48
56
  /** Wall-clock elapsed override (used when metrics are not in result.details). */
49
57
  elapsedMs?: number;
58
+ /** Width-dependent content lines rendered between the top border and the
59
+ * footer border of a compact box (replaces the blank breathing line).
60
+ * Each returned line is truncated to the box inner width by the renderer.
61
+ * Returns an empty array for no body (blank line preserved). */
62
+ bodyLines?: (contentWidth: number) => string[];
63
+ /** Right-side label embedded in the compact box bottom border before the
64
+ * corner (e.g. an expand hint such as `Ctrl+O for more`). */
65
+ bottomRightLabel?: string;
50
66
  }
51
67
 
52
68
  export function isExpanded(options: { expanded?: boolean } | undefined): boolean {
@@ -133,7 +149,7 @@ export function countWords(text: string): number {
133
149
  return count;
134
150
  }
135
151
 
136
- function formatCompactCount(value: number): string {
152
+ export function formatCompactCount(value: number): string {
137
153
  if (value < 1000) return `${Math.round(value)}`;
138
154
  if (value < 10000) return `${(value / 1000).toFixed(1)}k`;
139
155
  if (value < 1000000) return `${Math.round(value / 1000)}k`;
@@ -323,17 +339,47 @@ function formatBoxedStatusIcon(theme: BoxTheme, isError?: boolean): string {
323
339
  return theme.fg(isError ? "error" : "success", icon);
324
340
  }
325
341
 
326
- export function formatBoxedToolTitle(theme: BoxTheme, name: string, isError?: boolean): string {
327
- const rawTitle = `➔ ${name}`;
342
+ /**
343
+ * Colored `➔ Name` prefix for tool titles (identity color). The status glyph
344
+ * (✓/◌/✗) is appended separately by formatBoxedToolTitle.
345
+ */
346
+ export function formatToolTitlePrefix(theme: BoxTheme, name: string): string {
347
+ return colorFromExtra(theme, "bashPromptColor", "bashMode", `➔ ${name}`);
348
+ }
349
+
350
+ export type BoxedTitleStatus = "running" | "pending";
351
+
352
+ /**
353
+ * Boxed tool title: `➔ Name ✓` when settled, `➔ Name ◌` while running, plain
354
+ * `➔ Name` while pending, and a fully error-colored `➔ Name ✗` on failure.
355
+ * The ✓/◌ glyphs are never shown before the tool settles, so a card that is
356
+ * still executing never reads as finished.
357
+ */
358
+ export function formatBoxedToolTitle(
359
+ theme: BoxTheme,
360
+ name: string,
361
+ isError?: boolean,
362
+ status?: BoxedTitleStatus,
363
+ ): string {
328
364
  // On failure the whole title turns error-colored (not just the ✗) so a failed
329
365
  // tool reads instantly; on success the tool keeps its identity color and only
330
366
  // the ✓ carries the success color.
331
367
  const coloredTitle = isError
332
- ? theme.fg("error", `${rawTitle} ✗`)
333
- : `${colorFromExtra(theme, "bashPromptColor", "bashMode", rawTitle)} ${formatBoxedStatusIcon(theme, false)}`;
368
+ ? theme.fg("error", `➔ ${name} ✗`)
369
+ : status === "running"
370
+ ? `${formatToolTitlePrefix(theme, name)} ${theme.fg("text", "◌")}`
371
+ : status === "pending"
372
+ ? formatToolTitlePrefix(theme, name)
373
+ : `${formatToolTitlePrefix(theme, name)} ${formatBoxedStatusIcon(theme, false)}`;
334
374
  return typeof theme?.bold === "function" ? theme.bold(coloredTitle) : coloredTitle;
335
375
  }
336
376
 
377
+ /** Live running status label for pending/running cards and streaming footers. */
378
+ export function formatBoxedRunningStatus(theme: BoxTheme, elapsedMs: number | undefined): string {
379
+ const elapsed = elapsedMs === undefined ? "" : `${theme.fg("text", ` · ${(elapsedMs / 1000).toFixed(1)}s`)}`;
380
+ return `${theme.fg("dim", "◌ Running")}${elapsed}`;
381
+ }
382
+
337
383
  function boxText(theme: BoxTheme, text: string): string {
338
384
  return `${RESET_INTENSITY}${theme.fg("borderMuted", text)}`;
339
385
  }
@@ -423,6 +469,22 @@ export function boxBlankLine(theme: BoxTheme, width: number): string {
423
469
  return `${boxFrameText(theme, BOX_VERTICAL)}${sidePad}${" ".repeat(contentWidth)}${sidePad}${boxFrameText(theme, BOX_VERTICAL)}`;
424
470
  }
425
471
 
472
+ export function boxLineAligned(theme: BoxTheme, left: string, right: string, width: number): string {
473
+ const renderedWidth = boxWidth(width);
474
+ const contentWidth = boxInnerWidth(renderedWidth);
475
+ const rightWidth = safeVisibleWidth(right);
476
+ const sidePad = " ".repeat(BOX_SIDE_PADDING);
477
+
478
+ if (!right || rightWidth >= contentWidth) {
479
+ return boxLine(theme, right || left, renderedWidth);
480
+ }
481
+
482
+ const maxLeftWidth = Math.max(1, contentWidth - rightWidth - 1);
483
+ const truncatedLeft = safeTruncateToWidth(left, maxLeftWidth, "…");
484
+ const gap = " ".repeat(Math.max(1, contentWidth - safeVisibleWidth(truncatedLeft) - rightWidth));
485
+ return `${boxFrameText(theme, BOX_VERTICAL)}${sidePad}${truncatedLeft}${gap}${right}${sidePad}${boxFrameText(theme, BOX_VERTICAL)}`;
486
+ }
487
+
426
488
  export function boxLineWithRight(theme: BoxTheme, left: string, right: string, width: number): string {
427
489
  const renderedWidth = boxWidth(width);
428
490
  const contentWidth = boxInnerWidth(renderedWidth);
@@ -540,7 +602,12 @@ export function renderBoxedToolCall(
540
602
  },
541
603
  render(width: number): string[] {
542
604
  if (cache?.width === width) return cache.lines;
543
- const title = formatBoxedToolTitle(theme, toolName, options.isError);
605
+ const title = formatBoxedToolTitle(
606
+ theme,
607
+ toolName,
608
+ options.isError,
609
+ options.isPending ? (options.running ? "running" : "pending") : undefined,
610
+ );
544
611
  const headerLabel = options.headerDetail ? `${title} · ${options.headerDetail}` : title;
545
612
  const renderedWidth = boxWidth(width);
546
613
  const lines = [
@@ -548,22 +615,26 @@ export function renderBoxedToolCall(
548
615
  boxBlankLine(theme, renderedWidth),
549
616
  ...detailLines.flatMap((line) => boxedWrappedLines(theme, line, renderedWidth)),
550
617
  ];
551
- if (options.isPending) {
552
- const pendingText = options.pendingText ?? "Waiting for output…";
618
+ if (options.isPending && !options.resultSeen) {
619
+ // Pending/running card: close the box with the status label. Once a
620
+ // result renderer has produced a continuation, the box stays open and
621
+ // that continuation closes it.
622
+ const pendingLabel =
623
+ options.pendingLabel ?? theme.fg("dim", `… ${options.pendingText ?? "Waiting for output…"}`);
553
624
  lines.push(
554
625
  boxBlankLine(theme, renderedWidth),
555
626
  boxLabeledBorder(
556
627
  theme,
557
628
  BOX_ROUND_BOTTOM_LEFT,
558
629
  BOX_ROUND_BOTTOM_RIGHT,
559
- theme.fg("dim", `… ${pendingText}`),
630
+ pendingLabel,
560
631
  undefined,
561
632
  renderedWidth,
562
633
  ),
563
634
  );
564
635
  } else {
565
636
  // Leave the box open with trailing breathing room; the result renderer
566
- // continues it with the Response divider.
637
+ // continues it with the result divider.
567
638
  lines.push(boxBlankLine(theme, renderedWidth));
568
639
  }
569
640
  cache = { width, lines };
@@ -593,15 +664,23 @@ export function renderCompactBoxedToolCall(
593
664
  invalidate() {},
594
665
  render(width: number): string[] {
595
666
  const renderedWidth = boxWidth(width);
596
- const title = formatBoxedToolTitle(theme, toolName, options.isError);
667
+ const title = formatBoxedToolTitle(
668
+ theme,
669
+ toolName,
670
+ options.isError,
671
+ options.isPending ? (options.running ? "running" : "pending") : undefined,
672
+ );
597
673
  const headerLabel = detailLine ? `${title} · ${detailLine}` : title;
598
674
  const compactFooter =
599
675
  typeof options.state?.[COMPACT_FOOTER_KEY] === "string" ? options.state[COMPACT_FOOTER_KEY] : "";
600
676
  const _footerIsError = Boolean(options.state?.[COMPACT_FOOTER_ERROR_KEY]);
601
677
  const _footerIsPartial = Boolean(options.state?.[COMPACT_FOOTER_PARTIAL_KEY]);
678
+ const bodyLines = options.bodyLines ? options.bodyLines(boxInnerWidth(renderedWidth)) : [];
602
679
  const lines = [
603
680
  boxLabeledBorder(theme, BOX_ROUND_TOP_LEFT, BOX_ROUND_TOP_RIGHT, headerLabel, undefined, renderedWidth),
604
- boxBlankLine(theme, renderedWidth),
681
+ ...(bodyLines.length > 0
682
+ ? bodyLines.map((line) => boxLine(theme, line, renderedWidth))
683
+ : [boxBlankLine(theme, renderedWidth)]),
605
684
  ];
606
685
  if (compactFooter) {
607
686
  lines.push(
@@ -610,24 +689,28 @@ export function renderCompactBoxedToolCall(
610
689
  BOX_ROUND_BOTTOM_LEFT,
611
690
  BOX_ROUND_BOTTOM_RIGHT,
612
691
  compactFooter,
613
- undefined,
692
+ options.bottomRightLabel,
614
693
  renderedWidth,
615
694
  ),
616
695
  );
617
696
  } else if (options.isPending) {
618
- const pendingText = options.pendingText ?? "Waiting for output…";
697
+ const pendingLabel =
698
+ options.pendingLabel ??
699
+ (options.running
700
+ ? formatBoxedRunningStatus(theme, undefined)
701
+ : theme.fg("dim", `… ${options.pendingText ?? "Waiting for output…"}`));
619
702
  lines.push(
620
703
  boxLabeledBorder(
621
704
  theme,
622
705
  BOX_ROUND_BOTTOM_LEFT,
623
706
  BOX_ROUND_BOTTOM_RIGHT,
624
- theme.fg("dim", `… ${pendingText}`),
625
- undefined,
707
+ pendingLabel,
708
+ options.bottomRightLabel,
626
709
  renderedWidth,
627
710
  ),
628
711
  );
629
712
  } else {
630
- // No footer yet (transient, or the result opens the Response divider):
713
+ // No footer yet (transient, or the result opens the result divider):
631
714
  // leave the box open so the result renderer continues the same box.
632
715
  }
633
716
  return lines;
@@ -655,6 +738,10 @@ export function renderBoxedToolResult(
655
738
  expandHint?: string;
656
739
  isError?: boolean;
657
740
  isPartial?: boolean;
741
+ /** Skip the result divider entirely (streaming continuation into an open call box). */
742
+ showDivider?: boolean;
743
+ /** Error state marker prepended to the body (default `✗ Error`). */
744
+ errorLabel?: string;
658
745
  } = {},
659
746
  ): Component {
660
747
  let cache: RenderLinesCache | null = null;
@@ -668,7 +755,7 @@ export function renderBoxedToolResult(
668
755
  const renderedWidth = boxWidth(width);
669
756
  const maxContentWidth = boxInnerWidth(renderedWidth);
670
757
  const bodyLines = typeof body === "function" ? body(maxContentWidth) : body.render(maxContentWidth);
671
- const errorPrefix = options.isError ? [theme.fg("error", "✗ Error")] : [];
758
+ const errorPrefix = options.isError ? [theme.fg("error", options.errorLabel ?? "✗ Error")] : [];
672
759
  const outputLines =
673
760
  bodyLines.length > 0
674
761
  ? [...errorPrefix, ...bodyLines]
@@ -679,14 +766,18 @@ export function renderBoxedToolResult(
679
766
  ? options.dividerLabel(renderedWidth)
680
767
  : (options.dividerLabel ?? "Response");
681
768
  const rendered = [
682
- boxLabeledBorder(
683
- theme,
684
- BOX_DIVIDER_LEFT,
685
- BOX_DIVIDER_RIGHT,
686
- theme.fg("dim", dividerText),
687
- options.dividerRightLabel ? theme.fg("dim", options.dividerRightLabel) : undefined,
688
- renderedWidth,
689
- ),
769
+ ...(options.showDivider === false
770
+ ? []
771
+ : [
772
+ boxLabeledBorder(
773
+ theme,
774
+ BOX_DIVIDER_LEFT,
775
+ BOX_DIVIDER_RIGHT,
776
+ theme.fg("dim", dividerText),
777
+ options.dividerRightLabel ? theme.fg("dim", options.dividerRightLabel) : undefined,
778
+ renderedWidth,
779
+ ),
780
+ ]),
690
781
  boxBlankLine(theme, renderedWidth),
691
782
  ...renderBoxedOutputLines(theme, outputLines, renderedWidth, options.renderLineBudget ?? outputLines.length),
692
783
  boxBlankLine(theme, renderedWidth),
@@ -20,8 +20,6 @@ const extensionDir = dirname(fileURLToPath(import.meta.url));
20
20
  export const THEME_EXTRA_DEFAULTS: Readonly<Record<string, string>> = Object.freeze({
21
21
  assistantPrefix: "•",
22
22
  assistantPrefixColor: "",
23
- userPrefix: "❯",
24
- userPrefixColor: "accent",
25
23
  dividerChar: "─",
26
24
  dividerColor: "",
27
25
  showDivider: "true",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quandev104/pi-style",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "A native-layout, cohesive visual style package for Pi.",
5
5
  "license": "MIT",
6
6
  "type": "module",