pi-cc-extensions 0.8.28 → 0.8.30

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.
@@ -1017,7 +1017,7 @@ function fixedEditorContextScore(
1017
1017
  }
1018
1018
 
1019
1019
  /** Summary markers used by Pi and ccstyle; unlike the trailing hint, these survive truncation. */
1020
- const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿|●|✓|✗)/;
1020
+ const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿|●|✓|✗|…)/;
1021
1021
  /** fixedEditorContextScore max ≈ 100 + 2*(5+4+3+2); stop once we hit a near-perfect match. */
1022
1022
  const TOOL_CLICK_EARLY_EXIT_SCORE = 120;
1023
1023
 
@@ -1511,7 +1511,8 @@ function visibleMouseLine(tui: any, packet: SgrMousePacket): string {
1511
1511
  }
1512
1512
 
1513
1513
  function isCollapsedHintAtColumn(line: string, col: number): boolean {
1514
- const match = /(\([^()\n]* \/ click\)|click to show more)\s*$/.exec(line);
1514
+ // Rich diff hints place "click to show more" inside a trailing parenthesis.
1515
+ const match = /(\([^()\n]* \/ click\)|click to show more)(?=\)?\s*$)/.exec(line);
1515
1516
  if (!match?.[1] || col <= 0) return false;
1516
1517
  const start = visibleWidth(line.slice(0, match.index)) + 1;
1517
1518
  return col >= start && col < start + visibleWidth(match[1]);
@@ -2575,19 +2576,25 @@ function createCcstyleTool(
2575
2576
  }
2576
2577
 
2577
2578
  if (options?.isPartial) {
2578
- return new Text(theme.fg("muted", " ⎿ Pending…"), 0, 0);
2579
+ return new Text(theme.fg("muted", " ↳ Pending…"), 0, 0);
2579
2580
  }
2580
2581
 
2581
2582
  const isError = options?.isError || context?.isError;
2582
2583
  setToolVisualState(context, isError ? "error" : "success");
2583
2584
  const expanded = isToolExpanded(options, context);
