pi-claude-code-ui 1.0.72 → 1.0.74

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 CHANGED
@@ -3,11 +3,24 @@
3
3
  > [!IMPORTANT]
4
4
  > **1.0.69 — package rename (permanent).** Canonical npm name is now [`pi-claude-code-ui`](https://www.npmjs.com/package/pi-claude-code-ui). `pi-claude-style-tools` is legacy and will not receive further releases. Install with `pi install npm:pi-claude-code-ui` or `npm i pi-claude-code-ui`.
5
5
 
6
+ ## 1.0.74 — 2026-07-18
7
+
8
+ ### Fixed
9
+
10
+ - **Grouped tool boxes keep Agent breathe aligned** — group rows strip all breathe glyphs (including `·` and the blank off-phase) before re-prefixing a fresh light, so titles no longer walk sideways. Group header/child lights also follow the shared blink phase (and Agent breathe) instead of wall-clock `isBlinkOn()`.
11
+
12
+ ## 1.0.73 — 2026-07-18
13
+
14
+ ### Fixed
15
+
16
+ - **Agent rows align with other tools again** — removed the extra leading indent that only applied to Agent-family tool rows.
17
+ - **Agent breathe stays centered** — drop double-width `⬤` (it walked the baseline). Cycle is now single-cell glyphs `● → • → · → (invisible) → · → •` so the optical center never moves.
18
+
6
19
  ## 1.0.72 — 2026-07-18
7
20
 
8
21
  ### Changed
9
22
 
10
- - **Agent tools breathe** — `Agent` / subagent tools use a size cycle while pending (`⬤ → ● → • → (invisible) → • → ●`) instead of the ordinary on/off `●` blink, so agent work reads as a different kind of tool.
23
+ - **Agent tools breathe** — `Agent` / subagent tools use a size cycle while pending instead of the ordinary on/off `●` blink, so agent work reads as a different kind of tool.
11
24
 
12
25
  ## 1.0.71 — 2026-07-18
13
26
 
@@ -440,12 +440,6 @@ function isAgentFamilyToolName(name: unknown): boolean {
440
440
  return typeof name === "string" && AGENT_FAMILY_TOOL_NAMES.has(name);
441
441
  }
442
442
 
443
- function shouldIndentToolExecution(value: unknown): boolean {
444
- if (!value || typeof value !== "object") return false;
445
- const toolName = (value as Record<string, unknown>).toolName;
446
- return isAgentFamilyToolName(toolName);
447
- }
448
-
449
443
  function isTerminalImageLine(line: string): boolean {
450
444
  return line.includes(KITTY_IMAGE_PREFIX) || line.includes(ITERM2_IMAGE_PREFIX);
451
445
  }
@@ -550,8 +544,11 @@ function getToolGroupOverallStatus(tools: any[]): ToolStatus {
550
544
  // spot for ordinary tools. Agent-family tools use a breathing size cycle.
551
545
  const STATUS_DOT_FILLED = "●";
552
546
  const STATUS_DOT_BOLD = "\x1b[1m";
553
- // big → medium → small → invisible → small → medium (then back to big)
554
- const AGENT_BREATHE_GLYPHS = ["⬤", "●", "•", " ", "•", "●"] as const;
547
+ // Single-cell glyphs only (⬤ is often double-width and walks the baseline).
548
+ // Optical sizes share the same cell so the center stays put while breathing:
549
+ // big ● → medium • → small · → invisible → small · → medium •
550
+ const AGENT_BREATHE_GLYPHS = ["●", "•", "·", " ", "·", "•"] as const;
551
+ const AGENT_BREATHE_LEN = AGENT_BREATHE_GLYPHS.length;
555
552
 
556
553
  function paintStatusDot(colorAnsi: string): string {
557
554
  return `${colorAnsi}${STATUS_DOT_BOLD}${STATUS_DOT_FILLED}${TRANSPARENT_RESET}`;
@@ -562,17 +559,33 @@ function themeStatusDot(theme: Theme, colorKey: "success" | "error" | "dim" | "m
562
559
  return theme.fg(colorKey, `${STATUS_DOT_BOLD}${STATUS_DOT_FILLED}`);
563
560
  }
564
561
 
562
+ function agentBreatheGlyphRaw(): string {
563
+ // Always exactly one display cell — matches ordinary tool dots, keeps titles aligned.
564
+ return AGENT_BREATHE_GLYPHS[_globalBlinkPhaseIndex % AGENT_BREATHE_LEN];
565
+ }
566
+
567
+ function paintAgentBreatheDot(colorAnsi: string = TOOL_STATUS_SUCCESS): string {
568
+ const glyph = agentBreatheGlyphRaw();
569
+ if (glyph === " ") return " ";
570
+ // Bold only on the largest frame so weight changes without shifting the cell.
571
+ const bold = glyph === "●" ? STATUS_DOT_BOLD : "";
572
+ return `${colorAnsi}${bold}${glyph}${TRANSPARENT_RESET}`;
573
+ }
574
+
565
575
  function agentBreatheDot(theme: Theme): string {
566
- const glyph = AGENT_BREATHE_GLYPHS[_globalBlinkPhaseIndex % AGENT_BREATHE_GLYPHS.length];
576
+ const glyph = agentBreatheGlyphRaw();
567
577
  if (glyph === " ") return " ";
568
- return theme.fg("success", `${STATUS_DOT_BOLD}${glyph}`);
578
+ const bold = glyph === "●" ? STATUS_DOT_BOLD : "";
579
+ return theme.fg("success", `${bold}${glyph}`);
569
580
  }
570
581
 
571
- function groupStatusLight(status: ToolStatus): string {
582
+ function groupStatusLight(status: ToolStatus, options?: { agentBreathe?: boolean }): string {
572
583
  const color = status === "success" ? TOOL_STATUS_SUCCESS : status === "error" ? TOOL_STATUS_ERROR : TOOL_STATUS_PENDING;
573
584
  if (status === "pending") {
574
- // On phase: solid green. Off phase: disappear completely (space keeps alignment).
575
- return isBlinkOn() ? paintStatusDot(TOOL_STATUS_SUCCESS) : " ";
585
+ // Prefer the shared blink phase over wall-clock so group lights stay in sync
586
+ // with the global timer (and Agent breathe). Space keeps column alignment.
587
+ if (options?.agentBreathe) return paintAgentBreatheDot(TOOL_STATUS_SUCCESS);
588
+ return _globalBlinkPhase ? paintStatusDot(TOOL_STATUS_SUCCESS) : " ";
576
589
  }
577
590
  return paintStatusDot(color);
578
591
  }
@@ -610,7 +623,13 @@ function stripToolChrome(lines: string[]): string[] {
610
623
  }
611
624
 
612
625
  function stripLeadingToolStatus(line: string): string {
613
- return line.replace(/^((?:\x1b\[[0-9;]*m|[ \t]|[├└│─])*)(?:\x1b\[[0-9;]*m)*[●○✗■⬤•](?:\x1b\[[0-9;]*m)*\s+/, "$1");
626
+ // Drop the single-cell status marker so group rows can re-prefix a fresh light.
627
+ // Include Agent breathe glyphs (·) and the blank off-phase (space) so the title
628
+ // never keeps a leftover marker that shifts when size changes.
629
+ return line.replace(
630
+ /^((?:\x1b\[[0-9;]*m|[ \t]|[├└│─])*)(?:\x1b\[[0-9;]*m)*(?:[●○✗■⬤•·]| )(?:\x1b\[[0-9;]*m)*\s+/,
631
+ "$1",
632
+ );
614
633
  }
615
634
 
616
635
  function trimAnsiLeft(text: string): string {
@@ -684,19 +703,30 @@ function branchContinuation(index: number, total: number, theme?: Theme): string
684
703
  return index === total - 1 ? " " : ` ${rule}│${TRANSPARENT_RESET} `;
685
704
  }
686
705
 
687
- function formatBranchedToolLines(lines: string[], index: number, total: number, width: number, status: ToolStatus): string[] {
706
+ function formatBranchedToolLines(
707
+ lines: string[],
708
+ index: number,
709
+ total: number,
710
+ width: number,
711
+ status: ToolStatus,
712
+ options?: { agentBreathe?: boolean },
713
+ ): string[] {
688
714
  const output: string[] = [];
689
715
  const content = lines.filter((line) => isTerminalImageLine(line) || stripAnsi(line).trim().length > 0);
690
716
  const safeContent = content.length > 0 ? content : [""];
691
- const light = groupStatusLight(status);
717
+ const light = groupStatusLight(status, options);
692
718
  for (let lineIndex = 0; lineIndex < safeContent.length; lineIndex++) {
693
719
  const line = safeContent[lineIndex];
694
720
  if (isTerminalImageLine(line)) {
695
721
  output.push(line);
696
722
  continue;
697
723
  }
724
+ // Always strip any leftover status marker from the child call line before
725
+ // re-prefixing. Agent breathe used · which the old stripper missed, so the
726
+ // title walked sideways as size changed inside groups.
727
+ const body = lineIndex === 0 ? removeGroupedToolPrefix(line) : trimAnsiLeft(line);
698
728
  const prefix = lineIndex === 0 ? `${branchPrefix(index, total)}${light} ` : `${branchContinuation(index, total)} `;
699
- output.push(clampLineWidth(`${prefix}${trimAnsiLeft(line)}`, width));
729
+ output.push(clampLineWidth(`${prefix}${body}`, width));
700
730
  }
701
731
  return output;
702
732
  }
@@ -818,7 +848,11 @@ class ToolGroupComponent extends Container {
818
848
  const label = getToolGroupLabel(this.tools);
819
849
  const names = groupedName ? "" : formatToolNameList(this.tools);
820
850
  const overall: ToolStatus = status.error > 0 ? "error" : status.pending > 0 ? "pending" : "success";
821
- const light = groupStatusLight(overall);
851
+ // Group header breathes only when every pending member is Agent-family;
852
+ // mixed groups keep the ordinary on/off light.
853
+ const pendingTools = this.tools.filter((tool) => getToolStatusForGroup(tool) === "pending");
854
+ const headerBreathe = pendingTools.length > 0 && pendingTools.every((tool) => isAgentFamilyToolName(getToolName(tool)));
855
+ const light = groupStatusLight(overall, { agentBreathe: headerBreathe });
822
856
  const summaryLabel = `${label}:`;
823
857
  const countParts: string[] = [];
824
858
  if (status.pending) countParts.push(statusText("pending", status.pending));
@@ -835,7 +869,14 @@ class ToolGroupComponent extends Container {
835
869
  const rawLines = this.expanded
836
870
  ? getExpandedToolGroupLines(tool, childWidth, groupedName ? label : undefined)
837
871
  : [getCompactToolLine(tool, childWidth, groupedName ? label : undefined)];
838
- const branched = formatBranchedToolLines(rawLines, index, total, safeWidth, getToolStatusForGroup(tool));
872
+ const branched = formatBranchedToolLines(
873
+ rawLines,
874
+ index,
875
+ total,
876
+ safeWidth,
877
+ getToolStatusForGroup(tool),
878
+ { agentBreathe: isAgentFamilyToolName(getToolName(tool)) },
879
+ );
839
880
  for (let i = 0; i < branched.length; i++) {
840
881
  lines.push(clampLineWidth(branched[i], safeWidth));
841
882
  }
@@ -1018,10 +1059,11 @@ function patchGlobalToolBorders(): void {
1018
1059
  (this as any)[TOOL_RENDER_CACHE] = { width, mode: toolBackgroundMode, lines: rendered, ...branchCache };
1019
1060
  return rendered;
1020
1061
  }
1021
- const indentTool = shouldIndentToolExecution(this);
1062
+ // Agent-family tools stay column-aligned with every other tool row — no extra
1063
+ // leading indent (the old nested pad made Agent look offset from Read/Bash).
1022
1064
  const core = textLines.map((line) => {
1023
1065
  const normalized = normalizeLeadingCheckGlyph(line);
1024
- return clampLineWidth(stripOuterBackgroundAnsi(indentTool && normalized ? ` ${normalized}` : normalized), width);
1066
+ return clampLineWidth(stripOuterBackgroundAnsi(normalized), width);
1025
1067
  });
1026
1068
  const spacerLine = " ".repeat(width);
1027
1069
  let result: string[];
@@ -2681,7 +2723,7 @@ function _scheduleGlobalBlinkTimer(): void {
2681
2723
  _clearAllBlinkContexts();
2682
2724
  return;
2683
2725
  }
2684
- _globalBlinkPhaseIndex = (_globalBlinkPhaseIndex + 1) % AGENT_BREATHE_GLYPHS.length;
2726
+ _globalBlinkPhaseIndex = (_globalBlinkPhaseIndex + 1) % AGENT_BREATHE_LEN;
2685
2727
  _globalBlinkPhase = _globalBlinkPhaseIndex % 2 === 0;
2686
2728
  for (const entry of getBlinkingEntries()) {
2687
2729
  try { entry.invalidate(); } catch { /* noop */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-claude-code-ui",
3
- "version": "1.0.72",
3
+ "version": "1.0.74",
4
4
  "description": "Claude Code-style grouped tool rows for pi with Ctrl+O/Ctrl+Shift+O previews, math rendering, and consistent built-in, MCP, and custom tool rendering",
5
5
  "keywords": [
6
6
  "pi-package",