pi-claude-code-ui 1.0.70 → 1.0.72
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 +12 -0
- package/extensions/index.ts +75 -60
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
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.72 — 2026-07-18
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
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.
|
|
11
|
+
|
|
12
|
+
## 1.0.71 — 2026-07-18
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Long-running tools no longer freeze their status dots** — the 15s stale watchdog was treating quiet tools (no `tool_execution_update`) as leaked and killing the blink timer mid-run. While an agent is live the timer now heartbeats itself; stale cleanup only runs after the agent finishes. Also stop clearing blink state on `turn_end` (turns end before tools run).
|
|
17
|
+
|
|
6
18
|
## 1.0.70 — 2026-07-17
|
|
7
19
|
|
|
8
20
|
### Fixed
|
package/extensions/index.ts
CHANGED
|
@@ -434,10 +434,16 @@ function isToolExecutionLike(value: unknown): value is { toolName: string; toolC
|
|
|
434
434
|
return typeof candidate.toolName === "string" && typeof candidate.toolCallId === "string";
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
+
const AGENT_FAMILY_TOOL_NAMES = new Set(["Agent", "Agents", "get_subagent_result", "steer_subagent"]);
|
|
438
|
+
|
|
439
|
+
function isAgentFamilyToolName(name: unknown): boolean {
|
|
440
|
+
return typeof name === "string" && AGENT_FAMILY_TOOL_NAMES.has(name);
|
|
441
|
+
}
|
|
442
|
+
|
|
437
443
|
function shouldIndentToolExecution(value: unknown): boolean {
|
|
438
444
|
if (!value || typeof value !== "object") return false;
|
|
439
445
|
const toolName = (value as Record<string, unknown>).toolName;
|
|
440
|
-
return
|
|
446
|
+
return isAgentFamilyToolName(toolName);
|
|
441
447
|
}
|
|
442
448
|
|
|
443
449
|
function isTerminalImageLine(line: string): boolean {
|
|
@@ -541,9 +547,11 @@ function getToolGroupOverallStatus(tools: any[]): ToolStatus {
|
|
|
541
547
|
|
|
542
548
|
// Claude Code: solid filled circle that is either fully present or fully gone
|
|
543
549
|
// while pending — never a hollow outlined ○. Classic ● + bold is the sweet
|
|
544
|
-
// spot
|
|
550
|
+
// spot for ordinary tools. Agent-family tools use a breathing size cycle.
|
|
545
551
|
const STATUS_DOT_FILLED = "●";
|
|
546
552
|
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
555
|
|
|
548
556
|
function paintStatusDot(colorAnsi: string): string {
|
|
549
557
|
return `${colorAnsi}${STATUS_DOT_BOLD}${STATUS_DOT_FILLED}${TRANSPARENT_RESET}`;
|
|
@@ -554,6 +562,12 @@ function themeStatusDot(theme: Theme, colorKey: "success" | "error" | "dim" | "m
|
|
|
554
562
|
return theme.fg(colorKey, `${STATUS_DOT_BOLD}${STATUS_DOT_FILLED}`);
|
|
555
563
|
}
|
|
556
564
|
|
|
565
|
+
function agentBreatheDot(theme: Theme): string {
|
|
566
|
+
const glyph = AGENT_BREATHE_GLYPHS[_globalBlinkPhaseIndex % AGENT_BREATHE_GLYPHS.length];
|
|
567
|
+
if (glyph === " ") return " ";
|
|
568
|
+
return theme.fg("success", `${STATUS_DOT_BOLD}${glyph}`);
|
|
569
|
+
}
|
|
570
|
+
|
|
557
571
|
function groupStatusLight(status: ToolStatus): string {
|
|
558
572
|
const color = status === "success" ? TOOL_STATUS_SUCCESS : status === "error" ? TOOL_STATUS_ERROR : TOOL_STATUS_PENDING;
|
|
559
573
|
if (status === "pending") {
|
|
@@ -2590,9 +2604,10 @@ function indentBranchBlock(block: string): string {
|
|
|
2590
2604
|
|
|
2591
2605
|
const MAX_BLINKING_TOOLS = 5;
|
|
2592
2606
|
const BLINK_INTERVAL_MS = 500;
|
|
2593
|
-
//
|
|
2594
|
-
//
|
|
2595
|
-
//
|
|
2607
|
+
// Safety net ONLY for leaked entries after the agent has stopped. Quiet
|
|
2608
|
+
// long-running tools (sleep, sparse builds, waiting on network) legitimately
|
|
2609
|
+
// emit no tool_execution_update for minutes — treating that silence as "stale"
|
|
2610
|
+
// is what made their ● freeze mid-run.
|
|
2596
2611
|
const BLINK_STALE_TIMEOUT_MS = 15000;
|
|
2597
2612
|
let _lastBlinkActivity = 0;
|
|
2598
2613
|
|
|
@@ -2605,6 +2620,9 @@ type BlinkEntry = { key: any; order: number; invalidate: () => void };
|
|
|
2605
2620
|
const _blinkContexts = new Map<any, BlinkEntry>();
|
|
2606
2621
|
let _globalBlinkTimer: ReturnType<typeof setTimeout> | null = null;
|
|
2607
2622
|
let _blinkOrder = 0;
|
|
2623
|
+
// Shared phase for all blinkers. Ordinary tools use even/odd (on/off ●).
|
|
2624
|
+
// Agent-family tools map the index onto a 6-step size breath cycle.
|
|
2625
|
+
let _globalBlinkPhaseIndex = 0;
|
|
2608
2626
|
let _globalBlinkPhase = true;
|
|
2609
2627
|
|
|
2610
2628
|
function getBlinkIntervalMs(): number {
|
|
@@ -2632,6 +2650,18 @@ function updateBlinkActiveStates(): void {
|
|
|
2632
2650
|
}
|
|
2633
2651
|
}
|
|
2634
2652
|
|
|
2653
|
+
function _clearAllBlinkContexts(): void {
|
|
2654
|
+
for (const entry of _blinkContexts.values()) {
|
|
2655
|
+
try { entry.key._blinkActive = false; } catch { /* noop */ }
|
|
2656
|
+
}
|
|
2657
|
+
_blinkContexts.clear();
|
|
2658
|
+
if (_globalBlinkTimer) {
|
|
2659
|
+
clearTimeout(_globalBlinkTimer);
|
|
2660
|
+
_globalBlinkTimer = null;
|
|
2661
|
+
}
|
|
2662
|
+
updateBlinkActiveStates();
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2635
2665
|
function _scheduleGlobalBlinkTimer(): void {
|
|
2636
2666
|
if (_globalBlinkTimer) return;
|
|
2637
2667
|
const intervalMs = getBlinkIntervalMs();
|
|
@@ -2642,18 +2672,17 @@ function _scheduleGlobalBlinkTimer(): void {
|
|
|
2642
2672
|
updateBlinkActiveStates();
|
|
2643
2673
|
return;
|
|
2644
2674
|
}
|
|
2645
|
-
//
|
|
2646
|
-
//
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
_blinkContexts.clear();
|
|
2653
|
-
updateBlinkActiveStates();
|
|
2675
|
+
// While an agent run is live, quiet tools are still in flight — keep blinking.
|
|
2676
|
+
// Heartbeat here so sparse/no-output commands never look "stale".
|
|
2677
|
+
if (currentAgentWorkStartMs !== undefined) {
|
|
2678
|
+
markBlinkActivity();
|
|
2679
|
+
} else if (_lastBlinkActivity && Date.now() - _lastBlinkActivity > BLINK_STALE_TIMEOUT_MS) {
|
|
2680
|
+
// Agent already finished; leftover entries are leaks. Stop the re-render storm.
|
|
2681
|
+
_clearAllBlinkContexts();
|
|
2654
2682
|
return;
|
|
2655
2683
|
}
|
|
2656
|
-
|
|
2684
|
+
_globalBlinkPhaseIndex = (_globalBlinkPhaseIndex + 1) % AGENT_BREATHE_GLYPHS.length;
|
|
2685
|
+
_globalBlinkPhase = _globalBlinkPhaseIndex % 2 === 0;
|
|
2657
2686
|
for (const entry of getBlinkingEntries()) {
|
|
2658
2687
|
try { entry.invalidate(); } catch { /* noop */ }
|
|
2659
2688
|
}
|
|
@@ -2675,12 +2704,18 @@ function setupBlinkTimer(ctx: any): void {
|
|
|
2675
2704
|
const invalidate = typeof ctx?.invalidate === "function" ? () => safeInvalidate(ctx) : () => {};
|
|
2676
2705
|
const existing = _blinkContexts.get(key);
|
|
2677
2706
|
if (existing) {
|
|
2678
|
-
// Already tracked —
|
|
2707
|
+
// Already tracked — refresh invalidate + ensure the global timer is alive.
|
|
2708
|
+
// If a prior watchdog/pass stopped the timer without removing this entry
|
|
2709
|
+
// (or the timer simply died), a quiet long-running tool would otherwise
|
|
2710
|
+
// stay registered forever with a frozen ●.
|
|
2679
2711
|
existing.invalidate = invalidate;
|
|
2712
|
+
markBlinkActivity();
|
|
2713
|
+
_scheduleGlobalBlinkTimer();
|
|
2680
2714
|
return;
|
|
2681
2715
|
}
|
|
2682
2716
|
_blinkContexts.set(key, { key, order: ++_blinkOrder, invalidate });
|
|
2683
2717
|
key._blinkActive = false;
|
|
2718
|
+
markBlinkActivity();
|
|
2684
2719
|
updateBlinkActiveStates();
|
|
2685
2720
|
_stopGlobalBlinkTimerIfEmpty();
|
|
2686
2721
|
_scheduleGlobalBlinkTimer();
|
|
@@ -2709,9 +2744,13 @@ function blinkDot(ctx: any, theme: Theme): string {
|
|
|
2709
2744
|
}
|
|
2710
2745
|
setupBlinkTimer(ctx);
|
|
2711
2746
|
const key = getBlinkKey(ctx);
|
|
2747
|
+
if (key?._blinkActive !== true) return " ";
|
|
2748
|
+
// Agent-family tools breathe through sizes; ordinary tools still on/off ●.
|
|
2749
|
+
if (ctx?.state?._agentBreathe === true) {
|
|
2750
|
+
return agentBreatheDot(theme);
|
|
2751
|
+
}
|
|
2712
2752
|
// Claude Code: solid filled circle that either shows or fully disappears —
|
|
2713
2753
|
// never a hollow outlined ○ in the off phase.
|
|
2714
|
-
if (key?._blinkActive !== true) return " ";
|
|
2715
2754
|
return _globalBlinkPhase ? themeStatusDot(theme, "success") : " ";
|
|
2716
2755
|
}
|
|
2717
2756
|
|
|
@@ -4905,6 +4944,8 @@ function genericToolLabel(name: string): string {
|
|
|
4905
4944
|
function renderGenericToolCall(name: string, args: any, theme: Theme, ctx: any): Text {
|
|
4906
4945
|
syncToolCallStatus(ctx);
|
|
4907
4946
|
ctx.state._openAiPatchFiles = [];
|
|
4947
|
+
// Agent / subagent tools get a size-breathing pending marker, not on/off ●.
|
|
4948
|
+
if (isAgentFamilyToolName(name)) ctx.state._agentBreathe = true;
|
|
4908
4949
|
const sp = (path: string) => shortPath(ctx.cwd ?? process.cwd(), path);
|
|
4909
4950
|
const summary = stableCallSummary(ctx, "_callSummary", () => summarizeGenericToolCall(name, args, theme, sp));
|
|
4910
4951
|
return makeText(
|
|
@@ -6768,66 +6809,40 @@ export default function (pi: ExtensionAPI) {
|
|
|
6768
6809
|
registerMcpToolOverrides();
|
|
6769
6810
|
});
|
|
6770
6811
|
|
|
6771
|
-
// Streaming activity keeps the blink timer alive
|
|
6772
|
-
//
|
|
6812
|
+
// Streaming activity keeps the blink timer alive. Do NOT clear blink contexts
|
|
6813
|
+
// on turn_end — a turn ends when the assistant message finishes, BEFORE its
|
|
6814
|
+
// tools run. agent_end / agent_settled are the real "work finished" signals.
|
|
6773
6815
|
pi.on("turn_start", async () => { markBlinkActivity(); });
|
|
6774
6816
|
pi.on("message_start", async () => { markBlinkActivity(); });
|
|
6775
6817
|
pi.on("message_update", async () => { markBlinkActivity(); });
|
|
6776
6818
|
pi.on("tool_execution_start", async () => { markBlinkActivity(); });
|
|
6777
6819
|
// Partial tool output is the main long-running signal (bash streams for minutes).
|
|
6778
|
-
// Without this, the 15s stale watchdog can kill blink while a quiet command runs.
|
|
6779
6820
|
pi.on("tool_execution_update", async () => { markBlinkActivity(); });
|
|
6780
6821
|
pi.on("tool_execution_end", async () => { markBlinkActivity(); });
|
|
6822
|
+
// agent_end fires when a low-level run finishes (tools for that assistant message
|
|
6823
|
+
// are done). registerThinkingLabels clears currentAgentWorkStartMs on the same
|
|
6824
|
+
// event; defer so we only wipe blink state once the work marker is gone.
|
|
6825
|
+
// Do not use agent_settled here — older peer types don't include it, and the
|
|
6826
|
+
// live-agent heartbeat already keeps quiet long tools blinking until agent_end.
|
|
6781
6827
|
pi.on("agent_end", async () => {
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
_globalBlinkTimer = null;
|
|
6790
|
-
}
|
|
6791
|
-
});
|
|
6792
|
-
|
|
6793
|
-
// Safety net: clear blink timers after a turn's tools finish.
|
|
6794
|
-
// Keep Shiki hlCache across turns — it's an LRU (CACHE_LIMIT=48) and
|
|
6795
|
-
// re-highlighting the same diffs every turn was a free long-chat tax.
|
|
6796
|
-
// Full clear still happens on session_shutdown / theme rebind.
|
|
6797
|
-
pi.on("turn_end", async () => {
|
|
6798
|
-
for (const entry of _blinkContexts.values()) {
|
|
6799
|
-
entry.key._blinkActive = false;
|
|
6800
|
-
}
|
|
6801
|
-
_blinkContexts.clear();
|
|
6802
|
-
if (_globalBlinkTimer) {
|
|
6803
|
-
clearTimeout(_globalBlinkTimer);
|
|
6804
|
-
_globalBlinkTimer = null;
|
|
6805
|
-
}
|
|
6828
|
+
queueMicrotask(() => {
|
|
6829
|
+
if (currentAgentWorkStartMs !== undefined) {
|
|
6830
|
+
markBlinkActivity();
|
|
6831
|
+
return;
|
|
6832
|
+
}
|
|
6833
|
+
_clearAllBlinkContexts();
|
|
6834
|
+
});
|
|
6806
6835
|
});
|
|
6807
6836
|
// Session rebuild (resume/reload/fork) must not leave history partials blinking.
|
|
6808
6837
|
pi.on("session_start", async () => {
|
|
6809
|
-
|
|
6810
|
-
entry.key._blinkActive = false;
|
|
6811
|
-
}
|
|
6812
|
-
_blinkContexts.clear();
|
|
6813
|
-
if (_globalBlinkTimer) {
|
|
6814
|
-
clearTimeout(_globalBlinkTimer);
|
|
6815
|
-
_globalBlinkTimer = null;
|
|
6816
|
-
}
|
|
6838
|
+
_clearAllBlinkContexts();
|
|
6817
6839
|
});
|
|
6818
6840
|
pi.on("session_shutdown", async () => {
|
|
6819
|
-
|
|
6820
|
-
entry.key._blinkActive = false;
|
|
6821
|
-
}
|
|
6822
|
-
_blinkContexts.clear();
|
|
6841
|
+
_clearAllBlinkContexts();
|
|
6823
6842
|
clearRtkRewriteState();
|
|
6824
6843
|
WRITE_EXISTED_BEFORE.clear();
|
|
6825
6844
|
clearHighlightCache();
|
|
6826
6845
|
invalidateThemePaletteCache();
|
|
6827
6846
|
bumpToolBranchVisualEpoch();
|
|
6828
|
-
if (_globalBlinkTimer) {
|
|
6829
|
-
clearTimeout(_globalBlinkTimer);
|
|
6830
|
-
_globalBlinkTimer = null;
|
|
6831
|
-
}
|
|
6832
6847
|
});
|
|
6833
6848
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-claude-code-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.72",
|
|
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",
|