2585
+ const toolCallId = context?.toolCallId;
2584
2586
  if (shouldRenderRichDiff(config.mode, toolName, Boolean(isError))) {
2585
2587
  // Pass getter so Diff indicator / wrap / limits update on the next paint
2586
2588
  // without recreating the tool result component.
2587
2589
  const richResult = renderRichToolResult(
2588
2590
  toolName,
2589
2591
  result,
2590
- { ...options, expanded },
2592
+ {
2593
+ ...options,
2594
+ expanded,
2595
+ // Live hover state for the collapsed hint row (muted → text on hover).
2596
+ isHovered: () => !!toolCallId && toolCallId === hoveredToolCallId,
2597
+ },
2591
2598
  theme,
2592
2599
  context,
2593
2600
  writeExecutionMetadata,
@@ -2629,7 +2636,6 @@ function createCcstyleTool(
2629
2636
  );
2630
2637
  }
2631
2638
  if (context?.state) context.state.ccstyleIoView = undefined;
2632
- const toolCallId = context?.toolCallId;
2633
2639
  let cachedWidth: number | undefined;
2634
2640
  let cachedLine: string | undefined;
2635
2641
  let cachedHoveredLine: string | undefined;
@@ -7,12 +7,14 @@ import piAliases from "./pi-aliases.ts";
7
7
  import piStartupHeader from "./pi-startup-header.ts";
8
8
  import sessionReference from "./session-reference.ts";
9
9
  import subagentNotification from "./subagent-notification.ts";
10
+ import workingMessage from "./working-message.ts";
10
11
 
11
12
  export default function (pi: ExtensionAPI): void {
12
13
  piAliases(pi);
13
14
  piStartupHeader(pi);
14
15
  claudeCodeStyle(pi);
15
16
  subagentNotification(pi);
17
+ workingMessage(pi);
16
18
  context(pi);
17
19
  sessionReference(pi);
18
20
  agentAutocomplete(pi);
@@ -121,6 +121,8 @@ interface DiffRenderOptions {
121
121
  previousContent?: string;
122
122
  fileExistedBeforeWrite?: boolean;
123
123
  headerLabel?: string;
124
+ /** Live hover state for the collapsed hint row (muted → text on hover). */
125
+ isHovered?: () => boolean;
124
126
  invalidate?: () => void;
125
127
  }
126
128
 
@@ -2289,6 +2291,7 @@ function applyLineLimit(
2289
2291
  * remainder so the collapse hint still reflects total hidden content.
2290
2292
  */
2291
2293
  unprocessedLogicalRows = 0,
2294
+ hovered = false,
2292
2295
  ): string[] {
2293
2296
  const limit = resolveDiffDisplayLimit(expanded, maxCollapsedLines, maxExpandedLines);
2294
2297
  const safeUnprocessed = Math.max(0, unprocessedLogicalRows);
@@ -2319,7 +2322,10 @@ function applyLineLimit(
2319
2322
  return [
2320
2323
  ...shown.map((row) => clampDiffLineToWidth(row.text, width)),
2321
2324
  renderDiffSpacerLine(width),
2322
- clampDiffLineToWidth(theme.fg(expanded ? "warning" : "muted", hintText), width),
2325
+ clampDiffLineToWidth(
2326
+ theme.fg(expanded ? "warning" : hovered ? "text" : "muted", hintText),
2327
+ width,
2328
+ ),
2323
2329
  ];
2324
2330
  }
2325
2331
 
@@ -2403,6 +2409,7 @@ function createDiffRenderCache() {
2403
2409
  let cachedExpanded: boolean | undefined;
2404
2410
  let cachedMode: DiffPresentationMode | undefined;
2405
2411
  let cachedConfigKey: string | undefined;
2412
+ let cachedHovered: boolean | undefined;
2406
2413
  let cachedLines: string[] | undefined;
2407
2414
 
2408
2415
  return {
@@ -2411,13 +2418,15 @@ function createDiffRenderCache() {
2411
2418
  expanded: boolean,
2412
2419
  mode: DiffPresentationMode,
2413
2420
  configKey: string,
2421
+ hovered: boolean,
2414
2422
  ): string[] | undefined {
2415
2423
  if (
2416
2424
  cachedLines &&
2417
2425
  cachedWidth === width &&
2418
2426
  cachedExpanded === expanded &&
2419
2427
  cachedMode === mode &&
2420
- cachedConfigKey === configKey
2428
+ cachedConfigKey === configKey &&
2429
+ cachedHovered === hovered
2421
2430
  ) {
2422
2431
  return cachedLines;
2423
2432
  }
@@ -2428,12 +2437,14 @@ function createDiffRenderCache() {
2428
2437
  expanded: boolean,
2429
2438
  mode: DiffPresentationMode,
2430
2439
  configKey: string,
2440
+ hovered: boolean,
2431
2441
  lines: string[],
2432
2442
  ): string[] {
2433
2443
  cachedWidth = width;
2434
2444
  cachedExpanded = expanded;
2435
2445
  cachedMode = mode;
2436
2446
  cachedConfigKey = configKey;
2447
+ cachedHovered = hovered;
2437
2448
  cachedLines = lines;
2438
2449
  return lines;
2439
2450
  },
@@ -2442,6 +2453,7 @@ function createDiffRenderCache() {
2442
2453
  cachedExpanded = undefined;
2443
2454
  cachedMode = undefined;
2444
2455
  cachedConfigKey = undefined;
2456
+ cachedHovered = undefined;
2445
2457
  cachedLines = undefined;
2446
2458
  },
2447
2459
  };
@@ -2500,7 +2512,8 @@ export function renderEditDiffResult(
2500
2512
  const configKey = displayConfigCacheKey(live);
2501
2513
  const safeWidth = normalizeDiffRenderWidth(width);
2502
2514
  const mode = resolveDiffPresentationMode(live, safeWidth, canRenderSplitLayout(safeWidth));
2503
- const cached = cache.get(safeWidth, options.expanded, mode, configKey);
2515
+ const hovered = options.isHovered?.() ?? false;
2516
+ const cached = cache.get(safeWidth, options.expanded, mode, configKey, hovered);
2504
2517
  if (cached) {
2505
2518
  return cached;
2506
2519
  }
@@ -2511,6 +2524,7 @@ export function renderEditDiffResult(
2511
2524
  options.expanded,
2512
2525
  mode,
2513
2526
  configKey,
2527
+ hovered,
2514
2528
  clampDiffLinesToWidth(
2515
2529
  renderSingleDiffRow(
2516
2530
  buildDiffSummaryText(parsed.stats, safeWidth),
@@ -2565,6 +2579,7 @@ export function renderEditDiffResult(
2565
2579
  parsed.stats.hunks,
2566
2580
  theme,
2567
2581
  unprocessedLogicalRows,
2582
+ hovered,
2568
2583
  );
2569
2584
  const frame = renderDiffFrameLine(safeWidth, theme);
2570
2585
  const renderedLines =
@@ -2573,7 +2588,7 @@ export function renderEditDiffResult(
2573
2588
  : [...headerRows.map((row) => row.text), ...bodyWithLimit];
2574
2589
 
2575
2590
  const clampedLines = clampDiffLinesToWidth(renderedLines, safeWidth);
2576
- return cache.set(safeWidth, options.expanded, mode, configKey, clampedLines);
2591
+ return cache.set(safeWidth, options.expanded, mode, configKey, hovered, clampedLines);
2577
2592
  },
2578
2593
  invalidate: cache.invalidate,
2579
2594
  };
@@ -2893,7 +2908,8 @@ export function renderWriteDiffResult(
2893
2908
  : resolvedMode === "split"
2894
2909
  ? "unified"
2895
2910
  : resolvedMode;
2896
- const cached = cache.get(safeWidth, options.expanded, mode, configKey);
2911
+ const hovered = options.isHovered?.() ?? false;
2912
+ const cached = cache.get(safeWidth, options.expanded, mode, configKey, hovered);
2897
2913
  if (cached) {
2898
2914
  return cached;
2899
2915
  }
@@ -2914,6 +2930,7 @@ export function renderWriteDiffResult(
2914
2930
  options.expanded,
2915
2931
  mode,
2916
2932
  configKey,
2933
+ hovered,
2917
2934
  clampDiffLinesToWidth(
2918
2935
  [header, ...renderWriteOverwriteGuardRows(overwriteGuard, safeWidth, theme)],
2919
2936
  safeWidth,
@@ -2939,6 +2956,7 @@ export function renderWriteDiffResult(
2939
2956
  options.expanded,
2940
2957
  mode,
2941
2958
  configKey,
2959
+ hovered,
2942
2960
  clampDiffLinesToWidth(summaryRows, safeWidth),
2943
2961
  );
2944
2962
  }
@@ -2989,12 +3007,13 @@ export function renderWriteDiffResult(
2989
3007
  data.hunkCount,
2990
3008
  theme,
2991
3009
  unprocessedLogicalRows,
3010
+ hovered,
2992
3011
  );
2993
3012
  const frame = renderDiffFrameLine(safeWidth, theme);
2994
3013
  const renderedLines =
2995
3014
  mode === "unified" ? [header, frame, ...bodyWithLimit, frame] : [header, ...bodyWithLimit];
2996
3015
  const finalLines = clampDiffLinesToWidth(renderedLines, safeWidth);
2997
- return cache.set(safeWidth, options.expanded, mode, configKey, finalLines);
3016
+ return cache.set(safeWidth, options.expanded, mode, configKey, hovered, finalLines);
2998
3017
  },
2999
3018
  invalidate: cache.invalidate,
3000
3019
  };
@@ -47,7 +47,12 @@ export function renderRichToolResult(
47
47
  if (toolName === "edit") {
48
48
  return renderEditDiffResult(
49
49
  result?.details,
50
- { expanded, filePath, invalidate: () => context?.invalidate?.() },
50
+ {
51
+ expanded,
52
+ filePath,
53
+ isHovered: options?.isHovered,
54
+ invalidate: () => context?.invalidate?.(),
55
+ },
51
56
  displayConfig,
52
57
  theme,
53
58
  resultText(result),
@@ -69,6 +74,7 @@ export function renderRichToolResult(
69
74
  filePath,
70
75
  previousContent: metadata.previousContent,
71
76
  fileExistedBeforeWrite: metadata.fileExistedBeforeWrite,
77
+ isHovered: options?.isHovered,
72
78
  invalidate: () => context?.invalidate?.(),
73
79
  },
74
80
  displayConfig,
@@ -71,7 +71,7 @@ export function buildCollapsedDiffHintText(
71
71
  : undefined;
72
72
 
73
73
  const candidates = [
74
- `… (${[remainingText, hiddenHunksText, "Ctrl+O to expand"].filter(Boolean).join(" • ")})`,
74
+ `… (${[remainingText, hiddenHunksText, "click to show more"].filter(Boolean).join(" • ")})`,
75
75
  `… (${[remainingText, hiddenHunksText].filter(Boolean).join(" • ")})`,
76
76
  `… (${[shortRemainingText, shortHiddenHunksText].filter(Boolean).join(" • ")})`,
77
77
  options.hiddenHunks > 0
@@ -1,5 +1,5 @@
1
1
  import { AssistantMessageComponent, ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
2
- import { Container, Spacer, truncateToWidth } from "@earendil-works/pi-tui";
2
+ import { Container, Spacer, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
3
  import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "./tool-loading-icon.ts";
4
4
  import { sanitizeToolResultText } from "./tool-result-sanitize.ts";
5
5
 
@@ -94,6 +94,42 @@ function stripLeadingStatusIcon(line: string): string {
94
94
  );
95
95
  }
96
96
 
97
+ function stripBackgroundAnsi(line: string): string {
98
+ return line.replace(/\x1b\[(?:4[0-9]|10[0-7]|48(?:(?:;|:)[0-9]+)+|49)m/g, "");
99
+ }
100
+
101
+ function stripLeadingSpaces(line: string, count: number): string {
102
+ let offset = 0;
103
+ let removed = 0;
104
+ let ansi = "";
105
+ while (offset < line.length) {
106
+ const control = line.slice(offset).match(/^\x1b\[[0-?]*[ -/]*[@-~]/)?.[0];
107
+ if (control) {
108
+ ansi += control;
109
+ offset += control.length;
110
+ continue;
111
+ }
112
+ if (removed < count && line[offset] === " ") {
113
+ removed++;
114
+ offset++;
115
+ continue;
116
+ }
117
+ break;
118
+ }
119
+ return ansi + line.slice(offset);
120
+ }
121
+
122
+ function paddedBackgroundRow(theme: any, slot: string, content: string, width: number): string {
123
+ const innerWidth = Math.max(0, width - 2);
124
+ const clipped = truncateToWidth(stripBackgroundAnsi(content), innerWidth, "");
125
+ const row = ` ${clipped}${" ".repeat(Math.max(0, innerWidth - visibleWidth(clipped)))} `;
126
+ if (typeof theme?.bg !== "function") return row;
127
+ const bgAnsi =
128
+ theme.getBgAnsi?.(slot) ?? theme.bg(slot, "").match(/^\x1b\[[0-?]*[ -/]*[@-~]/)?.[0] ?? "";
129
+ const stable = bgAnsi ? row.replace(/\x1b\[(?:0)?m/g, (reset) => reset + bgAnsi) : row;
130
+ return theme.bg(slot, stable);
131
+ }
132
+
97
133
  function oneLine(value: unknown, max = 96): string {
98
134
  const text = sanitizeToolResultText(String(value ?? ""), 4096)
99
135
  .replace(/\s+/g, " ")
@@ -275,12 +311,13 @@ export class ToolGroupComponent extends Container {
275
311
  const lines = [
276
312
  "",
277
313
  truncateToWidth(
278
- ` ${fg(overallColor, statusIcon(overall))} ${label}: ${countText}${nameList} ${hint}`,
314
+ ` ${fg(overallColor, "●")} ${label}: ${countText}${nameList} ${hint}`,
279
315
  width,
280
316
  "…",
281
317
  ),
282
318
  ];
283
319
  const total = this.children.length;
320
+ const expandedLines: string[] = [];
284
321
  for (let index = 0; index < total; index++) {
285
322
  const tool = this.children[index];
286
323
  const toolStatus = status(tool);
@@ -291,14 +328,14 @@ export class ToolGroupComponent extends Container {
291
328
  const summary = toolSummary(tool);
292
329
  lines.push(
293
330
  truncateToWidth(
294
- ` ${fg("dim", branch)} ${fg(color, "●")} ${fg("toolTitle", summary.main)}${fg("dim", summary.detail)}`,
331
+ ` ${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} ${fg("toolTitle", summary.main)}${fg("dim", summary.detail)}`,
295
332
  width,
296
333
  "…",
297
334
  ),
298
335
  );
299
336
  continue;
300
337
  }
301
- const rendered = visibleLines(tool.render(Math.max(1, width - 5)));
338
+ const rendered = visibleLines(tool.render(Math.max(1, width - 2)));
302
339
  if (rendered.length) {
303
340
  rendered[0] = stripLeadingStatusIcon(rendered[0])
304
341
  .replace(/^ +/, "")
@@ -306,14 +343,27 @@ export class ToolGroupComponent extends Container {
306
343
  }
307
344
  const childLines = rendered.length ? rendered : [toolSummary(tool).main];
308
345
  for (let lineIndex = 0; lineIndex < childLines.length; lineIndex++) {
346
+ const content =
347
+ lineIndex === 0 ? childLines[lineIndex] : stripLeadingSpaces(childLines[lineIndex], 2);
309
348
  const prefix =
310
349
  lineIndex === 0
311
- ? ` ${fg("dim", branch)} ${fg(color, "●")} `
312
- : ` ${fg("dim", continuation)} `;
313
- lines.push(truncateToWidth(prefix + childLines[lineIndex], width, ""));
350
+ ? `${fg("dim", branch)} ${fg(color, statusIcon(toolStatus))} `
351
+ : fg("dim", continuation);
352
+ expandedLines.push(prefix + content);
353
+ }
354
+ }
355
+ if (this.expanded) {
356
+ const backgroundSlot =
357
+ overall === "error"
358
+ ? "toolErrorBg"
359
+ : overall === "pending"
360
+ ? "toolPendingBg"
361
+ : "toolSuccessBg";
362
+ for (const line of expandedLines) {
363
+ lines.push(paddedBackgroundRow(theme, backgroundSlot, line, width));
314
364
  }
365
+ lines.push(paddedBackgroundRow(theme, backgroundSlot, "", width));
315
366
  }
316
- lines.push("");
317
367
  return lines;
318
368
  }
319
369
  }
@@ -0,0 +1,213 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ const REFRESH_INTERVAL_MS = 1_000;
4
+ /** Elapsed time is only shown once the turn has run this long. */
5
+ const SHOW_TIMER_AFTER_MS = 3_000;
6
+
7
+ function formatDuration(ms: number): string {
8
+ const totalSec = Math.floor(ms / 1000);
9
+ const hours = Math.floor(totalSec / 3600);
10
+ const minutes = Math.floor((totalSec % 3600) / 60);
11
+ const seconds = totalSec % 60;
12
+ if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
13
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
14
+ return `${seconds}s`;
15
+ }
16
+
17
+ function formatCount(value: number): string {
18
+ return new Intl.NumberFormat("en-US").format(value);
19
+ }
20
+
21
+ function estimateTextLength(message: any): number {
22
+ if (!Array.isArray(message?.content)) return 0;
23
+ return message.content.reduce(
24
+ (sum: number, block: any) =>
25
+ sum + (block?.type === "text" && typeof block.text === "string" ? block.text.length : 0),
26
+ 0,
27
+ );
28
+ }
29
+
30
+ function textBlockLengths(message: any): number[] {
31
+ if (!Array.isArray(message?.content)) return [];
32
+ const lengths: number[] = [];
33
+ for (let index = 0; index < message.content.length; index++) {
34
+ const block = message.content[index];
35
+ if (block?.type === "text" && typeof block.text === "string") {
36
+ lengths[index] = block.text.length;
37
+ }
38
+ }
39
+ return lengths;
40
+ }
41
+
42
+ function outputUsage(message: any): number {
43
+ const value = Number(message?.usage?.output);
44
+ return Number.isFinite(value) && value > 0 ? Math.round(value) : 0;
45
+ }
46
+
47
+ /**
48
+ * Extend Pi's footer working row while preserving its spinner and "Working...":
49
+ * `⠋ Working... (↓ 1,234 tokens · 12s)`
50
+ *
51
+ * Live tokens use the same chars/4 estimate as pi-claude-code-ui, then switch to
52
+ * provider `usage.output` whenever the stream exposes an actual count.
53
+ */
54
+ export default function (pi: ExtensionAPI): void {
55
+ let turnActive = false;
56
+ let agentStartTime = 0;
57
+ let turnStartTime = 0;
58
+ let responseLength = 0;
59
+ let responseTextBlockLengths: number[] = [];
60
+ let providerOutputTokens = 0;
61
+ let refreshTimer: ReturnType<typeof setTimeout> | null = null;
62
+ let lastMessage: string | null = null;
63
+ let activeCtx: { ui: any; hasUI: boolean } | null = null;
64
+
65
+ function tokenCount(): number {
66
+ return providerOutputTokens || Math.max(0, Math.round(responseLength / 4));
67
+ }
68
+
69
+ function setTextBlockLength(index: number, length: number): void {
70
+ const previous = responseTextBlockLengths[index] ?? 0;
71
+ responseTextBlockLengths[index] = Math.max(0, length);
72
+ responseLength = Math.max(0, responseLength + responseTextBlockLengths[index] - previous);
73
+ }
74
+
75
+ function resetResponseTracking(message?: any): void {
76
+ responseTextBlockLengths = message ? textBlockLengths(message) : [];
77
+ responseLength = message ? estimateTextLength(message) : 0;
78
+ providerOutputTokens = message ? outputUsage(message) : 0;
79
+ }
80
+
81
+ function updateProviderUsage(message: any): void {
82
+ const output = outputUsage(message);
83
+ if (output > 0) providerOutputTokens = output;
84
+ }
85
+
86
+ function buildWorkingMessage(): string {
87
+ const elapsed = Date.now() - (agentStartTime || turnStartTime);
88
+ const tokens = tokenCount();
89
+ const parts: string[] = [];
90
+ if (tokens > 0) parts.push(`↓ ${formatCount(tokens)} tokens`);
91
+ if (elapsed >= SHOW_TIMER_AFTER_MS || tokens > 0) parts.push(formatDuration(elapsed));
92
+ return parts.length ? `Working... (${parts.join(" · ")})` : "";
93
+ }
94
+
95
+ function restoreDefaultWorkingMessage(): void {
96
+ lastMessage = null;
97
+ if (!activeCtx?.hasUI) return;
98
+ try {
99
+ activeCtx.ui.setWorkingMessage();
100
+ } catch {
101
+ // Noop when the TUI is unavailable.
102
+ }
103
+ }
104
+
105
+ function syncWorkingMessage(force = false): void {
106
+ if (!activeCtx?.hasUI) return;
107
+ const next = buildWorkingMessage();
108
+ if (!next) {
109
+ if (force) restoreDefaultWorkingMessage();
110
+ return;
111
+ }
112
+ if (!force && next === lastMessage) return;
113
+ lastMessage = next;
114
+ try {
115
+ activeCtx.ui.setWorkingMessage(next);
116
+ } catch {
117
+ // Noop when the TUI is unavailable.
118
+ }
119
+ }
120
+
121
+ function scheduleRefreshTick(): void {
122
+ if (!turnActive || refreshTimer) return;
123
+ refreshTimer = setTimeout(() => {
124
+ refreshTimer = null;
125
+ syncWorkingMessage();
126
+ scheduleRefreshTick();
127
+ }, REFRESH_INTERVAL_MS);
128
+ refreshTimer.unref?.();
129
+ }
130
+
131
+ function stopRefreshLoop(): void {
132
+ if (!refreshTimer) return;
133
+ clearTimeout(refreshTimer);
134
+ refreshTimer = null;
135
+ }
136
+
137
+ function clearDisplay(): void {
138
+ stopRefreshLoop();
139
+ agentStartTime = 0;
140
+ turnStartTime = 0;
141
+ resetResponseTracking();
142
+ restoreDefaultWorkingMessage();
143
+ }
144
+
145
+ pi.on("before_agent_start", async () => {
146
+ if (!agentStartTime) agentStartTime = Date.now();
147
+ });
148
+
149
+ pi.on("turn_start", async (_event, ctx) => {
150
+ turnActive = true;
151
+ activeCtx = ctx;
152
+ turnStartTime = Date.now();
153
+ if (!agentStartTime) agentStartTime = turnStartTime;
154
+ resetResponseTracking();
155
+ syncWorkingMessage(true);
156
+ scheduleRefreshTick();
157
+ });
158
+
159
+ pi.on("message_update", async (event, ctx) => {
160
+ activeCtx = ctx;
161
+ const evt = event?.assistantMessageEvent;
162
+ if (!evt) return;
163
+
164
+ if (evt.type === "start") {
165
+ resetResponseTracking(evt.partial);
166
+ } else if (evt.type === "text_start") {
167
+ setTextBlockLength(evt.contentIndex, 0);
168
+ updateProviderUsage(evt.partial);
169
+ } else if (evt.type === "text_delta") {
170
+ setTextBlockLength(
171
+ evt.contentIndex,
172
+ (responseTextBlockLengths[evt.contentIndex] ?? 0) +
173
+ (typeof evt.delta === "string" ? evt.delta.length : 0),
174
+ );
175
+ updateProviderUsage(evt.partial);
176
+ } else if (evt.type === "text_end") {
177
+ setTextBlockLength(
178
+ evt.contentIndex,
179
+ typeof evt.content === "string" ? evt.content.length : 0,
180
+ );
181
+ updateProviderUsage(evt.partial);
182
+ } else if (evt.type === "done") {
183
+ resetResponseTracking(evt.message);
184
+ } else if (evt.type === "error") {
185
+ resetResponseTracking(evt.error);
186
+ } else {
187
+ updateProviderUsage(evt.partial);
188
+ }
189
+
190
+ syncWorkingMessage();
191
+ scheduleRefreshTick();
192
+ });
193
+
194
+ pi.on("turn_end", async (_event, ctx) => {
195
+ turnActive = false;
196
+ activeCtx = ctx;
197
+ stopRefreshLoop();
198
+ resetResponseTracking();
199
+ // No completion message: return immediately to Pi's default idle state.
200
+ restoreDefaultWorkingMessage();
201
+ });
202
+
203
+ pi.on("agent_end", async () => {
204
+ turnActive = false;
205
+ clearDisplay();
206
+ });
207
+
208
+ pi.on("session_shutdown", async () => {
209
+ turnActive = false;
210
+ clearDisplay();
211
+ activeCtx = null;
212
+ });
213
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.8.28",
3
+ "version": "0.8.30",
4
4
  "description": "A Pi productivity suite with Claude Code-style UI, structured questionnaires, context inspection, and agent/session references",
5
5
  "type": "module",
6
6
  "keywords": [