pi-claude-code-ui 1.0.70 → 1.0.71

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.71 — 2026-07-18
7
+
8
+ ### Fixed
9
+
10
+ - **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).
11
+
6
12
  ## 1.0.70 — 2026-07-17
7
13
 
8
14
  ### Fixed
@@ -2590,9 +2590,10 @@ function indentBranchBlock(block: string): string {
2590
2590
 
2591
2591
  const MAX_BLINKING_TOOLS = 5;
2592
2592
  const BLINK_INTERVAL_MS = 500;
2593
- // If no streaming event occurs for this long, assume blink entries are leaked
2594
- // (a tool completed without clearing, or the turn ended without turn_end) and
2595
- // stop the re-render storm. Prevents idle TUI crashes and unbounded memory growth.
2593
+ // Safety net ONLY for leaked entries after the agent has stopped. Quiet
2594
+ // long-running tools (sleep, sparse builds, waiting on network) legitimately
2595
+ // emit no tool_execution_update for minutes — treating that silence as "stale"
2596
+ // is what made their ● freeze mid-run.
2596
2597
  const BLINK_STALE_TIMEOUT_MS = 15000;
2597
2598
  let _lastBlinkActivity = 0;
2598
2599
 
@@ -2632,6 +2633,18 @@ function updateBlinkActiveStates(): void {
2632
2633
  }
2633
2634
  }
2634
2635
 
