pi-claude-code-ui 1.0.69 → 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 +20 -0
- package/README.md +11 -0
- package/extensions/index.ts +181 -84
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
> [!IMPORTANT]
|
|
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
|
+
|
|
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
|
+
|
|
12
|
+
## 1.0.70 — 2026-07-17
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Live tool status dots blink again while commands stream** — partial tool rows re-arm the blink timer from both the call header and the live preview path, and `tool_execution_update` keeps the 15s stale watchdog from killing blink mid-bash.
|
|
17
|
+
- **Interrupted / resumed tools no longer blink forever** — only tools with `executionStarted` during a live agent run count as pending. History partials (resume, compaction, `/tree`, aborted runs without a toolResult) settle to a static green/dim dot and clear blink timers on `session_start`.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **No more `Running...` status row** — the blinking `●` on the tool heading is the only in-flight indicator. Live non-empty line count moves to the heading as muted `(N lines)`; the body shows only the output tail while streaming.
|
|
22
|
+
|
|
3
23
|
## 1.0.69 — 2026-07-17
|
|
4
24
|
|
|
5
25
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# pi-claude-code-ui
|
|
2
2
|
|
|
3
|
+
> [!IMPORTANT]
|
|
4
|
+
> **Package renamed in 1.0.69.** This project is now published as [`pi-claude-code-ui`](https://www.npmjs.com/package/pi-claude-code-ui) (was `pi-claude-style-tools`).
|
|
5
|
+
>
|
|
6
|
+
> Install / migrate:
|
|
7
|
+
> ```bash
|
|
8
|
+
> pi install npm:pi-claude-code-ui
|
|
9
|
+
> # or
|
|
10
|
+
> npm i pi-claude-code-ui
|
|
11
|
+
> ```
|
|
12
|
+
> See [CHANGELOG 1.0.69](./CHANGELOG.md#1069--2026-07-17) for the full release notes (status dots, bare branch connectors, and more).
|
|
13
|
+
|
|
3
14
|
Claude Code inspired tool rendering for Pi — Shiki-powered diffs, status dots, branch connectors, file icons, and configurable output modes.
|
|
4
15
|
|
|
5
16
|
## Features
|
package/extensions/index.ts
CHANGED
|
@@ -483,7 +483,13 @@ type ToolStatus = "pending" | "success" | "error";
|
|
|
483
483
|
function getToolStatusForGroup(tool: any): ToolStatus {
|
|
484
484
|
if (tool?.result?.isError) return "error";
|
|
485
485
|
if (tool?.result && tool?.isPartial !== true) return "success";
|
|
486
|
-
|
|
486
|
+
// Only in-flight tools that actually started this agent run count as pending.
|
|
487
|
+
// History rows reconstructed without a matching toolResult stay isPartial=true
|
|
488
|
+
// forever; treating them as pending made interrupted tools blink again on resume.
|
|
489
|
+
if (tool?.isPartial === true && tool?.executionStarted === true && currentAgentWorkStartMs !== undefined) {
|
|
490
|
+
return "pending";
|
|
491
|
+
}
|
|
492
|
+
return "success";
|
|
487
493
|
}
|
|
488
494
|
|
|
489
495
|
let TOOL_STATUS_SUCCESS = "\x1b[32m";
|
|
@@ -2117,6 +2123,28 @@ function patchToolExecutionBackgroundSync(): void {
|
|
|
2117
2123
|
proto[TOOL_BG_PATCH_FLAG] = true;
|
|
2118
2124
|
}
|
|
2119
2125
|
|
|
2126
|
+
function syncLiveToolRenderState(component: any): void {
|
|
2127
|
+
// updateDisplay paints call header BEFORE result. Pre-seed status + live line
|
|
2128
|
+
// count so the header's blinking ● and `(N lines)` trail stay in sync with
|
|
2129
|
+
// the partial result that is about to render underneath.
|
|
2130
|
+
const state = component?.rendererState;
|
|
2131
|
+
if (!state || typeof state !== "object") return;
|
|
2132
|
+
const ctxLike = {
|
|
2133
|
+
state,
|
|
2134
|
+
isPartial: component?.isPartial === true,
|
|
2135
|
+
executionStarted: component?.executionStarted === true,
|
|
2136
|
+
isError: component?.result?.isError === true,
|
|
2137
|
+
};
|
|
2138
|
+
syncToolCallStatus(ctxLike);
|
|
2139
|
+
if (component?.isPartial === true && component?.result) {
|
|
2140
|
+
const raw = getTextContent(component.result).replace(/\r\n/g, "\n").trimEnd();
|
|
2141
|
+
// tailLimit=0 → count-only (no line materialization) so huge bash tails stay cheap.
|
|
2142
|
+
state._liveLineCount = collectNonEmptyLines(raw, 0).total;
|
|
2143
|
+
} else if (component?.isPartial !== true) {
|
|
2144
|
+
delete state._liveLineCount;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2120
2148
|
function patchToolRenderCacheInvalidation(): void {
|
|
2121
2149
|
const proto = ToolExecutionComponent.prototype as any;
|
|
2122
2150
|
if (proto[TOOL_CACHE_PATCH_FLAG]) return;
|
|
@@ -2138,6 +2166,9 @@ function patchToolRenderCacheInvalidation(): void {
|
|
|
2138
2166
|
if (typeof original !== "function") continue;
|
|
2139
2167
|
proto[method] = function patchedToolMutation(...args: any[]) {
|
|
2140
2168
|
clearToolRenderCache(this);
|
|
2169
|
+
if (method === "updateDisplay" || method === "updateResult" || method === "invalidate") {
|
|
2170
|
+
syncLiveToolRenderState(this);
|
|
2171
|
+
}
|
|
2141
2172
|
const result = original.apply(this, args);
|
|
2142
2173
|
clearToolRenderCache(this);
|
|
2143
2174
|
return result;
|
|
@@ -2240,31 +2271,50 @@ function isBlinkOn(): boolean {
|
|
|
2240
2271
|
return Math.floor(Date.now() / 500) % 2 === 0;
|
|
2241
2272
|
}
|
|
2242
2273
|
|
|
2243
|
-
function toolHeader(tool: string, summary: string, theme: Theme, prefix = ""): string {
|
|
2274
|
+
function toolHeader(tool: string, summary: string, theme: Theme, prefix = "", trailing = ""): string {
|
|
2244
2275
|
applyThemePaletteIfNeeded(theme);
|
|
2245
2276
|
const label = theme.fg("toolTitle", theme.bold(tool));
|
|
2246
|
-
|
|
2247
|
-
|
|
2277
|
+
const body = summary
|
|
2278
|
+
? `${label} ${WRAP_MARK}${theme.fg("accent", summary)}`
|
|
2279
|
+
: label;
|
|
2280
|
+
return trailing ? `${prefix}${body}${trailing}` : `${prefix}${body}`;
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
function liveLineCountTrailing(ctx: any, theme: Theme): string {
|
|
2284
|
+
if (ctx?.isPartial !== true) return "";
|
|
2285
|
+
const count = ctx?.state?._liveLineCount;
|
|
2286
|
+
if (typeof count !== "number" || !Number.isFinite(count) || count <= 0) return "";
|
|
2287
|
+
return ` ${theme.fg("muted", `(${lineCountLabel(count)})`)}`;
|
|
2248
2288
|
}
|
|
2249
2289
|
|
|
2250
|
-
function setToolStatus(ctx: any, status: "pending" | "success" | "error"): void {
|
|
2251
|
-
ctx.state._toolStatus = status;
|
|
2290
|
+
function setToolStatus(ctx: any, status: "pending" | "success" | "error" | "idle"): void {
|
|
2291
|
+
if (ctx?.state) ctx.state._toolStatus = status;
|
|
2252
2292
|
}
|
|
2253
2293
|
|
|
2254
2294
|
function syncToolCallStatus(ctx: any): void {
|
|
2255
2295
|
if (ctx?.isPartial) {
|
|
2256
|
-
//
|
|
2257
|
-
//
|
|
2258
|
-
//
|
|
2259
|
-
//
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2296
|
+
// Blink only for tools that actually started in the current agent run.
|
|
2297
|
+
// History rebuilds (resume, compaction, /tree) leave unmatched tool calls
|
|
2298
|
+
// with isPartial=true forever and never set executionStarted — those must
|
|
2299
|
+
// render settled, not keep a pending blink alive across sessions.
|
|
2300
|
+
const agentLive = currentAgentWorkStartMs !== undefined;
|
|
2301
|
+
const started = ctx?.executionStarted === true;
|
|
2302
|
+
if (agentLive && started) {
|
|
2303
|
+
setToolStatus(ctx, "pending");
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
if (agentLive && !started) {
|
|
2307
|
+
// Args still streaming before tool_execution_start — static, no blink.
|
|
2308
|
+
setToolStatus(ctx, "idle");
|
|
2309
|
+
clearBlinkTimer(ctx);
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
setToolStatus(ctx, "success");
|
|
2313
|
+
clearBlinkTimer(ctx);
|
|
2265
2314
|
return;
|
|
2266
2315
|
}
|
|
2267
2316
|
setToolStatus(ctx, ctx.isError ? "error" : "success");
|
|
2317
|
+
clearBlinkTimer(ctx);
|
|
2268
2318
|
}
|
|
2269
2319
|
|
|
2270
2320
|
function shouldRevealCallArgs(ctx: any): boolean {
|
|
@@ -2480,9 +2530,10 @@ function getWriteWasNewFile(ctx: any, cwd: string, filePath: string, reveal = sh
|
|
|
2480
2530
|
}
|
|
2481
2531
|
|
|
2482
2532
|
function toolStatusDot(ctx: any, theme: Theme): string {
|
|
2483
|
-
const status = ctx.state?._toolStatus as "pending" | "success" | "error" | undefined;
|
|
2533
|
+
const status = ctx.state?._toolStatus as "pending" | "success" | "error" | "idle" | undefined;
|
|
2484
2534
|
if (status === "success") return `${themeStatusDot(theme, "success")} `;
|
|
2485
2535
|
if (status === "error") return `${themeStatusDot(theme, "error")} `;
|
|
2536
|
+
if (status === "idle") return `${themeStatusDot(theme, "dim")} `;
|
|
2486
2537
|
return `${blinkDot(ctx, theme)} `;
|
|
2487
2538
|
}
|
|
2488
2539
|
|
|
@@ -2539,9 +2590,10 @@ function indentBranchBlock(block: string): string {
|
|
|
2539
2590
|
|
|
2540
2591
|
const MAX_BLINKING_TOOLS = 5;
|
|
2541
2592
|
const BLINK_INTERVAL_MS = 500;
|
|
2542
|
-
//
|
|
2543
|
-
//
|
|
2544
|
-
//
|
|
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.
|
|
2545
2597
|
const BLINK_STALE_TIMEOUT_MS = 15000;
|
|
2546
2598
|
let _lastBlinkActivity = 0;
|
|
2547
2599
|
|
|
@@ -2581,6 +2633,18 @@ function updateBlinkActiveStates(): void {
|
|
|
2581
2633
|
}
|
|
2582
2634
|
}
|
|
2583
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
|
+
|
|
2584
2648
|
function _scheduleGlobalBlinkTimer(): void {
|
|
2585
2649
|
if (_globalBlinkTimer) return;
|
|
2586
2650
|
const intervalMs = getBlinkIntervalMs();
|
|
@@ -2591,15 +2655,13 @@ function _scheduleGlobalBlinkTimer(): void {
|
|
|
2591
2655
|
updateBlinkActiveStates();
|
|
2592
2656
|
return;
|
|
2593
2657
|
}
|
|
2594
|
-
//
|
|
2595
|
-
//
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
_blinkContexts.clear();
|
|
2602
|
-
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();
|
|
2603
2665
|
return;
|
|
2604
2666
|
}
|
|
2605
2667
|
_globalBlinkPhase = !_globalBlinkPhase;
|
|
@@ -2624,12 +2686,18 @@ function setupBlinkTimer(ctx: any): void {
|
|
|
2624
2686
|
const invalidate = typeof ctx?.invalidate === "function" ? () => safeInvalidate(ctx) : () => {};
|
|
2625
2687
|
const existing = _blinkContexts.get(key);
|
|
2626
2688
|
if (existing) {
|
|
2627
|
-
// Already tracked —
|
|
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 ●.
|
|
2628
2693
|
existing.invalidate = invalidate;
|
|
2694
|
+
markBlinkActivity();
|
|
2695
|
+
_scheduleGlobalBlinkTimer();
|
|
2629
2696
|
return;
|
|
2630
2697
|
}
|
|
2631
2698
|
_blinkContexts.set(key, { key, order: ++_blinkOrder, invalidate });
|
|
2632
2699
|
key._blinkActive = false;
|
|
2700
|
+
markBlinkActivity();
|
|
2633
2701
|
updateBlinkActiveStates();
|
|
2634
2702
|
_stopGlobalBlinkTimerIfEmpty();
|
|
2635
2703
|
_scheduleGlobalBlinkTimer();
|
|
@@ -2651,6 +2719,11 @@ function pendingToolChromeColor(theme: Theme): "dim" | "muted" | "thinkingText"
|
|
|
2651
2719
|
}
|
|
2652
2720
|
|
|
2653
2721
|
function blinkDot(ctx: any, theme: Theme): string {
|
|
2722
|
+
// Only true in-flight tools arm the blink timer. Idle partials (args still
|
|
2723
|
+
// streaming, or history rows left isPartial without a result) stay static.
|
|
2724
|
+
if (ctx?.state?._toolStatus !== "pending") {
|
|
2725
|
+
return themeStatusDot(theme, "dim");
|
|
2726
|
+
}
|
|
2654
2727
|
setupBlinkTimer(ctx);
|
|
2655
2728
|
const key = getBlinkKey(ctx);
|
|
2656
2729
|
// Claude Code: solid filled circle that either shows or fully disappears —
|
|
@@ -4724,6 +4797,10 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
4724
4797
|
// message history, so /new starts fresh while /resume picks up past prompts
|
|
4725
4798
|
// and the original session start time. Resetting here is what lets /new
|
|
4726
4799
|
// clear the totals (Math.min / Math.max seeding alone could never lower them).
|
|
4800
|
+
// Also drop the live-agent marker so history partials rebuilt during resume
|
|
4801
|
+
// never look "in flight" and re-arm blink timers.
|
|
4802
|
+
currentAgentWorkStartMs = undefined;
|
|
4803
|
+
currentAssistantMessageStartMs = undefined;
|
|
4727
4804
|
sessionStartMs = undefined;
|
|
4728
4805
|
userTurnCount = 0;
|
|
4729
4806
|
});
|
|
@@ -4847,7 +4924,10 @@ function renderGenericToolCall(name: string, args: any, theme: Theme, ctx: any):
|
|
|
4847
4924
|
ctx.state._openAiPatchFiles = [];
|
|
4848
4925
|
const sp = (path: string) => shortPath(ctx.cwd ?? process.cwd(), path);
|
|
4849
4926
|
const summary = stableCallSummary(ctx, "_callSummary", () => summarizeGenericToolCall(name, args, theme, sp));
|
|
4850
|
-
return makeText(
|
|
4927
|
+
return makeText(
|
|
4928
|
+
ctx.lastComponent,
|
|
4929
|
+
toolHeader(genericToolLabel(name), summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme)),
|
|
4930
|
+
);
|
|
4851
4931
|
}
|
|
4852
4932
|
|
|
4853
4933
|
function renderGenericToolResult(name: string, result: any, options: any, theme: Theme, ctx: any): Text {
|
|
@@ -4903,17 +4983,20 @@ function lineCountLabel(count: number): string {
|
|
|
4903
4983
|
|
|
4904
4984
|
function runningPreviewBlock(
|
|
4905
4985
|
result: any,
|
|
4906
|
-
|
|
4986
|
+
_statusText: string,
|
|
4907
4987
|
expanded: boolean,
|
|
4908
4988
|
theme: Theme,
|
|
4909
4989
|
ctx: any,
|
|
4910
4990
|
options: { lines?: string[]; totalLineCount?: number; styleLine?: (line: string) => string; tail?: boolean } = {},
|
|
4911
4991
|
): string {
|
|
4912
|
-
|
|
4992
|
+
// Keep the header status dot blinking while partial output streams. Call/result
|
|
4993
|
+
// renderers share rendererState, so setupBlinkTimer here re-arms the same key
|
|
4994
|
+
// the call header uses — but only when this tool actually started executing.
|
|
4995
|
+
syncToolCallStatus(ctx);
|
|
4996
|
+
if (ctx?.state?._toolStatus === "pending") setupBlinkTimer(ctx);
|
|
4997
|
+
else clearBlinkTimer(ctx);
|
|
4998
|
+
|
|
4913
4999
|
const limit = liveToolPreviewLimit();
|
|
4914
|
-
if (!liveToolPreviewEnabled() || limit <= 0) {
|
|
4915
|
-
return withBranch(statusText, theme);
|
|
4916
|
-
}
|
|
4917
5000
|
let lines: string[];
|
|
4918
5001
|
let totalLineCount: number;
|
|
4919
5002
|
if (options.lines) {
|
|
@@ -4926,8 +5009,13 @@ function runningPreviewBlock(
|
|
|
4926
5009
|
lines = collected.lines;
|
|
4927
5010
|
totalLineCount = collected.total;
|
|
4928
5011
|
}
|
|
4929
|
-
|
|
4930
|
-
|
|
5012
|
+
// Line count lives on the tool heading (via liveLineCountTrailing); keep it in
|
|
5013
|
+
// renderer state so the next renderCall pass can pick it up.
|
|
5014
|
+
if (ctx?.state) ctx.state._liveLineCount = totalLineCount;
|
|
5015
|
+
|
|
5016
|
+
if (!liveToolPreviewEnabled() || limit <= 0 || totalLineCount === 0) {
|
|
5017
|
+
// No status row — the blinking ● on the header is the only running indicator.
|
|
5018
|
+
return "";
|
|
4931
5019
|
}
|
|
4932
5020
|
|
|
4933
5021
|
const styleLine = options.styleLine ?? ((line: string) => theme.fg("dim", line || " "));
|
|
@@ -4942,7 +5030,7 @@ function runningPreviewBlock(
|
|
|
4942
5030
|
if (options.tail && !expanded && totalLineCount > previewSource.length) {
|
|
4943
5031
|
preview = `${theme.fg("muted", `... (${totalLineCount - previewSource.length} earlier lines${toolOutputDetailHint(theme, expanded, true)})`)}\n${preview}`;
|
|
4944
5032
|
}
|
|
4945
|
-
return withBranch(
|
|
5033
|
+
return withBranch(preview, theme);
|
|
4946
5034
|
}
|
|
4947
5035
|
|
|
4948
5036
|
function buildPersistentBashPreview(lines: string[], theme: Theme): string {
|
|
@@ -5317,7 +5405,7 @@ function renderApplyPatchCall(args: any, theme: Theme, ctx: any, sp: (path: stri
|
|
|
5317
5405
|
syncToolCallStatus(ctx);
|
|
5318
5406
|
const patchText = getStringArg(args, "patchText", "patch_text");
|
|
5319
5407
|
const summary = stableCallSummary(ctx, "_callSummary", () => summarizeOpenAiToolCall("apply_patch", args, theme, sp));
|
|
5320
|
-
const hdr = toolHeader("Apply Patch", summary, theme, toolStatusDot(ctx, theme));
|
|
5408
|
+
const hdr = toolHeader("Apply Patch", summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme));
|
|
5321
5409
|
|
|
5322
5410
|
if (!ctx.argsComplete) return makeText(ctx.lastComponent, hdr);
|
|
5323
5411
|
const preview = getCachedApplyPatchPreview(patchText, sp, ctx);
|
|
@@ -6150,7 +6238,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6150
6238
|
const line =
|
|
6151
6239
|
theme.fg("customMessageLabel", `\x1b[1m[skill]\x1b[22m `) +
|
|
6152
6240
|
theme.fg("customMessageText", skillName);
|
|
6153
|
-
return makeText(ctx.lastComponent, `${toolStatusDot(ctx, theme)}${line}`);
|
|
6241
|
+
return makeText(ctx.lastComponent, `${toolStatusDot(ctx, theme)}${line}${liveLineCountTrailing(ctx, theme)}`);
|
|
6154
6242
|
}
|
|
6155
6243
|
const summary = stableCallSummary(ctx, "_callSummary", () => {
|
|
6156
6244
|
let value = sp(args.path ?? "");
|
|
@@ -6162,7 +6250,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
6162
6250
|
}
|
|
6163
6251
|
return value;
|
|
6164
6252
|
});
|
|
6165
|
-
return makeText(
|
|
6253
|
+
return makeText(
|
|
6254
|
+
ctx.lastComponent,
|
|
6255
|
+
toolHeader("Read", summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme)),
|
|
6256
|
+
);
|
|
6166
6257
|
},
|
|
6167
6258
|
renderResult(result, { expanded, isPartial }, theme, ctx) {
|
|
6168
6259
|
if (isPartial) {
|
|
@@ -6197,7 +6288,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
6197
6288
|
const rewrite = ensureRtkRewriteForContext(ctx, args);
|
|
6198
6289
|
const summary = stableCallSummary(ctx, "_callSummary", () => summarizeText(args.command, 72));
|
|
6199
6290
|
const rtkBadge = rewrite ? theme.fg("muted", " (RTK)") : "";
|
|
6200
|
-
return makeText(
|
|
6291
|
+
return makeText(
|
|
6292
|
+
ctx.lastComponent,
|
|
6293
|
+
toolHeader("Bash", `${summary}${rtkBadge}`, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme)),
|
|
6294
|
+
);
|
|
6201
6295
|
},
|
|
6202
6296
|
renderResult(result, { expanded, isPartial }, theme, ctx) {
|
|
6203
6297
|
const details = result.details as BashToolDetails | undefined;
|
|
@@ -6205,13 +6299,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
6205
6299
|
const output = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
6206
6300
|
if (isPartial) {
|
|
6207
6301
|
const preview = collectNonEmptyLines(output, expanded ? undefined : liveToolPreviewLimit());
|
|
6208
|
-
const running = runningPreviewBlock(result,
|
|
6302
|
+
const running = runningPreviewBlock(result, "", expanded, theme, ctx, {
|
|
6209
6303
|
lines: preview.lines,
|
|
6210
6304
|
totalLineCount: preview.total,
|
|
6211
6305
|
styleLine: (line) => theme.fg("dim", line),
|
|
6212
6306
|
tail: true,
|
|
6213
6307
|
});
|
|
6214
|
-
const withRewrite = expanded && rewrite
|
|
6308
|
+
const withRewrite = expanded && rewrite
|
|
6309
|
+
? [running, withBranch(formatRtkRewriteDetails(rewrite, theme), theme)].filter(Boolean).join("\n")
|
|
6310
|
+
: running;
|
|
6215
6311
|
return makeText(ctx.lastComponent, withRewrite);
|
|
6216
6312
|
}
|
|
6217
6313
|
// Collapsed: only keep the live-preview tail (or nothing). Expanded: full list.
|
|
@@ -6255,7 +6351,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
6255
6351
|
if (args.path) value += ` in ${args.path}`;
|
|
6256
6352
|
return value;
|
|
6257
6353
|
});
|
|
6258
|
-
return makeText(
|
|
6354
|
+
return makeText(
|
|
6355
|
+
ctx.lastComponent,
|
|
6356
|
+
toolHeader("Grep", summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme)),
|
|
6357
|
+
);
|
|
6259
6358
|
},
|
|
6260
6359
|
renderResult(result, { expanded, isPartial }, theme, ctx) {
|
|
6261
6360
|
if (isPartial) {
|
|
@@ -6292,7 +6391,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
6292
6391
|
if (args.path) value += ` in ${args.path}`;
|
|
6293
6392
|
return value;
|
|
6294
6393
|
});
|
|
6295
|
-
return makeText(
|
|
6394
|
+
return makeText(
|
|
6395
|
+
ctx.lastComponent,
|
|
6396
|
+
toolHeader("Find", summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme)),
|
|
6397
|
+
);
|
|
6296
6398
|
},
|
|
6297
6399
|
renderResult(result, { expanded, isPartial }, theme, ctx) {
|
|
6298
6400
|
if (isPartial) {
|
|
@@ -6336,7 +6438,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
6336
6438
|
renderCall(args, theme, ctx) {
|
|
6337
6439
|
syncToolCallStatus(ctx);
|
|
6338
6440
|
const summary = stableCallSummary(ctx, "_callSummary", () => sp(args.path ?? "."));
|
|
6339
|
-
return makeText(
|
|
6441
|
+
return makeText(
|
|
6442
|
+
ctx.lastComponent,
|
|
6443
|
+
toolHeader("List", summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme)),
|
|
6444
|
+
);
|
|
6340
6445
|
},
|
|
6341
6446
|
renderResult(result, { expanded, isPartial }, theme, ctx) {
|
|
6342
6447
|
if (isPartial) {
|
|
@@ -6411,12 +6516,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
6411
6516
|
const base = sp(fp);
|
|
6412
6517
|
return shouldRevealCallArgs(ctx) ? `${base} ${theme.fg("muted", `(${lineCount(args.content ?? "")} lines)`)}` : base;
|
|
6413
6518
|
}, revealSummary);
|
|
6414
|
-
const hdr = toolHeader(label, summary, theme, toolStatusDot(ctx, theme));
|
|
6519
|
+
const hdr = toolHeader(label, summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme));
|
|
6415
6520
|
return makeText(ctx.lastComponent, hdr);
|
|
6416
6521
|
},
|
|
6417
6522
|
renderResult(result, { expanded, isPartial }, theme, ctx) {
|
|
6418
6523
|
if (isPartial) {
|
|
6419
|
-
return makeText(ctx.lastComponent, runningPreviewBlock(result,
|
|
6524
|
+
return makeText(ctx.lastComponent, runningPreviewBlock(result, "", expanded, theme, ctx));
|
|
6420
6525
|
}
|
|
6421
6526
|
clearBlinkTimer(ctx);
|
|
6422
6527
|
setToolStatus(ctx, ctx.isError ? "error" : "success");
|
|
@@ -6534,7 +6639,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6534
6639
|
const revealSummary = shouldRevealCallArgs(ctx) || (!!fp && hasOwnArg(args, "edits"));
|
|
6535
6640
|
const summary = stableCallSummary(ctx, "_callSummary", () => shouldRevealCallArgs(ctx) && operations.length > 1 ? `${sp(fp)} ${theme.fg("muted", `(${operations.length} edits)`)}` : sp(fp), revealSummary);
|
|
6536
6641
|
syncToolCallStatus(ctx);
|
|
6537
|
-
const hdr = toolHeader("Edit", summary, theme, ` ${toolStatusDot(ctx, theme)}
|
|
6642
|
+
const hdr = toolHeader("Edit", summary, theme, ` ${toolStatusDot(ctx, theme)}`, liveLineCountTrailing(ctx, theme));
|
|
6538
6643
|
if (!(ctx.argsComplete && operations.length > 0)) return makeText(ctx.lastComponent, hdr);
|
|
6539
6644
|
const diffWidth = branchDiffWidth();
|
|
6540
6645
|
const key = `edit:${fp}:${hashText(operations.map((edit) => `${edit.oldText}\u0000${edit.newText}`).join("\u0001"))}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
|
|
@@ -6620,7 +6725,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
6620
6725
|
syncToolCallStatus(ctx);
|
|
6621
6726
|
ctx.state._openAiPatchFiles = [];
|
|
6622
6727
|
const summary = stableCallSummary(ctx, "_callSummary", () => summarizeOpenAiToolCall(name, args, theme, sp));
|
|
6623
|
-
return makeText(
|
|
6728
|
+
return makeText(
|
|
6729
|
+
ctx.lastComponent,
|
|
6730
|
+
toolHeader(label, summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme)),
|
|
6731
|
+
);
|
|
6624
6732
|
},
|
|
6625
6733
|
renderResult(result: any, { expanded, isPartial }: any, theme: Theme, ctx: any) {
|
|
6626
6734
|
if (name === "apply_patch") return renderApplyPatchResult(result, isPartial, theme, ctx);
|
|
@@ -6677,51 +6785,40 @@ export default function (pi: ExtensionAPI) {
|
|
|
6677
6785
|
registerMcpToolOverrides();
|
|
6678
6786
|
});
|
|
6679
6787
|
|
|
6680
|
-
// Streaming activity keeps the blink timer alive
|
|
6681
|
-
//
|
|
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.
|
|
6682
6791
|
pi.on("turn_start", async () => { markBlinkActivity(); });
|
|
6683
6792
|
pi.on("message_start", async () => { markBlinkActivity(); });
|
|
6684
6793
|
pi.on("message_update", async () => { markBlinkActivity(); });
|
|
6685
6794
|
pi.on("tool_execution_start", async () => { markBlinkActivity(); });
|
|
6795
|
+
// Partial tool output is the main long-running signal (bash streams for minutes).
|
|
6796
|
+
pi.on("tool_execution_update", async () => { markBlinkActivity(); });
|
|
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.
|
|
6686
6803
|
pi.on("agent_end", async () => {
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
_globalBlinkTimer = null;
|
|
6695
|
-
}
|
|
6804
|
+
queueMicrotask(() => {
|
|
6805
|
+
if (currentAgentWorkStartMs !== undefined) {
|
|
6806
|
+
markBlinkActivity();
|
|
6807
|
+
return;
|
|
6808
|
+
}
|
|
6809
|
+
_clearAllBlinkContexts();
|
|
6810
|
+
});
|
|
6696
6811
|
});
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
// re-highlighting the same diffs every turn was a free long-chat tax.
|
|
6701
|
-
// Full clear still happens on session_shutdown / theme rebind.
|
|
6702
|
-
pi.on("turn_end", async () => {
|
|
6703
|
-
for (const entry of _blinkContexts.values()) {
|
|
6704
|
-
entry.key._blinkActive = false;
|
|
6705
|
-
}
|
|
6706
|
-
_blinkContexts.clear();
|
|
6707
|
-
if (_globalBlinkTimer) {
|
|
6708
|
-
clearTimeout(_globalBlinkTimer);
|
|
6709
|
-
_globalBlinkTimer = null;
|
|
6710
|
-
}
|
|
6812
|
+
// Session rebuild (resume/reload/fork) must not leave history partials blinking.
|
|
6813
|
+
pi.on("session_start", async () => {
|
|
6814
|
+
_clearAllBlinkContexts();
|
|
6711
6815
|
});
|
|
6712
6816
|
pi.on("session_shutdown", async () => {
|
|
6713
|
-
|
|
6714
|
-
entry.key._blinkActive = false;
|
|
6715
|
-
}
|
|
6716
|
-
_blinkContexts.clear();
|
|
6817
|
+
_clearAllBlinkContexts();
|
|
6717
6818
|
clearRtkRewriteState();
|
|
6718
6819
|
WRITE_EXISTED_BEFORE.clear();
|
|
6719
6820
|
clearHighlightCache();
|
|
6720
6821
|
invalidateThemePaletteCache();
|
|
6721
6822
|
bumpToolBranchVisualEpoch();
|
|
6722
|
-
if (_globalBlinkTimer) {
|
|
6723
|
-
clearTimeout(_globalBlinkTimer);
|
|
6724
|
-
_globalBlinkTimer = null;
|
|
6725
|
-
}
|
|
6726
6823
|
});
|
|
6727
6824
|
}
|
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.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",
|