pi-cc-extensions 0.8.25 → 0.8.27
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 +209 -94
- package/extensions/context.ts +48 -30
- package/extensions/index.ts +20 -0
- package/extensions/subagent-notification.ts +114 -0
- package/extensions/tool-diff/diff-renderer.ts +95 -55
- package/extensions/tool-diff/index.ts +2 -1
- package/extensions/tool-diff/shiki-highlight.ts +90 -0
- package/extensions/tool-grouping.ts +444 -0
- package/extensions/tool-loading-icon.ts +9 -0
- package/extensions/tool-result-sanitize.ts +2 -2
- package/package.json +3 -8
|
@@ -6,6 +6,12 @@ import {
|
|
|
6
6
|
type CompactStyleMode,
|
|
7
7
|
} from "./compact-style.ts";
|
|
8
8
|
import { showTextPreview } from "./context.ts";
|
|
9
|
+
import {
|
|
10
|
+
installToolGrouping,
|
|
11
|
+
ToolGroupComponent,
|
|
12
|
+
type ToolGroupingHooks,
|
|
13
|
+
} from "./tool-grouping.ts";
|
|
14
|
+
import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "./tool-loading-icon.ts";
|
|
9
15
|
import { sanitizeToolResultText } from "./tool-result-sanitize.ts";
|
|
10
16
|
import {
|
|
11
17
|
DEFAULT_TOOL_DISPLAY_CONFIG,
|
|
@@ -27,6 +33,7 @@ import {
|
|
|
27
33
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
28
34
|
import { homedir } from "node:os";
|
|
29
35
|
import { join } from "node:path";
|
|
36
|
+
import { inspect } from "node:util";
|
|
30
37
|
|
|
31
38
|
/**
|
|
32
39
|
* Claude Code Style for pi.
|
|
@@ -238,13 +245,30 @@ function oneLine(value: unknown, max = 72): string {
|
|
|
238
245
|
}
|
|
239
246
|
|
|
240
247
|
function rawTextFromResult(result: any): string {
|
|
241
|
-
|
|
242
|
-
|
|
248
|
+
return Array.isArray(result?.content)
|
|
249
|
+
? result.content
|
|
250
|
+
.filter((item: any) => item?.type === "text")
|
|
251
|
+
.map((item: any) => String(item.text ?? ""))
|
|
252
|
+
.join("\n")
|
|
253
|
+
: "";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function detailsFromResult(result: any): string {
|
|
257
|
+
if (result?.details === undefined) return "";
|
|
258
|
+
const details =
|
|
259
|
+
typeof result.details === "string"
|
|
260
|
+
? result.details
|
|
261
|
+
: inspect(result.details, { depth: 8, breakLength: 100, compact: false });
|
|
262
|
+
return sanitizeToolResultText(details, 16_384);
|
|
243
263
|
}
|
|
244
264
|
|
|
245
|
-
function textFromResult(result: any): string {
|
|
265
|
+
function textFromResult(result: any, expanded = false): string {
|
|
246
266
|
// Compact previews only need short text; bound sanitize work.
|
|
247
|
-
|
|
267
|
+
const content = sanitizeToolResultText(rawTextFromResult(result), 16_384);
|
|
268
|
+
const details = detailsFromResult(result);
|
|
269
|
+
if (!content) return details;
|
|
270
|
+
if (!expanded || !details || details === content) return content;
|
|
271
|
+
return `${content}\nDetails:\n${details}`;
|
|
248
272
|
}
|
|
249
273
|
|
|
250
274
|
export function outputLineCount(result: any): number {
|
|
@@ -267,23 +291,6 @@ function toolIcon(_name: string): string {
|
|
|
267
291
|
return "●";
|
|
268
292
|
}
|
|
269
293
|
|
|
270
|
-
// Match the braille loader shown to the left of pi's "Working..." row.
|
|
271
|
-
// Every frame is one cell wide, so tool titles remain horizontally stable.
|
|
272
|
-
const WORKING_LOADER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
273
|
-
const TOOL_PENDING_FRAMES: Record<string, string[]> = {
|
|
274
|
-
read: WORKING_LOADER_FRAMES,
|
|
275
|
-
bash: WORKING_LOADER_FRAMES,
|
|
276
|
-
edit: WORKING_LOADER_FRAMES,
|
|
277
|
-
write: WORKING_LOADER_FRAMES,
|
|
278
|
-
find: WORKING_LOADER_FRAMES,
|
|
279
|
-
grep: WORKING_LOADER_FRAMES,
|
|
280
|
-
ls: WORKING_LOADER_FRAMES,
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
function animationFrame(frames: string[], intervalMs = 120): string {
|
|
284
|
-
return frames[Math.floor(Date.now() / intervalMs) % frames.length] ?? frames[0] ?? "";
|
|
285
|
-
}
|
|
286
|
-
|
|
287
294
|
const activeAnimationContexts = new Set<any>();
|
|
288
295
|
let sharedAnimationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
289
296
|
|
|
@@ -308,7 +315,7 @@ function clearAllAnimations() {
|
|
|
308
315
|
}
|
|
309
316
|
}
|
|
310
317
|
|
|
311
|
-
function scheduleAnimation(context: any, intervalMs =
|
|
318
|
+
function scheduleAnimation(context: any, intervalMs = TOOL_LOADING_INTERVAL_MS) {
|
|
312
319
|
const state = (context.state ??= {});
|
|
313
320
|
if (state.ccstyleAnimationScheduled) return;
|
|
314
321
|
state.ccstyleAnimationScheduled = true;
|
|
@@ -326,8 +333,8 @@ function scheduleAnimation(context: any, intervalMs = 80) {
|
|
|
326
333
|
}
|
|
327
334
|
}
|
|
328
335
|
|
|
329
|
-
function pendingIcon(
|
|
330
|
-
return
|
|
336
|
+
function pendingIcon(_name: string): string {
|
|
337
|
+
return toolLoadingIcon();
|
|
331
338
|
}
|
|
332
339
|
|
|
333
340
|
type ToolVisualState = "pending" | "success" | "error";
|
|
@@ -434,7 +441,7 @@ const EXPANDED_TOOL_IO_VIEW_GENERATION = Symbol("ccstyle-expanded-tool-io-view")
|
|
|
434
441
|
* Expanded tool body with clear Input / Output sections (Grok Build–style).
|
|
435
442
|
*
|
|
436
443
|
* Visual frame:
|
|
437
|
-
*
|
|
444
|
+
* ├ Input [show more]
|
|
438
445
|
* │ path: src/a.ts
|
|
439
446
|
* │
|
|
440
447
|
* └ Output [show more]
|
|
@@ -538,7 +545,7 @@ export class ExpandedToolIoView {
|
|
|
538
545
|
|
|
539
546
|
const theme = this.theme;
|
|
540
547
|
const safeWidth = Math.max(1, Math.floor(width));
|
|
541
|
-
const rail = "
|
|
548
|
+
const rail = " │ ";
|
|
542
549
|
const railWidth = visibleWidth(rail);
|
|
543
550
|
const bodyWidth = toolViewportWidth(safeWidth);
|
|
544
551
|
const contentWidth = Math.max(1, bodyWidth - railWidth);
|
|
@@ -547,12 +554,12 @@ export class ExpandedToolIoView {
|
|
|
547
554
|
this.truncated = { input: false, output: false };
|
|
548
555
|
|
|
549
556
|
const pushHeader = (
|
|
550
|
-
corner: "
|
|
557
|
+
corner: "├" | "└",
|
|
551
558
|
label: string,
|
|
552
559
|
section: ToolIoSection,
|
|
553
560
|
showMore: boolean,
|
|
554
561
|
) => {
|
|
555
|
-
const mark = theme.fg("dim", `
|
|
562
|
+
const mark = theme.fg("dim", ` ${corner} `);
|
|
556
563
|
const title = theme.fg(
|
|
557
564
|
"accent",
|
|
558
565
|
typeof theme.bold === "function" ? theme.bold(label) : label,
|
|
@@ -563,12 +570,13 @@ export class ExpandedToolIoView {
|
|
|
563
570
|
lines.push(truncateToWidth(mark + title + more, safeWidth, ""));
|
|
564
571
|
};
|
|
565
572
|
|
|
566
|
-
const pushRailLine = (styledContent: string) => {
|
|
567
|
-
|
|
573
|
+
const pushRailLine = (styledContent: string, continued = true) => {
|
|
574
|
+
const prefix = continued ? rail : " ";
|
|
575
|
+
lines.push(truncateToWidth(theme.fg("dim", prefix) + styledContent, safeWidth, ""));
|
|
568
576
|
};
|
|
569
577
|
|
|
570
578
|
const pushBlankRail = () => {
|
|
571
|
-
lines.push(truncateToWidth(theme.fg("dim", "
|
|
579
|
+
lines.push(truncateToWidth(theme.fg("dim", " │"), safeWidth, ""));
|
|
572
580
|
};
|
|
573
581
|
|
|
574
582
|
/** Style `key: value` input rows — dim keys, readable values. */
|
|
@@ -576,16 +584,16 @@ export class ExpandedToolIoView {
|
|
|
576
584
|
const match = rawLine.match(/^([A-Za-z_][\w.-]*)(:\s*)(.*)$/);
|
|
577
585
|
if (!match) return theme.fg("muted", rawLine);
|
|
578
586
|
const [, key, sep, rest] = match;
|
|
579
|
-
return theme.fg("dim", key + sep) + theme.fg("
|
|
587
|
+
return theme.fg("dim", key + sep) + theme.fg("muted", rest ?? "");
|
|
580
588
|
};
|
|
581
589
|
|
|
582
590
|
const pushBody = (
|
|
583
591
|
body: string,
|
|
584
|
-
opts: { input?: boolean; limit: number },
|
|
592
|
+
opts: { input?: boolean; limit: number; continued?: boolean },
|
|
585
593
|
): boolean /* truncated */ => {
|
|
586
594
|
const raw = body.replace(/\t/g, " ").replace(/\n+$/, "");
|
|
587
595
|
if (!raw.trim()) {
|
|
588
|
-
pushRailLine(theme.fg("dim", "(empty)"));
|
|
596
|
+
pushRailLine(theme.fg("dim", "(empty)"), opts.continued);
|
|
589
597
|
return false;
|
|
590
598
|
}
|
|
591
599
|
const sourceLines = raw.split("\n");
|
|
@@ -600,11 +608,11 @@ export class ExpandedToolIoView {
|
|
|
600
608
|
// theme/wrap measurements disagree slightly.
|
|
601
609
|
const truncated = wrapped.length > opts.limit || sourceLines.length > opts.limit;
|
|
602
610
|
const visible = truncated ? wrapped.slice(0, Math.min(opts.limit, wrapped.length)) : wrapped;
|
|
603
|
-
for (const line of visible) pushRailLine(line);
|
|
611
|
+
for (const line of visible) pushRailLine(line, opts.continued);
|
|
604
612
|
if (truncated) {
|
|
605
613
|
const hidden = Math.max(0, wrapped.length - visible.length);
|
|
606
614
|
if (hidden > 0) {
|
|
607
|
-
pushRailLine(theme.fg("dim", `… +${hidden} more lines`));
|
|
615
|
+
pushRailLine(theme.fg("dim", `… +${hidden} more lines`), opts.continued);
|
|
608
616
|
}
|
|
609
617
|
}
|
|
610
618
|
return truncated;
|
|
@@ -628,16 +636,20 @@ export class ExpandedToolIoView {
|
|
|
628
636
|
|
|
629
637
|
if (hasInput) {
|
|
630
638
|
this.truncated.input = inputWouldTruncate;
|
|
631
|
-
pushHeader("
|
|
632
|
-
pushBody(this.inputBody, {
|
|
639
|
+
pushHeader("├", "Input", "input", inputWouldTruncate);
|
|
640
|
+
pushBody(this.inputBody, {
|
|
641
|
+
input: true,
|
|
642
|
+
limit: this.maxInputLines,
|
|
643
|
+
continued: true,
|
|
644
|
+
});
|
|
633
645
|
pushBlankRail();
|
|
634
646
|
this.truncated.output = outputWouldTruncate;
|
|
635
647
|
pushHeader("└", "Output", "output", outputWouldTruncate);
|
|
636
|
-
pushBody(outputText, { limit: this.maxOutputLines });
|
|
648
|
+
pushBody(outputText, { limit: this.maxOutputLines, continued: false });
|
|
637
649
|
} else {
|
|
638
650
|
this.truncated.output = outputWouldTruncate;
|
|
639
|
-
pushHeader("
|
|
640
|
-
pushBody(outputText, { limit: this.maxOutputLines });
|
|
651
|
+
pushHeader("└", "Output", "output", outputWouldTruncate);
|
|
652
|
+
pushBody(outputText, { limit: this.maxOutputLines, continued: false });
|
|
641
653
|
}
|
|
642
654
|
|
|
643
655
|
this.cachedWidth = width;
|
|
@@ -682,7 +694,7 @@ function bodyExceedsLineLimit(
|
|
|
682
694
|
if (asInput) {
|
|
683
695
|
const match = source.match(/^([A-Za-z_][\w.-]*)(:\s*)(.*)$/);
|
|
684
696
|
styled = match
|
|
685
|
-
? theme.fg("dim", match[1] + match[2]) + theme.fg("
|
|
697
|
+
? theme.fg("dim", match[1] + match[2]) + theme.fg("muted", match[3] ?? "")
|
|
686
698
|
: theme.fg("muted", source);
|
|
687
699
|
} else {
|
|
688
700
|
styled = theme.fg(bodyColor, source);
|
|
@@ -695,16 +707,16 @@ function bodyExceedsLineLimit(
|
|
|
695
707
|
}
|
|
696
708
|
|
|
697
709
|
export function renderCollapsedToolResult(body: string, collapsedHint = ""): string {
|
|
698
|
-
return `
|
|
710
|
+
return ` ↳ ${body}${collapsedHint}`;
|
|
699
711
|
}
|
|
700
712
|
|
|
701
713
|
export function renderCollapsedToolResultToWidth(
|
|
702
714
|
body: string,
|
|
703
715
|
collapsedHint: string,
|
|
704
716
|
width: number,
|
|
717
|
+
prefix = " ↳ ",
|
|
705
718
|
): string {
|
|
706
719
|
const previewWidth = toolViewportWidth(width);
|
|
707
|
-
const prefix = " ↳ ";
|
|
708
720
|
const bodyWidth = Math.max(1, previewWidth - visibleWidth(prefix) - visibleWidth(collapsedHint));
|
|
709
721
|
return truncateToWidth(
|
|
710
722
|
prefix + middleTruncateToWidth(body, bodyWidth) + collapsedHint,
|
|
@@ -879,6 +891,7 @@ let assistantMessageActive = false;
|
|
|
879
891
|
let scrollButtonSyncScheduled = false;
|
|
880
892
|
let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
|
|
881
893
|
let hoveredToolCallId: string | null = null;
|
|
894
|
+
let hoveredToolGroup: ToolGroupComponent | null = null;
|
|
882
895
|
|
|
883
896
|
function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
|
|
884
897
|
const pattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
@@ -1004,7 +1017,7 @@ function fixedEditorContextScore(
|
|
|
1004
1017
|
}
|
|
1005
1018
|
|
|
1006
1019
|
/** Summary markers used by Pi and ccstyle; unlike the trailing hint, these survive truncation. */
|
|
1007
|
-
const COLLAPSED_TOOL_SUMMARY = /^\s*(
|
|
1020
|
+
const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿|●|✓|✗)/;
|
|
1008
1021
|
/** fixedEditorContextScore max ≈ 100 + 2*(5+4+3+2); stop once we hit a near-perfect match. */
|
|
1009
1022
|
const TOOL_CLICK_EARLY_EXIT_SCORE = 120;
|
|
1010
1023
|
|
|
@@ -1498,7 +1511,7 @@ function visibleMouseLine(tui: any, packet: SgrMousePacket): string {
|
|
|
1498
1511
|
}
|
|
1499
1512
|
|
|
1500
1513
|
function isCollapsedHintAtColumn(line: string, col: number): boolean {
|
|
1501
|
-
const match = /(\([^()\n]* \/ click\))\s*$/.exec(line);
|
|
1514
|
+
const match = /(\([^()\n]* \/ click\)|click to show more)\s*$/.exec(line);
|
|
1502
1515
|
if (!match?.[1] || col <= 0) return false;
|
|
1503
1516
|
const start = visibleWidth(line.slice(0, match.index)) + 1;
|
|
1504
1517
|
return col >= start && col < start + visibleWidth(match[1]);
|
|
@@ -1544,10 +1557,19 @@ function setHoveredToolIo(view: ExpandedToolIoView | null, section: ToolIoSectio
|
|
|
1544
1557
|
return true;
|
|
1545
1558
|
}
|
|
1546
1559
|
|
|
1560
|
+
function setHoveredToolGroup(group: ToolGroupComponent | null): boolean {
|
|
1561
|
+
if (group === hoveredToolGroup) return false;
|
|
1562
|
+
hoveredToolGroup?.setHintHovered(false);
|
|
1563
|
+
hoveredToolGroup = group;
|
|
1564
|
+
group?.setHintHovered(true);
|
|
1565
|
+
return true;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1547
1568
|
function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
1548
1569
|
if ((packet.code & 32) === 0 || packet.final !== "M") return;
|
|
1549
1570
|
const visibleLine = visibleMouseLine(tui, packet);
|
|
1550
1571
|
let nextToolCallId: string | null = null;
|
|
1572
|
+
let nextGroup: ToolGroupComponent | null = null;
|
|
1551
1573
|
let nextIoView: ExpandedToolIoView | null = null;
|
|
1552
1574
|
let nextIoSection: ToolIoSection | null = null;
|
|
1553
1575
|
|
|
@@ -1557,6 +1579,7 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
|
1557
1579
|
) {
|
|
1558
1580
|
const hit = findToolAtScreenRow(tui, packet.row);
|
|
1559
1581
|
nextToolCallId = hit && !hit.component.expanded ? hit.component.toolCallId : null;
|
|
1582
|
+
nextGroup = hit?.component instanceof ToolGroupComponent ? hit.component : null;
|
|
1560
1583
|
} else if (visibleLine.includes(SHOW_MORE_LABEL)) {
|
|
1561
1584
|
const hit = findToolAtScreenRow(tui, packet.row);
|
|
1562
1585
|
const view = hit && hit.component.expanded ? resolveIoViewFromTool(hit.component) : null;
|
|
@@ -1570,11 +1593,13 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
|
1570
1593
|
|
|
1571
1594
|
const changed = nextToolCallId !== hoveredToolCallId;
|
|
1572
1595
|
hoveredToolCallId = nextToolCallId;
|
|
1573
|
-
if (setHoveredToolIo(nextIoView, nextIoSection) ||
|
|
1596
|
+
if (setHoveredToolIo(nextIoView, nextIoSection) || setHoveredToolGroup(nextGroup) || changed)
|
|
1597
|
+
tui.requestRender?.();
|
|
1574
1598
|
}
|
|
1575
1599
|
|
|
1576
1600
|
function isExpandedToolCardHit(hit: ToolRenderHit, width: number): boolean {
|
|
1577
1601
|
if (!Boolean(hit.component.expanded)) return false;
|
|
1602
|
+
if (hit.component instanceof ToolGroupComponent) return true;
|
|
1578
1603
|
const box = hit.component.contentBox;
|
|
1579
1604
|
if (!box || !Array.isArray(hit.component.children) || !hit.component.children.includes(box)) {
|
|
1580
1605
|
return false;
|
|
@@ -1588,11 +1613,11 @@ function isExpandedToolCardHit(hit: ToolRenderHit, width: number): boolean {
|
|
|
1588
1613
|
|
|
1589
1614
|
function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
1590
1615
|
const line = visibleMouseLine(tui, packet);
|
|
1591
|
-
const collapsedHint = isCollapsedHintAtColumn(line, packet.col);
|
|
1592
1616
|
const hasShowMore = line.includes(SHOW_MORE_LABEL);
|
|
1593
1617
|
|
|
1594
1618
|
const hit = findToolAtScreenRow(tui, packet.row);
|
|
1595
1619
|
if (!hit) return false;
|
|
1620
|
+
const collapsedHint = isCollapsedHintAtColumn(line, packet.col);
|
|
1596
1621
|
const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
|
|
1597
1622
|
if (Boolean(hit.component.expanded)) {
|
|
1598
1623
|
if (hasShowMore && tryOpenToolIoShowMore(tui, packet, hit)) return true;
|
|
@@ -1600,6 +1625,7 @@ function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
1600
1625
|
const previousHeight = renderComponentTree(hit.component, width).length;
|
|
1601
1626
|
hit.component.setExpanded(false);
|
|
1602
1627
|
hoveredToolCallId = null;
|
|
1628
|
+
setHoveredToolGroup(null);
|
|
1603
1629
|
setHoveredToolIo(null, null);
|
|
1604
1630
|
hit.component.invalidate?.();
|
|
1605
1631
|
const nextHeight = renderComponentTree(hit.component, width).length;
|
|
@@ -1611,6 +1637,7 @@ function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
|
1611
1637
|
|
|
1612
1638
|
hit.component.setExpanded(true);
|
|
1613
1639
|
hoveredToolCallId = null;
|
|
1640
|
+
setHoveredToolGroup(null);
|
|
1614
1641
|
setHoveredToolIo(null, null);
|
|
1615
1642
|
hit.component.invalidate?.();
|
|
1616
1643
|
tui.requestRender?.();
|
|
@@ -1798,6 +1825,7 @@ function teardownToolMouseInteraction(): void {
|
|
|
1798
1825
|
toolMouseInputUnsubscribe?.();
|
|
1799
1826
|
toolMouseInputUnsubscribe = null;
|
|
1800
1827
|
hoveredToolCallId = null;
|
|
1828
|
+
setHoveredToolGroup(null);
|
|
1801
1829
|
setHoveredToolIo(null, null);
|
|
1802
1830
|
try {
|
|
1803
1831
|
toolMouseTui?.terminal?.write?.(TOOL_MOUSE_DISABLE);
|
|
@@ -1877,16 +1905,26 @@ function scheduleSessionRender(refresh?: () => void): void {
|
|
|
1877
1905
|
const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
|
|
1878
1906
|
const ANSI_FG_RESET = "\x1b[39m";
|
|
1879
1907
|
|
|
1880
|
-
function refreshCurrentTranscript(
|
|
1908
|
+
function refreshCurrentTranscript(
|
|
1909
|
+
compactStyle: CompactStyleHooks,
|
|
1910
|
+
ctx?: any,
|
|
1911
|
+
toolGrouping?: ToolGroupingHooks,
|
|
1912
|
+
): void {
|
|
1913
|
+
toolGrouping?.refresh();
|
|
1881
1914
|
compactStyle.refresh();
|
|
1882
1915
|
toolMouseTui?.requestRender?.(true);
|
|
1883
1916
|
ctx?.ui?.requestRender?.(true);
|
|
1884
1917
|
}
|
|
1885
1918
|
|
|
1886
|
-
function applyStyleMode(
|
|
1919
|
+
function applyStyleMode(
|
|
1920
|
+
mode: CompactStyleMode,
|
|
1921
|
+
ctx: any,
|
|
1922
|
+
compactStyle: CompactStyleHooks,
|
|
1923
|
+
toolGrouping?: ToolGroupingHooks,
|
|
1924
|
+
): void {
|
|
1887
1925
|
config.mode = mode;
|
|
1888
1926
|
saveConfig();
|
|
1889
|
-
refreshCurrentTranscript(compactStyle, ctx);
|
|
1927
|
+
refreshCurrentTranscript(compactStyle, ctx, toolGrouping);
|
|
1890
1928
|
ctx.ui.notify(`Claude Code style: ${mode}`, "info");
|
|
1891
1929
|
}
|
|
1892
1930
|
|
|
@@ -2011,7 +2049,11 @@ function renderSectionTabBar(
|
|
|
2011
2049
|
return truncateToWidth(pieces.join(""), Math.max(0, width));
|
|
2012
2050
|
}
|
|
2013
2051
|
|
|
2014
|
-
async function showCcstylePanel(
|
|
2052
|
+
async function showCcstylePanel(
|
|
2053
|
+
ctx: any,
|
|
2054
|
+
compactStyle: CompactStyleHooks,
|
|
2055
|
+
toolGrouping?: ToolGroupingHooks,
|
|
2056
|
+
): Promise<void> {
|
|
2015
2057
|
if (ctx?.mode !== "tui" || !ctx?.hasUI || typeof ctx.ui?.custom !== "function") {
|
|
2016
2058
|
ctx.ui?.notify?.("/ccstyle requires TUI mode", "warning");
|
|
2017
2059
|
return;
|
|
@@ -2110,7 +2152,7 @@ async function showCcstylePanel(ctx: any, compactStyle: CompactStyleHooks): Prom
|
|
|
2110
2152
|
switch (id) {
|
|
2111
2153
|
case "mode":
|
|
2112
2154
|
modeSetting.description = modeSettingDescription(value as CompactStyleMode);
|
|
2113
|
-
applyStyleMode(value as CompactStyleMode, ctx, compactStyle);
|
|
2155
|
+
applyStyleMode(value as CompactStyleMode, ctx, compactStyle, toolGrouping);
|
|
2114
2156
|
return;
|
|
2115
2157
|
case "fixedEditorFeatures":
|
|
2116
2158
|
config.fixedEditorFeatures = value === "on";
|
|
@@ -2287,21 +2329,64 @@ function singleLine(text: string) {
|
|
|
2287
2329
|
};
|
|
2288
2330
|
}
|
|
2289
2331
|
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
return
|
|
2303
|
-
|
|
2304
|
-
|
|
2332
|
+
function insetComponent(component: any): any {
|
|
2333
|
+
return {
|
|
2334
|
+
render: (width: number) =>
|
|
2335
|
+
component.render(Math.max(1, width - 1)).map((line: string) => {
|
|
2336
|
+
const nestedMarker = line.replace(/^((?:\x1b\[[0-?]*[ -/]*[@-~])*)↳/, "$1 ↳");
|
|
2337
|
+
return ` ${nestedMarker}`;
|
|
2338
|
+
}),
|
|
2339
|
+
invalidate: () => component.invalidate?.(),
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
function humanizeToolLabel(label: string): string {
|
|
2344
|
+
return label
|
|
2345
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
2346
|
+
.replace(/[_-]+/g, " ")
|
|
2347
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
function singleToolCallSummary(
|
|
2351
|
+
toolName: string,
|
|
2352
|
+
label: string,
|
|
2353
|
+
args: any,
|
|
2354
|
+
): { main: string; detail: string } {
|
|
2355
|
+
const title = label === toolName ? humanizeToolLabel(label) : label;
|
|
2356
|
+
if (!args || typeof args !== "object") return { main: title, detail: "" };
|
|
2357
|
+
if (AGENT_FAMILY_TOOL_NAMES.has(toolName) && args.agent_id) {
|
|
2358
|
+
return { main: `${title} ${oneLine(args.agent_id, Infinity)}`, detail: "" };
|
|
2359
|
+
}
|
|
2360
|
+
if (toolName === "read") {
|
|
2361
|
+
const details = [
|
|
2362
|
+
args.offset !== undefined ? `offset=${args.offset}` : "",
|
|
2363
|
+
args.limit !== undefined ? `limit=${args.limit}` : "",
|
|
2364
|
+
].filter(Boolean);
|
|
2365
|
+
return {
|
|
2366
|
+
main: `${title}${args.path ? ` ${oneLine(args.path, Infinity)}` : ""}`,
|
|
2367
|
+
detail: details.length ? ` (${details.join(", ")})` : "",
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
const value =
|
|
2371
|
+
args.path ??
|
|
2372
|
+
args.file_path ??
|
|
2373
|
+
args.command ??
|
|
2374
|
+
args.query ??
|
|
2375
|
+
args.question ??
|
|
2376
|
+
args.pattern ??
|
|
2377
|
+
args.url ??
|
|
2378
|
+
args.name ??
|
|
2379
|
+
args.tool_use_id ??
|
|
2380
|
+
args.toolCallId ??
|
|
2381
|
+
args.id ??
|
|
2382
|
+
args.message;
|
|
2383
|
+
return {
|
|
2384
|
+
main:
|
|
2385
|
+
value !== undefined && value !== null && typeof value !== "object"
|
|
2386
|
+
? `${title} ${oneLine(value, Infinity)}`
|
|
2387
|
+
: title,
|
|
2388
|
+
detail: "",
|
|
2389
|
+
};
|
|
2305
2390
|
}
|
|
2306
2391
|
|
|
2307
2392
|
export function middleTruncateToWidth(text: string, width: number): string {
|
|
@@ -2337,7 +2422,7 @@ function createCcstyleTool(
|
|
|
2337
2422
|
): any {
|
|
2338
2423
|
const toolName = originalTool.name;
|
|
2339
2424
|
const label = isMcpToolDefinition(originalTool, toolName)
|
|
2340
|
-
? toolName
|
|
2425
|
+
? humanizeMcpToolName(toolName)
|
|
2341
2426
|
: originalTool.label || toolName;
|
|
2342
2427
|
|
|
2343
2428
|
return {
|
|
@@ -2358,20 +2443,18 @@ function createCcstyleTool(
|
|
|
2358
2443
|
visualState === "success"
|
|
2359
2444
|
? `${BRIGHT_GREEN}${rawIcon}${ANSI_FG_RESET}`
|
|
2360
2445
|
: theme.fg(toolIconColor(context), rawIcon);
|
|
2361
|
-
|
|
2362
|
-
const argument = toolCallArgument(args);
|
|
2446
|
+
const summary = singleToolCallSummary(toolName, label, args);
|
|
2363
2447
|
let cachedWidth: number | undefined;
|
|
2364
2448
|
let cachedLine: string | undefined;
|
|
2365
2449
|
return {
|
|
2366
2450
|
render(width: number) {
|
|
2367
2451
|
if (cachedLine !== undefined && cachedWidth === width) return [cachedLine];
|
|
2368
|
-
const
|
|
2369
|
-
const
|
|
2370
|
-
const
|
|
2371
|
-
const call = shown === undefined ? label : `${label}(${shown})`;
|
|
2452
|
+
const viewportWidth = toolViewportWidth(width);
|
|
2453
|
+
const callWidth = Math.max(0, viewportWidth - visibleWidth(icon) - 2);
|
|
2454
|
+
const mainWidth = Math.max(0, callWidth - visibleWidth(summary.detail));
|
|
2372
2455
|
cachedWidth = width;
|
|
2373
|
-
cachedLine =
|
|
2374
|
-
return [cachedLine];
|
|
2456
|
+
cachedLine = ` ${icon} ${theme.fg("toolTitle", middleTruncateToWidth(summary.main, mainWidth))}${theme.fg("muted", summary.detail)}`;
|
|
2457
|
+
return [truncateToWidth(cachedLine, viewportWidth, "")];
|
|
2375
2458
|
},
|
|
2376
2459
|
invalidate() {},
|
|
2377
2460
|
};
|
|
@@ -2387,7 +2470,7 @@ function createCcstyleTool(
|
|
|
2387
2470
|
}
|
|
2388
2471
|
|
|
2389
2472
|
if (options?.isPartial) {
|
|
2390
|
-
return new Text(theme.fg("muted", "
|
|
2473
|
+
return new Text(theme.fg("muted", " ⎿ Pending…"), 0, 0);
|
|
2391
2474
|
}
|
|
2392
2475
|
|
|
2393
2476
|
const isError = options?.isError || context?.isError;
|
|
@@ -2405,17 +2488,24 @@ function createCcstyleTool(
|
|
|
2405
2488
|
writeExecutionMetadata,
|
|
2406
2489
|
getToolDisplayConfig,
|
|
2407
2490
|
);
|
|
2408
|
-
if (richResult) return richResult;
|
|
2491
|
+
if (richResult) return insetComponent(richResult);
|
|
2409
2492
|
}
|
|
2410
2493
|
|
|
2411
|
-
const text = textFromResult(result);
|
|
2494
|
+
const text = textFromResult(result, expanded);
|
|
2412
2495
|
const args = context?.args;
|
|
2413
|
-
const
|
|
2414
|
-
|
|
2415
|
-
const
|
|
2416
|
-
const
|
|
2417
|
-
|
|
2418
|
-
|
|
2496
|
+
const outputLines = outputLineCount(result) || countLines(text);
|
|
2497
|
+
const lineWord = outputLines === 1 ? "line" : "lines";
|
|
2498
|
+
const action = toolName === "read" ? "loaded" : "returned";
|
|
2499
|
+
const rendered = isError
|
|
2500
|
+
? text
|
|
2501
|
+
? oneLine(text)
|
|
2502
|
+
: "Failed"
|
|
2503
|
+
: outputLines
|
|
2504
|
+
? `${outputLines} ${lineWord} ${action}`
|
|
2505
|
+
: "Done";
|
|
2506
|
+
const expandable = !expanded && hasExpandableDetail(text, args);
|
|
2507
|
+
const hint = expandable ? theme.fg("muted", " • click to show more") : "";
|
|
2508
|
+
const hoveredHint = expandable ? theme.fg("text", " • click to show more") : "";
|
|
2419
2509
|
if (expanded) {
|
|
2420
2510
|
return renderExpandedToolResult(
|
|
2421
2511
|
text || "",
|
|
@@ -2461,7 +2551,13 @@ function createCcstyleTool(
|
|
|
2461
2551
|
const GLOBAL_TOOL_RENDER_PATCH = Symbol.for("pi.ccstyle.global-tool-render-patch");
|
|
2462
2552
|
const COMPONENT_TOOL_RENDER_MODE = Symbol.for("pi.ccstyle.component-tool-render-mode");
|
|
2463
2553
|
const COMPONENT_TOOL_SELF_SHELL_MODE = Symbol.for("pi.ccstyle.component-tool-self-shell-mode");
|
|
2464
|
-
const
|
|
2554
|
+
const AGENT_FAMILY_TOOL_NAMES = new Set([
|
|
2555
|
+
"Agent",
|
|
2556
|
+
"Agents",
|
|
2557
|
+
"get_subagent_result",
|
|
2558
|
+
"steer_subagent",
|
|
2559
|
+
]);
|
|
2560
|
+
const DEDICATED_SUBAGENT_TOOL_NAMES = new Set(["Agent"]);
|
|
2465
2561
|
|
|
2466
2562
|
type ToolRenderMethods = {
|
|
2467
2563
|
hasRendererDefinition: (...args: any[]) => boolean;
|
|
@@ -2489,9 +2585,23 @@ type GlobalToolRenderPatch = {
|
|
|
2489
2585
|
originalGetResultRenderer: ToolRenderMethods["getResultRenderer"];
|
|
2490
2586
|
};
|
|
2491
2587
|
|
|
2492
|
-
function isMcpToolDefinition(definition: any, toolName: string): boolean {
|
|
2493
|
-
const label = typeof definition?.label === "string" ? definition.label : "";
|
|
2494
|
-
|
|
2588
|
+
export function isMcpToolDefinition(definition: any, toolName: string): boolean {
|
|
2589
|
+
const label = typeof definition?.label === "string" ? definition.label.trim() : "";
|
|
2590
|
+
if (/^MCP(?::|$)/i.test(label)) return true;
|
|
2591
|
+
if (toolName === "mcp" || /^mcp[_:-]|[_:-]mcp[_:-]/i.test(toolName)) return true;
|
|
2592
|
+
if (label) return false;
|
|
2593
|
+
const description = typeof definition?.description === "string" ? definition.description : "";
|
|
2594
|
+
return /\bModel Context Protocol\b/i.test(description);
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
export function humanizeMcpToolName(toolName: string): string {
|
|
2598
|
+
const words = toolName
|
|
2599
|
+
.replace(/^mcp(?:[_:-]+)+/i, "")
|
|
2600
|
+
.split(/[_:-]+/)
|
|
2601
|
+
.filter(Boolean);
|
|
2602
|
+
return words.length
|
|
2603
|
+
? words.map((word) => word[0]!.toUpperCase() + word.slice(1)).join(" ")
|
|
2604
|
+
: "MCP";
|
|
2495
2605
|
}
|
|
2496
2606
|
|
|
2497
2607
|
/** Return true when this tool must keep its original renderer. */
|
|
@@ -2501,7 +2611,7 @@ export function preservesOriginalRenderer(
|
|
|
2501
2611
|
builtInToolDefinition?: any,
|
|
2502
2612
|
excludeRenderers: readonly string[] = config.excludeRenderers,
|
|
2503
2613
|
): boolean {
|
|
2504
|
-
if (
|
|
2614
|
+
if (DEDICATED_SUBAGENT_TOOL_NAMES.has(toolName)) return true;
|
|
2505
2615
|
if (!excludeRenderers.includes(toolName)) return false;
|
|
2506
2616
|
return [extensionDefinition, builtInToolDefinition].some(
|
|
2507
2617
|
(definition) =>
|
|
@@ -2547,7 +2657,7 @@ function shouldUseSelfShell(component: any, patch: GlobalToolRenderPatch): boole
|
|
|
2547
2657
|
const toolName = String(component.toolName || definition?.name || "");
|
|
2548
2658
|
const useSelfShell =
|
|
2549
2659
|
patch.enabled() &&
|
|
2550
|
-
|
|
2660
|
+
DEDICATED_SUBAGENT_TOOL_NAMES.has(toolName) &&
|
|
2551
2661
|
definition != null &&
|
|
2552
2662
|
definition.renderShell === undefined;
|
|
2553
2663
|
component[COMPONENT_TOOL_SELF_SHELL_MODE] = useSelfShell;
|
|
@@ -2785,6 +2895,7 @@ export default function (pi: ExtensionAPI, configOverride?: Partial<Config>) {
|
|
|
2785
2895
|
if (configOverride) config = normalizeConfig({ ...config, ...configOverride });
|
|
2786
2896
|
const writeExecutionMetadata = installWriteOverride(pi, new WriteExecutionMetadataStore());
|
|
2787
2897
|
const globalToolRendering = installGlobalToolRendering(writeExecutionMetadata);
|
|
2898
|
+
const toolGrouping = installToolGrouping(() => config.mode === "on");
|
|
2788
2899
|
deactivateLegacyCompactionRendering();
|
|
2789
2900
|
const compactStyle: CompactStyleHooks = installCompactStyle(pi, {
|
|
2790
2901
|
getMode: () => config.mode,
|
|
@@ -2806,11 +2917,11 @@ export default function (pi: ExtensionAPI, configOverride?: Partial<Config>) {
|
|
|
2806
2917
|
handler: async (args, ctx) => {
|
|
2807
2918
|
const arg = args.trim().toLowerCase();
|
|
2808
2919
|
if (!arg || arg === "panel") {
|
|
2809
|
-
await showCcstylePanel(ctx, compactStyle);
|
|
2920
|
+
await showCcstylePanel(ctx, compactStyle, toolGrouping);
|
|
2810
2921
|
return;
|
|
2811
2922
|
}
|
|
2812
2923
|
if (arg === "on" || arg === "off" || arg === "compact") {
|
|
2813
|
-
applyStyleMode(arg, ctx, compactStyle);
|
|
2924
|
+
applyStyleMode(arg, ctx, compactStyle, toolGrouping);
|
|
2814
2925
|
return;
|
|
2815
2926
|
}
|
|
2816
2927
|
if (arg === "status") {
|
|
@@ -2822,6 +2933,7 @@ export default function (pi: ExtensionAPI, configOverride?: Partial<Config>) {
|
|
|
2822
2933
|
});
|
|
2823
2934
|
|
|
2824
2935
|
pi.on("session_start", async (event, ctx) => {
|
|
2936
|
+
toolGrouping.setTheme(ctx.ui.theme);
|
|
2825
2937
|
compactStyle?.onSessionStart(event, ctx);
|
|
2826
2938
|
pendingScrollMessages = 0;
|
|
2827
2939
|
assistantMessageActive = false;
|
|
@@ -2831,6 +2943,7 @@ export default function (pi: ExtensionAPI, configOverride?: Partial<Config>) {
|
|
|
2831
2943
|
});
|
|
2832
2944
|
|
|
2833
2945
|
pi.on("session_compact", async (event, ctx) => {
|
|
2946
|
+
toolGrouping.setTheme(ctx.ui.theme);
|
|
2834
2947
|
compactStyle?.onSessionCompact(event, ctx);
|
|
2835
2948
|
// Compaction rebuilds the transcript without session_start. Rebind after
|
|
2836
2949
|
// other TUI extensions may have replaced the root input dispatcher.
|
|
@@ -2864,6 +2977,7 @@ export default function (pi: ExtensionAPI, configOverride?: Partial<Config>) {
|
|
|
2864
2977
|
});
|
|
2865
2978
|
|
|
2866
2979
|
pi.on("tool_execution_start", async (event, ctx) => {
|
|
2980
|
+
toolGrouping.setTheme(ctx.ui.theme);
|
|
2867
2981
|
compactStyle?.onToolExecutionStart(event, ctx);
|
|
2868
2982
|
});
|
|
2869
2983
|
|
|
@@ -2880,6 +2994,7 @@ export default function (pi: ExtensionAPI, configOverride?: Partial<Config>) {
|
|
|
2880
2994
|
writeExecutionMetadata.clear();
|
|
2881
2995
|
teardownToolMouseInteraction();
|
|
2882
2996
|
deactivateGlobalToolRendering(globalToolRendering);
|
|
2997
|
+
toolGrouping.shutdown();
|
|
2883
2998
|
deactivateLegacyCompactionRendering();
|
|
2884
2999
|
clearAllAnimations();
|
|
2885
3000
|
});
|