pi-claude-code-ui 1.0.73 → 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,6 +3,12 @@
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
+
6
12
  ## 1.0.73 — 2026-07-18
7
13
 
8
14
  ### Fixed
@@ -559,20 +559,33 @@ function themeStatusDot(theme: Theme, colorKey: "success" | "error" | "dim" | "m
559
559
  return theme.fg(colorKey, `${STATUS_DOT_BOLD}${STATUS_DOT_FILLED}`);
560
560
  }
561
561
 
562
- function agentBreatheDot(theme: Theme): string {
562
+ function agentBreatheGlyphRaw(): string {
563
563
  // Always exactly one display cell — matches ordinary tool dots, keeps titles aligned.
564
- const glyph = AGENT_BREATHE_GLYPHS[_globalBlinkPhaseIndex % AGENT_BREATHE_LEN];
564
+ return AGENT_BREATHE_GLYPHS[_globalBlinkPhaseIndex % AGENT_BREATHE_LEN];
565
+ }
566
+
567
+ function paintAgentBreatheDot(colorAnsi: string = TOOL_STATUS_SUCCESS): string {
568
+ const glyph = agentBreatheGlyphRaw();
565
569
  if (glyph === " ") return " ";
566
570
  // Bold only on the largest frame so weight changes without shifting the cell.
567
571
  const bold = glyph === "●" ? STATUS_DOT_BOLD : "";
572
+ return `${colorAnsi}${bold}${glyph}${TRANSPARENT_RESET}`;
573
+ }
574
+
575
+ function agentBreatheDot(theme: Theme): string {
576
+ const glyph = agentBreatheGlyphRaw();
577
+ if (glyph === " ") return " ";
578
+ const bold = glyph === "●" ? STATUS_DOT_BOLD : "";
568
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-claude-code-ui",
3
- "version": "1.0.73",
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",