pi-cc-extensions 0.8.29 → 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.
- package/extensions/claude-code-style.ts +11 -5
- package/extensions/index.ts +2 -0
- package/extensions/tool-diff/diff-renderer.ts +25 -6
- package/extensions/tool-diff/index.ts +7 -1
- package/extensions/tool-diff/line-width-safety.ts +1 -1
- package/extensions/tool-grouping.ts +58 -8
- package/extensions/working-message.ts +213 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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", "
|
|
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
|
-
{
|
|
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;
|
package/extensions/index.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
|
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
|
-
{
|
|
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, "
|
|
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,
|
|
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,
|
|
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 -
|
|
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
|
-
?
|
|
312
|
-
:
|
|
313
|
-
|
|
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