2636
+ function _clearAllBlinkContexts(): void {
2637
+ for (const entry of _blinkContexts.values()) {
2638
+ try { entry.key._blinkActive = false; } catch { /* noop */ }
2639
+ }
2640
+ _blinkContexts.clear();
2641
+ if (_globalBlinkTimer) {
2642
+ clearTimeout(_globalBlinkTimer);
2643
+ _globalBlinkTimer = null;
2644
+ }
2645
+ updateBlinkActiveStates();
2646
+ }
2647
+
2635
2648
  function _scheduleGlobalBlinkTimer(): void {
2636
2649
  if (_globalBlinkTimer) return;
2637
2650
  const intervalMs = getBlinkIntervalMs();
@@ -2642,15 +2655,13 @@ function _scheduleGlobalBlinkTimer(): void {
2642
2655
  updateBlinkActiveStates();
2643
2656
  return;
2644
2657
  }
2645
- // Watchdog: if nothing has streamed for a while, the remaining entries are
2646
- // almost certainly leaked. Clear them and stop re-arming so we don't keep
2647
- // forcing full TUI re-renders while idle (render-assertion crash / OOM).
2648
- if (_lastBlinkActivity && Date.now() - _lastBlinkActivity > BLINK_STALE_TIMEOUT_MS) {
2649
- for (const entry of _blinkContexts.values()) {
2650
- try { entry.key._blinkActive = false; } catch { /* noop */ }
2651
- }
2652
- _blinkContexts.clear();
2653
- updateBlinkActiveStates();
2658
+ // While an agent run is live, quiet tools are still in flight — keep blinking.
2659
+ // Heartbeat here so sparse/no-output commands never look "stale".
2660
+ if (currentAgentWorkStartMs !== undefined) {
2661
+ markBlinkActivity();
2662
+ } else if (_lastBlinkActivity && Date.now() - _lastBlinkActivity > BLINK_STALE_TIMEOUT_MS) {
2663
+ // Agent already finished; leftover entries are leaks. Stop the re-render storm.
2664
+ _clearAllBlinkContexts();
2654
2665
  return;
2655
2666
  }
2656
2667
  _globalBlinkPhase = !_globalBlinkPhase;
@@ -2675,12 +2686,18 @@ function setupBlinkTimer(ctx: any): void {
2675
2686
  const invalidate = typeof ctx?.invalidate === "function" ? () => safeInvalidate(ctx) : () => {};
2676
2687
  const existing = _blinkContexts.get(key);
2677
2688
  if (existing) {
2678
- // Already tracked — just refresh the invalidate fn, skip expensive recalc
2689
+ // Already tracked — refresh invalidate + ensure the global timer is alive.
2690
+ // If a prior watchdog/pass stopped the timer without removing this entry
2691
+ // (or the timer simply died), a quiet long-running tool would otherwise
2692
+ // stay registered forever with a frozen ●.
2679
2693
  existing.invalidate = invalidate;
2694
+ markBlinkActivity();
2695
+ _scheduleGlobalBlinkTimer();
2680
2696
  return;
2681
2697
  }
2682
2698
  _blinkContexts.set(key, { key, order: ++_blinkOrder, invalidate });
2683
2699
  key._blinkActive = false;
2700
+ markBlinkActivity();
2684
2701
  updateBlinkActiveStates();
2685
2702
  _stopGlobalBlinkTimerIfEmpty();
2686
2703
  _scheduleGlobalBlinkTimer();
@@ -6768,66 +6785,40 @@ export default function (pi: ExtensionAPI) {
6768
6785
  registerMcpToolOverrides();
6769
6786
  });
6770
6787
 
6771
- // Streaming activity keeps the blink timer alive; agent_end clears leaked
6772
- // entries so the re-render loop can't keep running while idle.
6788
+ // Streaming activity keeps the blink timer alive. Do NOT clear blink contexts
6789
+ // on turn_end — a turn ends when the assistant message finishes, BEFORE its
6790
+ // tools run. agent_end / agent_settled are the real "work finished" signals.
6773
6791
  pi.on("turn_start", async () => { markBlinkActivity(); });
6774
6792
  pi.on("message_start", async () => { markBlinkActivity(); });
6775
6793
  pi.on("message_update", async () => { markBlinkActivity(); });
6776
6794
  pi.on("tool_execution_start", async () => { markBlinkActivity(); });
6777
6795
  // 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
6796
  pi.on("tool_execution_update", async () => { markBlinkActivity(); });
6780
6797
  pi.on("tool_execution_end", async () => { markBlinkActivity(); });
6798
+ // agent_end fires when a low-level run finishes (tools for that assistant message
6799
+ // are done). registerThinkingLabels clears currentAgentWorkStartMs on the same
6800
+ // event; defer so we only wipe blink state once the work marker is gone.
6801
+ // Do not use agent_settled here — older peer types don't include it, and the
6802
+ // live-agent heartbeat already keeps quiet long tools blinking until agent_end.
6781
6803
  pi.on("agent_end", async () => {
6782
- markBlinkActivity();
6783
- for (const entry of _blinkContexts.values()) {
6784
- entry.key._blinkActive = false;
6785
- }
6786
- _blinkContexts.clear();
6787
- if (_globalBlinkTimer) {
6788
- clearTimeout(_globalBlinkTimer);
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
- }
6804
+ queueMicrotask(() => {
6805
+ if (currentAgentWorkStartMs !== undefined) {
6806
+ markBlinkActivity();
6807
+ return;
6808
+ }
6809
+ _clearAllBlinkContexts();
6810
+ });
6806
6811
  });
6807
6812
  // Session rebuild (resume/reload/fork) must not leave history partials blinking.
6808
6813
  pi.on("session_start", async () => {
6809
- for (const entry of _blinkContexts.values()) {
6810
- entry.key._blinkActive = false;
6811
- }
6812
- _blinkContexts.clear();
6813
- if (_globalBlinkTimer) {
6814
- clearTimeout(_globalBlinkTimer);
6815
- _globalBlinkTimer = null;
6816
- }
6814
+ _clearAllBlinkContexts();
6817
6815
  });
6818
6816
  pi.on("session_shutdown", async () => {
6819
- for (const entry of _blinkContexts.values()) {
6820
- entry.key._blinkActive = false;
6821
- }
6822
- _blinkContexts.clear();
6817
+ _clearAllBlinkContexts();
6823
6818
  clearRtkRewriteState();
6824
6819
  WRITE_EXISTED_BEFORE.clear();
6825
6820
  clearHighlightCache();
6826
6821
  invalidateThemePaletteCache();
6827
6822
  bumpToolBranchVisualEpoch();
6828
- if (_globalBlinkTimer) {
6829
- clearTimeout(_globalBlinkTimer);
6830
- _globalBlinkTimer = null;
6831
- }
6832
6823
  });
6833
6824
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-claude-code-ui",
3
- "version": "1.0.70",
3
+ "version": "1.0.71",
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",