pi-cc-extensions 0.8.32 → 0.8.34
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 +419 -410
- package/extensions/compact-thinking.ts +147 -21
- package/extensions/fixed-editor.ts +186 -16
- package/extensions/tool-grouping.ts +4 -0
- package/package.json +1 -1
|
@@ -9,7 +9,9 @@ import type { CompactThinkingConfig, CompactThinkingController } from "./compact
|
|
|
9
9
|
import { showTextPreview } from "./context.ts";
|
|
10
10
|
import {
|
|
11
11
|
getFixedEditorScrollButtonHitbox,
|
|
12
|
+
getFixedEditorViewport,
|
|
12
13
|
installFixedEditor,
|
|
14
|
+
setBeforeFixedEditorStart,
|
|
13
15
|
type FixedEditorController,
|
|
14
16
|
} from "./fixed-editor.ts";
|
|
15
17
|
import {
|
|
@@ -65,7 +67,6 @@ export type Config = {
|
|
|
65
67
|
|
|
66
68
|
const AGENT_DIR = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
67
69
|
const CONFIG_PATH = join(AGENT_DIR, "claude-code-style.json");
|
|
68
|
-
const LEGACY_COMPACT_THINKING_CONFIG_PATH = join(AGENT_DIR, "compact-thinking.json");
|
|
69
70
|
|
|
70
71
|
const DIFF_VIEW_MODES: DiffViewMode[] = ["auto", "split", "unified"];
|
|
71
72
|
const DIFF_INDICATOR_MODES: DiffIndicatorMode[] = ["bars", "classic", "none"];
|
|
@@ -247,34 +248,12 @@ function loadConfig(): Config {
|
|
|
247
248
|
const source = existsSync(CONFIG_PATH)
|
|
248
249
|
? (JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Record<string, unknown>)
|
|
249
250
|
: {};
|
|
250
|
-
let migrated = false;
|
|
251
|
-
if (existsSync(LEGACY_COMPACT_THINKING_CONFIG_PATH)) {
|
|
252
|
-
try {
|
|
253
|
-
const legacy = JSON.parse(
|
|
254
|
-
readFileSync(LEGACY_COMPACT_THINKING_CONFIG_PATH, "utf8"),
|
|
255
|
-
) as Record<string, unknown>;
|
|
256
|
-
for (const key of [
|
|
257
|
-
"useSummaryTitlesAsThinkingTitle",
|
|
258
|
-
"previewLines",
|
|
259
|
-
"animationIntervalMs",
|
|
260
|
-
] as const) {
|
|
261
|
-
if (!(key in source) && key in legacy) {
|
|
262
|
-
source[key] = legacy[key];
|
|
263
|
-
migrated = true;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
} catch {
|
|
267
|
-
// A malformed legacy file must not invalidate the ccstyle source.
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
251
|
const normalized = normalizeConfig(source);
|
|
271
|
-
// Persist one-time migrations while retaining existing ccstyle values.
|
|
272
252
|
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
source.mode !== "compact")
|
|
253
|
+
typeof source.enabled === "boolean" &&
|
|
254
|
+
source.mode !== "on" &&
|
|
255
|
+
source.mode !== "off" &&
|
|
256
|
+
source.mode !== "compact"
|
|
278
257
|
) {
|
|
279
258
|
try {
|
|
280
259
|
writeFileSync(CONFIG_PATH, JSON.stringify(normalized, null, 2));
|
|
@@ -524,6 +503,8 @@ export class ExpandedToolIoView {
|
|
|
524
503
|
private hoveredSection: ToolIoSection | null = null;
|
|
525
504
|
/** Which sections currently show the [show more] affordance (after last render). */
|
|
526
505
|
private truncated: { input: boolean; output: boolean } = { input: false, output: false };
|
|
506
|
+
/** 0-based header line indexes that carry [show more] after last render. */
|
|
507
|
+
private showMoreHeaderRows: { input?: number; output?: number } = {};
|
|
527
508
|
|
|
528
509
|
constructor(
|
|
529
510
|
theme: any,
|
|
@@ -591,6 +572,18 @@ export class ExpandedToolIoView {
|
|
|
591
572
|
return null;
|
|
592
573
|
}
|
|
593
574
|
|
|
575
|
+
/** Precise header rows marked for show-more hit testing (last render). */
|
|
576
|
+
showMoreHeaderLineIndexes(): ReadonlyArray<{ section: ToolIoSection; line: number }> {
|
|
577
|
+
const out: Array<{ section: ToolIoSection; line: number }> = [];
|
|
578
|
+
if (this.showMoreHeaderRows.input !== undefined) {
|
|
579
|
+
out.push({ section: "input", line: this.showMoreHeaderRows.input });
|
|
580
|
+
}
|
|
581
|
+
if (this.showMoreHeaderRows.output !== undefined) {
|
|
582
|
+
out.push({ section: "output", line: this.showMoreHeaderRows.output });
|
|
583
|
+
}
|
|
584
|
+
return out;
|
|
585
|
+
}
|
|
586
|
+
|
|
594
587
|
/** Column range (1-based, visible cells) of [show more] on a rendered header, if present. */
|
|
595
588
|
showMoreHitbox(plainLine: string): { startCol: number; endCol: number } | null {
|
|
596
589
|
const line = plainLine.replace(/\x1b\[[0-9;]*m/g, "");
|
|
@@ -603,8 +596,9 @@ export class ExpandedToolIoView {
|
|
|
603
596
|
}
|
|
604
597
|
|
|
605
598
|
render(width: number): string[] {
|
|
606
|
-
|
|
607
|
-
|
|
599
|
+
if (this.cachedLines !== undefined && this.cachedWidth === width) {
|
|
600
|
+
return withIoViewMarkers(this, this.cachedLines);
|
|
601
|
+
}
|
|
608
602
|
|
|
609
603
|
const theme = this.theme;
|
|
610
604
|
const safeWidth = Math.max(1, Math.floor(width));
|
|
@@ -615,6 +609,7 @@ export class ExpandedToolIoView {
|
|
|
615
609
|
const bodyColor = this.isError ? "error" : "toolOutput";
|
|
616
610
|
const lines: string[] = [];
|
|
617
611
|
this.truncated = { input: false, output: false };
|
|
612
|
+
this.showMoreHeaderRows = {};
|
|
618
613
|
|
|
619
614
|
const pushHeader = (
|
|
620
615
|
corner: "├" | "└",
|
|
@@ -630,6 +625,7 @@ export class ExpandedToolIoView {
|
|
|
630
625
|
const more = showMore
|
|
631
626
|
? theme.fg(this.hoveredSection === section ? "text" : "dim", ` ${SHOW_MORE_LABEL}`)
|
|
632
627
|
: "";
|
|
628
|
+
if (showMore) this.showMoreHeaderRows[section] = lines.length;
|
|
633
629
|
lines.push(truncateToWidth(mark + title + more, safeWidth, ""));
|
|
634
630
|
};
|
|
635
631
|
|
|
@@ -717,7 +713,7 @@ export class ExpandedToolIoView {
|
|
|
717
713
|
|
|
718
714
|
this.cachedWidth = width;
|
|
719
715
|
this.cachedLines = lines;
|
|
720
|
-
return lines;
|
|
716
|
+
return withIoViewMarkers(this, lines);
|
|
721
717
|
}
|
|
722
718
|
|
|
723
719
|
invalidate(): void {
|
|
@@ -917,17 +913,75 @@ type SgrMousePacket = {
|
|
|
917
913
|
final: "M" | "m";
|
|
918
914
|
};
|
|
919
915
|
|
|
920
|
-
type
|
|
916
|
+
type FrameToolRender = {
|
|
917
|
+
component: any;
|
|
918
|
+
lines: string[];
|
|
919
|
+
contentBoxLines: number;
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
/** Final painted placement of one outermost tool/group row after parent layout. */
|
|
923
|
+
type FrameToolPlacement = {
|
|
921
924
|
component: any;
|
|
922
|
-
|
|
923
|
-
|
|
925
|
+
componentRow: number;
|
|
926
|
+
lineIndex: number;
|
|
927
|
+
/** Marker-stripped final line text as painted after parent layout. */
|
|
928
|
+
finalLine: string;
|
|
929
|
+
view?: ExpandedToolIoView;
|
|
930
|
+
section?: ToolIoSection;
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
type InteractionRegion = {
|
|
934
|
+
kind: "collapsed-hint" | "expanded-card" | "show-more" | "scroll-bottom";
|
|
924
935
|
row: number;
|
|
936
|
+
startCol: number;
|
|
937
|
+
endCol: number;
|
|
938
|
+
component?: any;
|
|
939
|
+
view?: ExpandedToolIoView;
|
|
940
|
+
section?: ToolIoSection;
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
type InteractionFrame = { regions: InteractionRegion[] };
|
|
944
|
+
|
|
945
|
+
/** Zero-width APC row marker (like pi CURSOR_MARKER); stripped before terminal output. */
|
|
946
|
+
const TOOL_FRAME_MARKER_RE = /_cc:t(\d+):(\d+)/g;
|
|
947
|
+
const TOOL_VIEW_MARKER_RE = /_cc:v(\d+):([io])/g;
|
|
948
|
+
const toolFrameMarker = (id: number, row: number) => `_cc:t${id}:${row}`;
|
|
949
|
+
const toolViewMarker = (id: number, section: ToolIoSection) =>
|
|
950
|
+
`_cc:v${id}:${section === "input" ? "i" : "o"}`;
|
|
951
|
+
|
|
952
|
+
/** Per-frame ExpandedToolIoView ids for unambiguous show-more hit testing. */
|
|
953
|
+
type IoViewFrameState = {
|
|
954
|
+
viewIds: Map<ExpandedToolIoView, number>;
|
|
955
|
+
idToView: Map<number, ExpandedToolIoView>;
|
|
956
|
+
nextId: number;
|
|
925
957
|
};
|
|
958
|
+
let activeIoViewFrame: IoViewFrameState | null = null;
|
|
959
|
+
|
|
960
|
+
function frameViewId(view: ExpandedToolIoView): number | null {
|
|
961
|
+
if (!activeIoViewFrame) return null;
|
|
962
|
+
let id = activeIoViewFrame.viewIds.get(view);
|
|
963
|
+
if (id === undefined) {
|
|
964
|
+
id = activeIoViewFrame.nextId++;
|
|
965
|
+
activeIoViewFrame.viewIds.set(view, id);
|
|
966
|
+
activeIoViewFrame.idToView.set(id, view);
|
|
967
|
+
}
|
|
968
|
+
return id;
|
|
969
|
+
}
|
|
926
970
|
|
|
927
|
-
|
|
971
|
+
/** cachedLines stay clean; only the returned paint copy carries show-more view markers. */
|
|
972
|
+
function withIoViewMarkers(view: ExpandedToolIoView, lines: string[]): string[] {
|
|
973
|
+
const id = frameViewId(view);
|
|
974
|
+
if (id === null) return lines;
|
|
975
|
+
// Mark by exact header row from render — never scan body text for Input/Output labels.
|
|
976
|
+
const marked = lines.slice();
|
|
977
|
+
for (const { section, line } of view.showMoreHeaderLineIndexes()) {
|
|
978
|
+
if (line < 0 || line >= marked.length) continue;
|
|
979
|
+
marked[line] = `${marked[line]}${toolViewMarker(id, section)}`;
|
|
980
|
+
}
|
|
981
|
+
return marked;
|
|
982
|
+
}
|
|
928
983
|
|
|
929
984
|
const TOOL_MOUSE_WIDGET_KEY = "ccstyle-tool-mouse";
|
|
930
|
-
const TOOL_MOUSE_ENABLE = "\x1b[?1000h\x1b[?1003h\x1b[?1006h";
|
|
931
985
|
const TOOL_MOUSE_MOTION_ENABLE = "\x1b[?1003h\x1b[?1006h";
|
|
932
986
|
const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
|
|
933
987
|
const ZENTUI_PAGE_UP_INPUT = /^\x1b\[5;9(?::[12])?~$|^\x1b\[57421;9(?::[12])?u$|^\x1b\[1;6A$/;
|
|
@@ -959,7 +1013,7 @@ let scrollButtonSyncScheduled = false;
|
|
|
959
1013
|
let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
|
|
960
1014
|
let hoveredToolCallId: string | null = null;
|
|
961
1015
|
let hoveredToolGroup: ToolGroupComponent | null = null;
|
|
962
|
-
let
|
|
1016
|
+
let latestInteractionFrame: InteractionFrame = { regions: [] };
|
|
963
1017
|
|
|
964
1018
|
function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
|
|
965
1019
|
const pattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
@@ -1005,49 +1059,6 @@ function isToolExecutionComponent(value: any): boolean {
|
|
|
1005
1059
|
);
|
|
1006
1060
|
}
|
|
1007
1061
|
|
|
1008
|
-
function renderedLineCount(component: any, width: number, cache: WeakMap<object, number>): number {
|
|
1009
|
-
if (!component || typeof component !== "object") return 0;
|
|
1010
|
-
const cached = cache.get(component);
|
|
1011
|
-
if (cached !== undefined) return cached;
|
|
1012
|
-
|
|
1013
|
-
let count = 0;
|
|
1014
|
-
try {
|
|
1015
|
-
const lines = component.render(width);
|
|
1016
|
-
count = Array.isArray(lines) ? lines.length : 0;
|
|
1017
|
-
} catch {
|
|
1018
|
-
count = 0;
|
|
1019
|
-
}
|
|
1020
|
-
cache.set(component, count);
|
|
1021
|
-
return count;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
function collectToolRenderHits(
|
|
1025
|
-
component: any,
|
|
1026
|
-
start: number,
|
|
1027
|
-
width: number,
|
|
1028
|
-
hits: ToolRenderHit[],
|
|
1029
|
-
cache: WeakMap<object, number>,
|
|
1030
|
-
seen: Set<object>,
|
|
1031
|
-
): number {
|
|
1032
|
-
if (!component || typeof component !== "object" || seen.has(component)) return start;
|
|
1033
|
-
seen.add(component);
|
|
1034
|
-
|
|
1035
|
-
const count = renderedLineCount(component, width, cache);
|
|
1036
|
-
if (isToolExecutionComponent(component)) {
|
|
1037
|
-
if (count > 0) hits.push({ component, start, end: start + count, row: 0 });
|
|
1038
|
-
return start + count;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
let childStart = start;
|
|
1042
|
-
if (Array.isArray(component.children)) {
|
|
1043
|
-
for (const child of component.children) {
|
|
1044
|
-
collectToolRenderHits(child, childStart, width, hits, cache, seen);
|
|
1045
|
-
childStart += renderedLineCount(child, width, cache);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
return start + count;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
1062
|
function collectToolComponents(component: any, tools: any[], seen = new Set<any>()): void {
|
|
1052
1063
|
if (!component || typeof component !== "object" || seen.has(component)) return;
|
|
1053
1064
|
seen.add(component);
|
|
@@ -1059,109 +1070,49 @@ function collectToolComponents(component: any, tools: any[], seen = new Set<any>
|
|
|
1059
1070
|
for (const child of component.children) collectToolComponents(child, tools, seen);
|
|
1060
1071
|
}
|
|
1061
1072
|
|
|
1062
|
-
function
|
|
1063
|
-
return (
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
const
|
|
1080
|
-
if (candidate
|
|
1081
|
-
|
|
1073
|
+
function stripToolFrameMarkers(line: string): string {
|
|
1074
|
+
return line.replace(TOOL_FRAME_MARKER_RE, "").replace(TOOL_VIEW_MARKER_RE, "");
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function extractToolFramePlacements(
|
|
1078
|
+
lines: string[],
|
|
1079
|
+
idToComponent: Map<number, any>,
|
|
1080
|
+
idToView: Map<number, ExpandedToolIoView>,
|
|
1081
|
+
): { lines: string[]; placements: FrameToolPlacement[] } {
|
|
1082
|
+
const placements: FrameToolPlacement[] = [];
|
|
1083
|
+
const cleaned = lines.map((line, lineIndex) => {
|
|
1084
|
+
const toolMatches = [...line.matchAll(TOOL_FRAME_MARKER_RE)];
|
|
1085
|
+
const viewMatches = [...line.matchAll(TOOL_VIEW_MARKER_RE)];
|
|
1086
|
+
const finalLine = stripToolFrameMarkers(line);
|
|
1087
|
+
let view: ExpandedToolIoView | undefined;
|
|
1088
|
+
let section: ToolIoSection | undefined;
|
|
1089
|
+
for (const match of viewMatches) {
|
|
1090
|
+
const candidate = idToView.get(Number(match[1]));
|
|
1091
|
+
if (!candidate) continue;
|
|
1092
|
+
view = candidate;
|
|
1093
|
+
section = match[2] === "i" ? "input" : "output";
|
|
1094
|
+
break;
|
|
1082
1095
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1096
|
+
for (const match of toolMatches) {
|
|
1097
|
+
const component = idToComponent.get(Number(match[1]));
|
|
1098
|
+
if (!component) continue;
|
|
1099
|
+
placements.push({
|
|
1100
|
+
component,
|
|
1101
|
+
componentRow: Number(match[2]),
|
|
1102
|
+
lineIndex,
|
|
1103
|
+
finalLine,
|
|
1104
|
+
view,
|
|
1105
|
+
section,
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
return finalLine;
|
|
1109
|
+
});
|
|
1110
|
+
return { lines: cleaned, placements };
|
|
1085
1111
|
}
|
|
1086
1112
|
|
|
1087
1113
|
/** Summary markers used by Pi and ccstyle; unlike the trailing hint, these survive truncation. */
|
|
1088
1114
|
const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿|●|✓|✗|…)/;
|
|
1089
1115
|
|
|
1090
|
-
function findToolAtFixedEditorRow(
|
|
1091
|
-
tui: any,
|
|
1092
|
-
visibleRow: number,
|
|
1093
|
-
previousLines: string[],
|
|
1094
|
-
width: number,
|
|
1095
|
-
): ToolRenderHit | null {
|
|
1096
|
-
if (visibleRow < 0 || visibleRow >= previousLines.length) return null;
|
|
1097
|
-
const visibleLines = previousLines.map((line) => stripTerminalSequences(String(line)));
|
|
1098
|
-
const rawClickedLine = String(previousLines[visibleRow] ?? "");
|
|
1099
|
-
const clickedLine = visibleLines[visibleRow] ?? "";
|
|
1100
|
-
const hasBackground = /\x1b\[(?:4[0-8]|10[0-7])(?:;[0-9;:]*)?m/.test(rawClickedLine);
|
|
1101
|
-
if (!clickedLine && !hasBackground) return null;
|
|
1102
|
-
|
|
1103
|
-
let candidates = frameToolRenders;
|
|
1104
|
-
if (candidates.length === 0) {
|
|
1105
|
-
const tools: any[] = [];
|
|
1106
|
-
collectToolComponents(tui, tools);
|
|
1107
|
-
candidates = tools.map((component) => ({
|
|
1108
|
-
component,
|
|
1109
|
-
lines: renderComponentTree(component, width),
|
|
1110
|
-
}));
|
|
1111
|
-
}
|
|
1112
|
-
const clickedCollapsedSummary = COLLAPSED_TOOL_SUMMARY.test(clickedLine);
|
|
1113
|
-
let best: { hit: ToolRenderHit; score: number } | null = null;
|
|
1114
|
-
for (const { component, lines } of candidates) {
|
|
1115
|
-
// Collapsed tools open only from their summary row; expanded tools collapse from any body row.
|
|
1116
|
-
if (!component.expanded && !clickedCollapsedSummary) continue;
|
|
1117
|
-
const renderedLines = lines.map((line) => stripTerminalSequences(String(line)));
|
|
1118
|
-
for (let renderedRow = 0; renderedRow < renderedLines.length; renderedRow++) {
|
|
1119
|
-
if (!fixedEditorLineMatch(renderedLines[renderedRow] ?? "", clickedLine)) continue;
|
|
1120
|
-
const score = fixedEditorContextScore(renderedLines, renderedRow, visibleLines, visibleRow);
|
|
1121
|
-
if (!best || score >= best.score) {
|
|
1122
|
-
best = {
|
|
1123
|
-
hit: {
|
|
1124
|
-
component,
|
|
1125
|
-
start: visibleRow,
|
|
1126
|
-
end: visibleRow + renderedLines.length,
|
|
1127
|
-
row: renderedRow,
|
|
1128
|
-
},
|
|
1129
|
-
score,
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
return best?.hit ?? null;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
function findToolAtScreenRow(tui: any, screenRow: number): ToolRenderHit | null {
|
|
1138
|
-
const previousLines = Array.isArray(tui?.previousLines) ? tui.previousLines : [];
|
|
1139
|
-
const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
|
|
1140
|
-
if (useFixedEditorFeatures(tui)) {
|
|
1141
|
-
// Zentui replaces Pi's root render with the already-sliced visible
|
|
1142
|
-
// transcript. previousViewportTop remains cursor bookkeeping and must not
|
|
1143
|
-
// be added to a physical mouse row here.
|
|
1144
|
-
return findToolAtFixedEditorRow(tui, screenRow - 1, previousLines, width);
|
|
1145
|
-
}
|
|
1146
|
-
const viewportTop = Number.isFinite(tui?.previousViewportTop) ? tui.previousViewportTop : 0;
|
|
1147
|
-
const bufferRow = viewportTop + screenRow - 1;
|
|
1148
|
-
if (bufferRow < 0 || bufferRow >= previousLines.length) return null;
|
|
1149
|
-
|
|
1150
|
-
const hits: ToolRenderHit[] = [];
|
|
1151
|
-
const cache = new WeakMap<object, number>();
|
|
1152
|
-
collectToolRenderHits(tui, 0, width, hits, cache, new Set<object>());
|
|
1153
|
-
|
|
1154
|
-
const clickedLine = stripTerminalSequences(String(previousLines[bufferRow] ?? ""));
|
|
1155
|
-
for (const hit of hits) {
|
|
1156
|
-
if (bufferRow < hit.start || bufferRow >= hit.end) continue;
|
|
1157
|
-
// The stable summary marker keeps the whole row clickable even when the
|
|
1158
|
-
// trailing keyboard/click hint was truncated (common for subagent output).
|
|
1159
|
-
if (!hit.component.expanded && !COLLAPSED_TOOL_SUMMARY.test(clickedLine)) continue;
|
|
1160
|
-
return { ...hit, row: bufferRow - hit.start };
|
|
1161
|
-
}
|
|
1162
|
-
return null;
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
1116
|
function isFixedEditorTui(tui: any): boolean {
|
|
1166
1117
|
const terminal = tui?.terminal;
|
|
1167
1118
|
if (!terminal) return false;
|
|
@@ -1359,15 +1310,8 @@ function containsEditorLike(component: any, focused: any, seen = new Set<any>())
|
|
|
1359
1310
|
);
|
|
1360
1311
|
}
|
|
1361
1312
|
|
|
1362
|
-
function isScrollButtonAtScreenRow(
|
|
1363
|
-
|
|
1364
|
-
const hitbox = getFixedEditorScrollButtonHitbox();
|
|
1365
|
-
return Boolean(
|
|
1366
|
-
hitbox &&
|
|
1367
|
-
packet.row === hitbox.row &&
|
|
1368
|
-
packet.col >= hitbox.startCol &&
|
|
1369
|
-
packet.col <= hitbox.endCol,
|
|
1370
|
-
);
|
|
1313
|
+
function isScrollButtonAtScreenRow(_tui: any, packet: SgrMousePacket): boolean {
|
|
1314
|
+
return interactionRegionAt(packet)?.kind === "scroll-bottom";
|
|
1371
1315
|
}
|
|
1372
1316
|
|
|
1373
1317
|
function jumpToBottomWithoutSubmit(tui: any): boolean {
|
|
@@ -1414,139 +1358,50 @@ function scheduleCollapseViewportCompensation(
|
|
|
1414
1358
|
});
|
|
1415
1359
|
}
|
|
1416
1360
|
|
|
1417
|
-
/** toolCallId → latest expanded IO view (survives context/state identity quirks). */
|
|
1418
|
-
const ioViewsByToolCallId = new Map<string, ExpandedToolIoView>();
|
|
1419
1361
|
const ioViewInvalidators = new WeakMap<ExpandedToolIoView, () => void>();
|
|
1420
|
-
const ioViewCollectors = new Set<Set<ExpandedToolIoView>>();
|
|
1421
|
-
let frameIoViews = new Set<ExpandedToolIoView>();
|
|
1422
1362
|
let hoveredToolIoView: ExpandedToolIoView | null = null;
|
|
1423
1363
|
let hoveredToolIoSection: ToolIoSection | null = null;
|
|
1424
1364
|
|
|
1425
|
-
function recordRenderedIoView(view: ExpandedToolIoView): void {
|
|
1426
|
-
for (const collector of ioViewCollectors) collector.add(view);
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
1365
|
function rememberIoView(context: any, view: ExpandedToolIoView): void {
|
|
1430
1366
|
if (!context || typeof context !== "object") return;
|
|
1431
1367
|
if (typeof context.invalidate === "function") ioViewInvalidators.set(view, context.invalidate);
|
|
1432
1368
|
if (!context.state || typeof context.state !== "object") context.state = {};
|
|
1433
|
-
|
|
1434
|
-
state.ccstyleIoView = view;
|
|
1435
|
-
const id =
|
|
1436
|
-
(typeof context?.toolCallId === "string" && context.toolCallId) ||
|
|
1437
|
-
(typeof context?.id === "string" && context.id) ||
|
|
1438
|
-
(typeof state?.toolCallId === "string" && state.toolCallId) ||
|
|
1439
|
-
undefined;
|
|
1440
|
-
if (id) {
|
|
1441
|
-
ioViewsByToolCallId.set(id, view);
|
|
1442
|
-
// Bound growth in long sessions.
|
|
1443
|
-
if (ioViewsByToolCallId.size > 200) {
|
|
1444
|
-
const oldest = ioViewsByToolCallId.keys().next().value;
|
|
1445
|
-
if (oldest !== undefined) ioViewsByToolCallId.delete(oldest);
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1369
|
+
context.state.ccstyleIoView = view;
|
|
1448
1370
|
}
|
|
1449
1371
|
|
|
1450
|
-
function
|
|
1451
|
-
const
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
undefined;
|
|
1457
|
-
if (id) {
|
|
1458
|
-
const mapped = ioViewsByToolCallId.get(id);
|
|
1459
|
-
if (mapped) return mapped;
|
|
1460
|
-
}
|
|
1461
|
-
// Some hosts keep the last result component on the tool instance.
|
|
1462
|
-
const last = component?.lastComponent ?? component?.resultComponent ?? component?.content;
|
|
1463
|
-
if (isExpandedToolIoView(last)) return last;
|
|
1464
|
-
return null;
|
|
1372
|
+
function collapsedHintHitbox(line: string): { startCol: number; endCol: number } | null {
|
|
1373
|
+
const plain = stripTerminalSequencesPreservingLayout(line);
|
|
1374
|
+
const match = /(\([^()\n]* \/ click\)|click to show more)(?=\)?\s*$)/.exec(plain);
|
|
1375
|
+
if (!match?.[1]) return null;
|
|
1376
|
+
const startCol = visibleWidth(plain.slice(0, match.index)) + 1;
|
|
1377
|
+
return { startCol, endCol: startCol + visibleWidth(match[1]) - 1 };
|
|
1465
1378
|
}
|
|
1466
1379
|
|
|
1467
|
-
function
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
): { view: ExpandedToolIoView; section: ToolIoSection } | null {
|
|
1472
|
-
const plain = visibleMouseLine(tui, packet);
|
|
1473
|
-
const resolved = resolveIoViewFromTool(component);
|
|
1474
|
-
const renderedViews =
|
|
1475
|
-
frameIoViews.size > 0
|
|
1476
|
-
? [...frameIoViews]
|
|
1477
|
-
: [
|
|
1478
|
-
...(resolved ? [resolved] : []),
|
|
1479
|
-
...[...ioViewsByToolCallId.values()].filter((view) => view !== resolved),
|
|
1480
|
-
];
|
|
1481
|
-
const preferred = resolved && renderedViews.includes(resolved) ? resolved : null;
|
|
1482
|
-
const candidates = preferred
|
|
1483
|
-
? [preferred, ...renderedViews.filter((view) => view !== preferred)]
|
|
1484
|
-
: renderedViews;
|
|
1485
|
-
const previousLines = (Array.isArray(tui?.previousLines) ? tui.previousLines : []).map(
|
|
1486
|
-
(line: unknown) => stripTerminalSequencesPreservingLayout(String(line)),
|
|
1380
|
+
function interactionRegionAt(packet: SgrMousePacket): InteractionRegion | null {
|
|
1381
|
+
const matches = latestInteractionFrame.regions.filter(
|
|
1382
|
+
(region) =>
|
|
1383
|
+
region.row === packet.row && packet.col >= region.startCol && packet.col <= region.endCol,
|
|
1487
1384
|
);
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
const box = view.showMoreHitbox(plain);
|
|
1495
|
-
if (!section || !box || packet.col < box.startCol || packet.col > box.endCol) continue;
|
|
1496
|
-
const cachedLines = Array.isArray((view as any).cachedLines)
|
|
1497
|
-
? (view as any).cachedLines.map((line: unknown) =>
|
|
1498
|
-
stripTerminalSequencesPreservingLayout(String(line)),
|
|
1499
|
-
)
|
|
1500
|
-
: [];
|
|
1501
|
-
let score = view === preferred ? 1 : 0;
|
|
1502
|
-
for (let row = 0; row < cachedLines.length; row++) {
|
|
1503
|
-
if (!fixedEditorLineMatch(cachedLines[row] ?? "", plain)) continue;
|
|
1504
|
-
score = Math.max(score, fixedEditorContextScore(cachedLines, row, previousLines, visibleRow));
|
|
1505
|
-
}
|
|
1506
|
-
if (!best || score > best.score) best = { view, section, score };
|
|
1507
|
-
}
|
|
1508
|
-
return best && { view: best.view, section: best.section };
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
/**
|
|
1512
|
-
* If the click lands on a truncated section's [show more], open the /context-style
|
|
1513
|
-
* full-text preview instead of toggling expand/collapse.
|
|
1514
|
-
*/
|
|
1515
|
-
function visibleMouseLine(tui: any, packet: SgrMousePacket): string {
|
|
1516
|
-
const previousLines = Array.isArray(tui?.previousLines) ? tui.previousLines : [];
|
|
1517
|
-
const viewportTop = useFixedEditorFeatures(tui)
|
|
1518
|
-
? 0
|
|
1519
|
-
: Number.isFinite(tui?.previousViewportTop)
|
|
1520
|
-
? tui.previousViewportTop
|
|
1521
|
-
: 0;
|
|
1522
|
-
return stripTerminalSequencesPreservingLayout(
|
|
1523
|
-
String(previousLines[viewportTop + packet.row - 1] ?? ""),
|
|
1385
|
+
return (
|
|
1386
|
+
matches.find((region) => region.kind === "show-more") ??
|
|
1387
|
+
matches.find((region) => region.kind === "scroll-bottom") ??
|
|
1388
|
+
matches.find((region) => region.kind === "collapsed-hint") ??
|
|
1389
|
+
matches.find((region) => region.kind === "expanded-card") ??
|
|
1390
|
+
null
|
|
1524
1391
|
);
|
|
1525
1392
|
}
|
|
1526
1393
|
|
|
1527
|
-
function
|
|
1528
|
-
|
|
1529
|
-
const
|
|
1530
|
-
if (!
|
|
1531
|
-
const start = visibleWidth(line.slice(0, match.index)) + 1;
|
|
1532
|
-
return col >= start && col < start + visibleWidth(match[1]);
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
function tryOpenToolIoShowMore(tui: any, packet: SgrMousePacket, hit: ToolRenderHit): boolean {
|
|
1536
|
-
if (!Boolean(hit.component.expanded)) return false;
|
|
1537
|
-
const match = resolveShowMoreAt(tui, packet, hit.component);
|
|
1538
|
-
if (!match) return false;
|
|
1539
|
-
const { view: ioView, section } = match;
|
|
1540
|
-
|
|
1394
|
+
function tryOpenToolIoShowMore(region: InteractionRegion): boolean {
|
|
1395
|
+
const ioView = region.view;
|
|
1396
|
+
const section = region.section;
|
|
1397
|
+
if (!ioView || !section) return false;
|
|
1541
1398
|
const ui = toolMouseUi;
|
|
1542
1399
|
if (!ui || typeof ui.custom !== "function") {
|
|
1543
1400
|
ui?.notify?.("Full preview requires TUI custom UI", "warning");
|
|
1544
1401
|
return true;
|
|
1545
1402
|
}
|
|
1546
|
-
|
|
1547
1403
|
const title = section === "input" ? "Tool Input" : "Tool Output";
|
|
1548
1404
|
const content = section === "input" ? ioView.getInputBody() : ioView.getOutputBody();
|
|
1549
|
-
// Fire-and-forget overlay; click is still consumed.
|
|
1550
1405
|
void showTextPreview({ ui }, title, content || "(empty)");
|
|
1551
1406
|
return true;
|
|
1552
1407
|
}
|
|
@@ -1576,31 +1431,15 @@ function setHoveredToolGroup(group: ToolGroupComponent | null): boolean {
|
|
|
1576
1431
|
|
|
1577
1432
|
function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
1578
1433
|
if ((packet.code & 32) === 0 || packet.final !== "M") return;
|
|
1579
|
-
const
|
|
1434
|
+
const region = interactionRegionAt(packet);
|
|
1435
|
+
const nextScrollButtonHovered = region?.kind === "scroll-bottom";
|
|
1580
1436
|
const scrollButtonChanged = nextScrollButtonHovered !== scrollButtonHovered;
|
|
1581
1437
|
scrollButtonHovered = nextScrollButtonHovered;
|
|
1582
|
-
const
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
if (
|
|
1589
|
-
COLLAPSED_TOOL_SUMMARY.test(visibleLine) &&
|
|
1590
|
-
isCollapsedHintAtColumn(visibleLine, packet.col)
|
|
1591
|
-
) {
|
|
1592
|
-
const hit = findToolAtScreenRow(tui, packet.row);
|
|
1593
|
-
nextToolCallId = hit && !hit.component.expanded ? hit.component.toolCallId : null;
|
|
1594
|
-
nextGroup = hit?.component instanceof ToolGroupComponent ? hit.component : null;
|
|
1595
|
-
} else if (visibleLine.includes(SHOW_MORE_LABEL)) {
|
|
1596
|
-
const hit = findToolAtScreenRow(tui, packet.row);
|
|
1597
|
-
const match = resolveShowMoreAt(tui, packet, hit?.component);
|
|
1598
|
-
if (match) {
|
|
1599
|
-
nextIoView = match.view;
|
|
1600
|
-
nextIoSection = match.section;
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1438
|
+
const component = region?.component;
|
|
1439
|
+
const nextToolCallId = region?.kind === "collapsed-hint" ? (component?.toolCallId ?? null) : null;
|
|
1440
|
+
const nextGroup = component instanceof ToolGroupComponent ? component : null;
|
|
1441
|
+
const nextIoView = region?.kind === "show-more" ? (region.view ?? null) : null;
|
|
1442
|
+
const nextIoSection = region?.kind === "show-more" ? (region.section ?? null) : null;
|
|
1604
1443
|
const changed = nextToolCallId !== hoveredToolCallId;
|
|
1605
1444
|
hoveredToolCallId = nextToolCallId;
|
|
1606
1445
|
if (
|
|
@@ -1612,49 +1451,31 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
|
|
|
1612
1451
|
tui.requestRender?.();
|
|
1613
1452
|
}
|
|
1614
1453
|
|
|
1615
|
-
function isExpandedToolCardHit(hit: ToolRenderHit, width: number): boolean {
|
|
1616
|
-
if (!Boolean(hit.component.expanded)) return false;
|
|
1617
|
-
if (hit.component instanceof ToolGroupComponent) return true;
|
|
1618
|
-
const box = hit.component.contentBox;
|
|
1619
|
-
if (!box || !Array.isArray(hit.component.children) || !hit.component.children.includes(box)) {
|
|
1620
|
-
return false;
|
|
1621
|
-
}
|
|
1622
|
-
const boxLines = box.render?.(width);
|
|
1623
|
-
const componentLines = renderComponentTree(hit.component, width);
|
|
1624
|
-
if (!Array.isArray(boxLines) || boxLines.length === 0) return false;
|
|
1625
|
-
const cardStart = componentLines.length - boxLines.length;
|
|
1626
|
-
return hit.row >= cardStart && hit.row < componentLines.length;
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
1454
|
function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
|
|
1630
|
-
const
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1455
|
+
const region = interactionRegionAt(packet);
|
|
1456
|
+
if (!region) return false;
|
|
1457
|
+
if (region.kind === "scroll-bottom") return jumpToBottomWithoutSubmit(tui);
|
|
1458
|
+
if (region.kind === "show-more") return tryOpenToolIoShowMore(region);
|
|
1459
|
+
const component = region.component;
|
|
1460
|
+
if (!component) return false;
|
|
1636
1461
|
const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
|
|
1637
|
-
if (
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
const previousHeight = renderComponentTree(hit.component, width).length;
|
|
1641
|
-
hit.component.setExpanded(false);
|
|
1462
|
+
if (region.kind === "expanded-card") {
|
|
1463
|
+
const previousHeight = renderComponentTree(component, width).length;
|
|
1464
|
+
component.setExpanded(false);
|
|
1642
1465
|
hoveredToolCallId = null;
|
|
1643
1466
|
setHoveredToolGroup(null);
|
|
1644
1467
|
setHoveredToolIo(null, null);
|
|
1645
|
-
|
|
1646
|
-
const nextHeight = renderComponentTree(
|
|
1468
|
+
component.invalidate?.();
|
|
1469
|
+
const nextHeight = renderComponentTree(component, width).length;
|
|
1647
1470
|
tui.requestRender?.();
|
|
1648
1471
|
scheduleCollapseViewportCompensation(tui, previousHeight - nextHeight, packet);
|
|
1649
1472
|
return true;
|
|
1650
1473
|
}
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
hit.component.setExpanded(true);
|
|
1474
|
+
component.setExpanded(true);
|
|
1654
1475
|
hoveredToolCallId = null;
|
|
1655
1476
|
setHoveredToolGroup(null);
|
|
1656
1477
|
setHoveredToolIo(null, null);
|
|
1657
|
-
|
|
1478
|
+
component.invalidate?.();
|
|
1658
1479
|
tui.requestRender?.();
|
|
1659
1480
|
return true;
|
|
1660
1481
|
}
|
|
@@ -1758,82 +1579,248 @@ function restoreToolMouseRenderPatch(): void {
|
|
|
1758
1579
|
toolMouseRenderPatchWrapper = null;
|
|
1759
1580
|
toolMouseRenderPatchState = null;
|
|
1760
1581
|
toolMouseRawWrite = null;
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1582
|
+
latestInteractionFrame = { regions: [] };
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
function buildInteractionFrame(
|
|
1586
|
+
tui: any,
|
|
1587
|
+
renderedTools: FrameToolRender[],
|
|
1588
|
+
placements: FrameToolPlacement[],
|
|
1589
|
+
): InteractionFrame {
|
|
1590
|
+
const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
|
|
1591
|
+
const fixed = useFixedEditorFeatures(tui);
|
|
1592
|
+
const viewport = fixed ? getFixedEditorViewport(tui) : null;
|
|
1593
|
+
// fixed-editor: tui.render already returned the visible root slice (screen rows).
|
|
1594
|
+
// native: full buffer; map with the post-doRender previousViewportTop.
|
|
1595
|
+
const lineIndexToScreenRow = (lineIndex: number) =>
|
|
1596
|
+
fixed ? lineIndex + 1 : lineIndex - (Number(tui?.previousViewportTop) || 0) + 1;
|
|
1597
|
+
const visibleRows = fixed
|
|
1598
|
+
? (viewport?.visibleLines.length ??
|
|
1599
|
+
(Array.isArray(tui?.previousLines) ? tui.previousLines.length : Number.POSITIVE_INFINITY))
|
|
1600
|
+
: Math.max(1, Number(tui?.terminal?.rows) || Number.POSITIVE_INFINITY);
|
|
1601
|
+
const regions: InteractionRegion[] = [];
|
|
1602
|
+
const renderedByComponent = new Map<any, FrameToolRender>();
|
|
1603
|
+
for (const rendered of renderedTools) renderedByComponent.set(rendered.component, rendered);
|
|
1604
|
+
const placementsByComponent = new Map<any, FrameToolPlacement[]>();
|
|
1605
|
+
for (const placement of placements) {
|
|
1606
|
+
const list = placementsByComponent.get(placement.component) ?? [];
|
|
1607
|
+
list.push(placement);
|
|
1608
|
+
placementsByComponent.set(placement.component, list);
|
|
1609
|
+
}
|
|
1610
|
+
for (const [component, componentPlacements] of placementsByComponent) {
|
|
1611
|
+
const rendered = renderedByComponent.get(component);
|
|
1612
|
+
if (!rendered) continue;
|
|
1613
|
+
for (const placement of componentPlacements) {
|
|
1614
|
+
const finalRow = lineIndexToScreenRow(placement.lineIndex);
|
|
1615
|
+
if (finalRow < 1 || finalRow > visibleRows) continue;
|
|
1616
|
+
// Hit columns come from the final painted line (parent may prefix/transform).
|
|
1617
|
+
const line = placement.finalLine;
|
|
1618
|
+
if (!component.expanded) {
|
|
1619
|
+
const box = collapsedHintHitbox(line);
|
|
1620
|
+
if (box && COLLAPSED_TOOL_SUMMARY.test(stripTerminalSequences(line))) {
|
|
1621
|
+
regions.push({ kind: "collapsed-hint", row: finalRow, ...box, component });
|
|
1622
|
+
}
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
if (placement.view && placement.section) {
|
|
1626
|
+
const plain = stripTerminalSequencesPreservingLayout(line);
|
|
1627
|
+
const box = placement.view.showMoreHitbox(plain);
|
|
1628
|
+
if (box) {
|
|
1629
|
+
regions.push({
|
|
1630
|
+
kind: "show-more",
|
|
1631
|
+
row: finalRow,
|
|
1632
|
+
...box,
|
|
1633
|
+
component,
|
|
1634
|
+
view: placement.view,
|
|
1635
|
+
section: placement.section,
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
if (!component.expanded) continue;
|
|
1641
|
+
let cardStart = 0;
|
|
1642
|
+
if (!(component instanceof ToolGroupComponent)) {
|
|
1643
|
+
const box = component.contentBox;
|
|
1644
|
+
if (!box || !Array.isArray(component.children) || !component.children.includes(box)) {
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
if (!rendered.contentBoxLines) continue;
|
|
1648
|
+
cardStart = Math.max(0, rendered.lines.length - rendered.contentBoxLines);
|
|
1649
|
+
}
|
|
1650
|
+
for (const placement of componentPlacements) {
|
|
1651
|
+
if (placement.componentRow < cardStart) continue;
|
|
1652
|
+
const finalRow = lineIndexToScreenRow(placement.lineIndex);
|
|
1653
|
+
if (finalRow >= 1 && finalRow <= visibleRows) {
|
|
1654
|
+
regions.push({
|
|
1655
|
+
kind: "expanded-card",
|
|
1656
|
+
row: finalRow,
|
|
1657
|
+
startCol: 1,
|
|
1658
|
+
endCol: width,
|
|
1659
|
+
component,
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
const scrollHitbox = getFixedEditorScrollButtonHitbox();
|
|
1665
|
+
if (scrollButtonVisible && useFixedEditorFeatures(tui) && scrollHitbox) {
|
|
1666
|
+
regions.push({ kind: "scroll-bottom", ...scrollHitbox });
|
|
1667
|
+
}
|
|
1668
|
+
return { regions };
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function defineRenderOverride(
|
|
1672
|
+
target: any,
|
|
1673
|
+
wrapped: (...args: any[]) => any,
|
|
1674
|
+
): PropertyDescriptor | undefined {
|
|
1675
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, "render");
|
|
1676
|
+
try {
|
|
1677
|
+
Object.defineProperty(
|
|
1678
|
+
target,
|
|
1679
|
+
"render",
|
|
1680
|
+
descriptor && "value" in descriptor
|
|
1681
|
+
? { ...descriptor, value: wrapped }
|
|
1682
|
+
: {
|
|
1683
|
+
configurable: true,
|
|
1684
|
+
enumerable: descriptor?.enumerable ?? false,
|
|
1685
|
+
writable: true,
|
|
1686
|
+
value: wrapped,
|
|
1687
|
+
},
|
|
1688
|
+
);
|
|
1689
|
+
return descriptor;
|
|
1690
|
+
} catch {
|
|
1691
|
+
return undefined;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
function restoreRenderOverride(target: any, descriptor: PropertyDescriptor | undefined): void {
|
|
1696
|
+
try {
|
|
1697
|
+
if (descriptor) Object.defineProperty(target, "render", descriptor);
|
|
1698
|
+
else delete target.render;
|
|
1699
|
+
} catch {
|
|
1700
|
+
// Keep restoring siblings after a hostile descriptor change.
|
|
1701
|
+
}
|
|
1764
1702
|
}
|
|
1765
1703
|
|
|
1766
1704
|
function patchToolMouseMotionAfterRender(tui: any): void {
|
|
1767
|
-
|
|
1705
|
+
// Same tui is not enough: footer/compositor rebuild may replace doRender under us.
|
|
1706
|
+
if (
|
|
1707
|
+
toolMouseRenderPatchTui === tui &&
|
|
1708
|
+
toolMouseRenderPatchState?.active &&
|
|
1709
|
+
tui.doRender === toolMouseRenderPatchWrapper
|
|
1710
|
+
) {
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1768
1713
|
restoreToolMouseRenderPatch();
|
|
1769
1714
|
const original = tui?.doRender;
|
|
1770
1715
|
const terminal = tui?.terminal;
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
while (prototype && !rawWrite) {
|
|
1774
|
-
const candidate = Object.getOwnPropertyDescriptor(prototype, "write")?.value;
|
|
1775
|
-
if (typeof candidate === "function") rawWrite = candidate;
|
|
1776
|
-
prototype = Object.getPrototypeOf(prototype);
|
|
1777
|
-
}
|
|
1778
|
-
if (typeof original !== "function" || !rawWrite) return;
|
|
1716
|
+
const rawWrite = typeof terminal?.write === "function" ? terminal.write : undefined;
|
|
1717
|
+
if (typeof original !== "function") return;
|
|
1779
1718
|
|
|
1780
|
-
toolMouseRawWrite = (data) => Reflect.apply(rawWrite, terminal, [data]);
|
|
1719
|
+
toolMouseRawWrite = rawWrite ? (data) => Reflect.apply(rawWrite, terminal, [data]) : null;
|
|
1781
1720
|
const patchState = { active: true };
|
|
1782
1721
|
const wrapper = function (this: any, ...args: any[]) {
|
|
1783
1722
|
if (!patchState.active) return Reflect.apply(original, this, args);
|
|
1784
|
-
const renderedViews = new Set<ExpandedToolIoView>();
|
|
1785
1723
|
const renderedTools: FrameToolRender[] = [];
|
|
1786
|
-
const
|
|
1787
|
-
const
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1724
|
+
const idToComponent = new Map<number, any>();
|
|
1725
|
+
const frame: IoViewFrameState = {
|
|
1726
|
+
viewIds: new Map(),
|
|
1727
|
+
idToView: new Map(),
|
|
1728
|
+
nextId: 0,
|
|
1729
|
+
};
|
|
1730
|
+
const restores: Array<{ target: any; descriptor?: PropertyDescriptor }> = [];
|
|
1731
|
+
const outermost: any[] = [];
|
|
1732
|
+
collectToolComponents(this, outermost);
|
|
1733
|
+
let nextId = 0;
|
|
1734
|
+
for (const component of outermost) {
|
|
1791
1735
|
const originalRender = component.render;
|
|
1792
1736
|
if (typeof originalRender !== "function") continue;
|
|
1737
|
+
const id = nextId++;
|
|
1738
|
+
idToComponent.set(id, component);
|
|
1793
1739
|
const wrappedRender = function (this: any, ...renderArgs: any[]) {
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1740
|
+
let contentBoxLines = 0;
|
|
1741
|
+
const box = component.contentBox;
|
|
1742
|
+
let boxRestore: { target: any; descriptor?: PropertyDescriptor } | undefined;
|
|
1743
|
+
if (
|
|
1744
|
+
box &&
|
|
1745
|
+
Array.isArray(component.children) &&
|
|
1746
|
+
component.children.includes(box) &&
|
|
1747
|
+
typeof box.render === "function"
|
|
1748
|
+
) {
|
|
1749
|
+
const boxOriginal = box.render;
|
|
1750
|
+
const boxWrapped = function (this: any, ...boxArgs: any[]) {
|
|
1751
|
+
const boxLines = Reflect.apply(boxOriginal, this, boxArgs);
|
|
1752
|
+
if (Array.isArray(boxLines)) contentBoxLines = boxLines.length;
|
|
1753
|
+
return boxLines;
|
|
1754
|
+
};
|
|
1755
|
+
const boxDescriptor = defineRenderOverride(box, boxWrapped);
|
|
1756
|
+
if (boxDescriptor !== undefined || box.render === boxWrapped) {
|
|
1757
|
+
boxRestore = { target: box, descriptor: boxDescriptor };
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
try {
|
|
1761
|
+
const lines = Reflect.apply(originalRender, this, renderArgs);
|
|
1762
|
+
if (!Array.isArray(lines)) return lines;
|
|
1763
|
+
renderedTools.push({
|
|
1764
|
+
component,
|
|
1765
|
+
lines: lines.map((line) => String(line)),
|
|
1766
|
+
contentBoxLines,
|
|
1767
|
+
});
|
|
1768
|
+
return lines.map((line, row) => `${line}${toolFrameMarker(id, row)}`);
|
|
1769
|
+
} finally {
|
|
1770
|
+
if (boxRestore) restoreRenderOverride(boxRestore.target, boxRestore.descriptor);
|
|
1771
|
+
}
|
|
1797
1772
|
};
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
"render",
|
|
1802
|
-
descriptor && "value" in descriptor
|
|
1803
|
-
? { ...descriptor, value: wrappedRender }
|
|
1804
|
-
: {
|
|
1805
|
-
configurable: true,
|
|
1806
|
-
enumerable: descriptor?.enumerable ?? false,
|
|
1807
|
-
writable: true,
|
|
1808
|
-
value: wrappedRender,
|
|
1809
|
-
},
|
|
1810
|
-
);
|
|
1811
|
-
restores.push({ component, descriptor });
|
|
1812
|
-
} catch {
|
|
1813
|
-
// Non-configurable render accessors cannot be observed safely.
|
|
1773
|
+
const descriptor = defineRenderOverride(component, wrappedRender);
|
|
1774
|
+
if (descriptor !== undefined || component.render === wrappedRender) {
|
|
1775
|
+
restores.push({ target: component, descriptor });
|
|
1814
1776
|
}
|
|
1815
1777
|
}
|
|
1816
|
-
|
|
1778
|
+
let placements: FrameToolPlacement[] = [];
|
|
1779
|
+
const originalTuiRender = typeof this.render === "function" ? this.render : null;
|
|
1780
|
+
let tuiRenderDescriptor: PropertyDescriptor | undefined;
|
|
1781
|
+
let sawTuiRender = false;
|
|
1782
|
+
if (originalTuiRender) {
|
|
1783
|
+
const wrappedTuiRender = function (this: any, ...renderArgs: any[]) {
|
|
1784
|
+
const lines = Reflect.apply(originalTuiRender, this, renderArgs);
|
|
1785
|
+
if (!Array.isArray(lines)) return lines;
|
|
1786
|
+
sawTuiRender = true;
|
|
1787
|
+
const extracted = extractToolFramePlacements(
|
|
1788
|
+
lines.map((line) => String(line)),
|
|
1789
|
+
idToComponent,
|
|
1790
|
+
frame.idToView,
|
|
1791
|
+
);
|
|
1792
|
+
placements = extracted.placements;
|
|
1793
|
+
return extracted.lines;
|
|
1794
|
+
};
|
|
1795
|
+
tuiRenderDescriptor = defineRenderOverride(this, wrappedTuiRender);
|
|
1796
|
+
}
|
|
1817
1797
|
let succeeded = false;
|
|
1798
|
+
const previousFrame = activeIoViewFrame;
|
|
1799
|
+
activeIoViewFrame = frame;
|
|
1818
1800
|
try {
|
|
1819
1801
|
const result = Reflect.apply(original, this, args);
|
|
1820
1802
|
succeeded = true;
|
|
1821
|
-
|
|
1803
|
+
// Test harnesses may paint via doRender without tui.render; recover markers there.
|
|
1804
|
+
if (!sawTuiRender && Array.isArray(this.previousLines)) {
|
|
1805
|
+
const extracted = extractToolFramePlacements(
|
|
1806
|
+
this.previousLines.map((line: unknown) => String(line)),
|
|
1807
|
+
idToComponent,
|
|
1808
|
+
frame.idToView,
|
|
1809
|
+
);
|
|
1810
|
+
this.previousLines = extracted.lines;
|
|
1811
|
+
placements = extracted.placements;
|
|
1812
|
+
}
|
|
1813
|
+
if (!useFixedEditorFeatures(this)) toolMouseRawWrite?.(TOOL_MOUSE_MOTION_ENABLE);
|
|
1822
1814
|
return result;
|
|
1823
1815
|
} finally {
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
} catch {
|
|
1829
|
-
// Keep restoring the remaining instances after a hostile descriptor change.
|
|
1830
|
-
}
|
|
1816
|
+
activeIoViewFrame = previousFrame;
|
|
1817
|
+
if (originalTuiRender) restoreRenderOverride(this, tuiRenderDescriptor);
|
|
1818
|
+
for (const { target, descriptor } of restores.reverse()) {
|
|
1819
|
+
restoreRenderOverride(target, descriptor);
|
|
1831
1820
|
}
|
|
1832
1821
|
if (succeeded) {
|
|
1833
|
-
|
|
1834
|
-
frameToolRenders = renderedTools;
|
|
1822
|
+
latestInteractionFrame = buildInteractionFrame(this, renderedTools, placements);
|
|
1835
1823
|
}
|
|
1836
|
-
ioViewCollectors.delete(renderedViews);
|
|
1837
1824
|
}
|
|
1838
1825
|
};
|
|
1839
1826
|
try {
|
|
@@ -1846,7 +1833,7 @@ function patchToolMouseMotionAfterRender(tui: any): void {
|
|
|
1846
1833
|
toolMouseRenderPatchOriginal = original;
|
|
1847
1834
|
toolMouseRenderPatchWrapper = wrapper;
|
|
1848
1835
|
toolMouseRenderPatchState = patchState;
|
|
1849
|
-
toolMouseRawWrite(TOOL_MOUSE_MOTION_ENABLE);
|
|
1836
|
+
if (!useFixedEditorFeatures(tui)) toolMouseRawWrite?.(TOOL_MOUSE_MOTION_ENABLE);
|
|
1850
1837
|
}
|
|
1851
1838
|
|
|
1852
1839
|
function handleToolMouseInput(data: string): { consume: true } | undefined {
|
|
@@ -1894,7 +1881,7 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
|
|
|
1894
1881
|
return consumed ? { consume: true } : undefined;
|
|
1895
1882
|
}
|
|
1896
1883
|
|
|
1897
|
-
function teardownToolMouseInteraction(): void {
|
|
1884
|
+
function teardownToolMouseInteraction(nextFixedEditorFeatures = false): void {
|
|
1898
1885
|
if (sessionRenderTimer) {
|
|
1899
1886
|
clearTimeout(sessionRenderTimer);
|
|
1900
1887
|
sessionRenderTimer = null;
|
|
@@ -1905,7 +1892,9 @@ function teardownToolMouseInteraction(): void {
|
|
|
1905
1892
|
setHoveredToolGroup(null);
|
|
1906
1893
|
setHoveredToolIo(null, null);
|
|
1907
1894
|
try {
|
|
1908
|
-
|
|
1895
|
+
if (!toolMouseFixedFeaturesEnabled && !nextFixedEditorFeatures) {
|
|
1896
|
+
toolMouseTui?.terminal?.write?.(TOOL_MOUSE_DISABLE);
|
|
1897
|
+
}
|
|
1909
1898
|
} catch {
|
|
1910
1899
|
// The terminal may already be closed during shutdown.
|
|
1911
1900
|
}
|
|
@@ -1934,7 +1923,7 @@ export function installToolMouseInteraction(
|
|
|
1934
1923
|
ctx: any,
|
|
1935
1924
|
fixedEditorFeatures = config.fixedEditorFeatures,
|
|
1936
1925
|
): void {
|
|
1937
|
-
teardownToolMouseInteraction();
|
|
1926
|
+
teardownToolMouseInteraction(fixedEditorFeatures);
|
|
1938
1927
|
if (ctx?.mode !== "tui" || !ctx?.hasUI) return;
|
|
1939
1928
|
if (typeof ctx.ui?.onTerminalInput !== "function" || typeof ctx.ui?.setWidget !== "function")
|
|
1940
1929
|
return;
|
|
@@ -1943,12 +1932,13 @@ export function installToolMouseInteraction(
|
|
|
1943
1932
|
toolMouseFixedFeaturesEnabled = fixedEditorFeatures;
|
|
1944
1933
|
ctx.ui.setWidget(TOOL_MOUSE_WIDGET_KEY, (tui: any, theme: any) => {
|
|
1945
1934
|
toolMouseTui = tui;
|
|
1946
|
-
if (fixedEditorFeatures)
|
|
1947
|
-
|
|
1935
|
+
if (fixedEditorFeatures) patchToolMouseInputCapture(tui);
|
|
1936
|
+
// Fixed mode: wrap only after compositor install makes features live.
|
|
1937
|
+
if (!fixedEditorFeatures || useFixedEditorFeatures(tui)) {
|
|
1948
1938
|
patchToolMouseMotionAfterRender(tui);
|
|
1949
1939
|
}
|
|
1950
|
-
//
|
|
1951
|
-
tui?.terminal?.write?.(
|
|
1940
|
+
// The fixed-editor compositor owns its mouse mode; native Pi still needs motion enabled.
|
|
1941
|
+
if (!fixedEditorFeatures) tui?.terminal?.write?.(TOOL_MOUSE_MOTION_ENABLE);
|
|
1952
1942
|
const widget = {
|
|
1953
1943
|
render: (width: number) => renderScrollButton(width, theme),
|
|
1954
1944
|
invalidate() {},
|
|
@@ -1975,7 +1965,10 @@ function scheduleSessionRender(refresh?: () => void): void {
|
|
|
1975
1965
|
sessionRenderTimer = setTimeout(() => {
|
|
1976
1966
|
sessionRenderTimer = null;
|
|
1977
1967
|
if (toolMouseTui !== tui) return;
|
|
1978
|
-
|
|
1968
|
+
// Do not pre-wrap while fixed-editor compositor install is still pending.
|
|
1969
|
+
if (!toolMouseFixedFeaturesEnabled || useFixedEditorFeatures(tui)) {
|
|
1970
|
+
patchToolMouseMotionAfterRender(tui);
|
|
1971
|
+
}
|
|
1979
1972
|
refreshToolRendererComponents(tui);
|
|
1980
1973
|
refresh?.();
|
|
1981
1974
|
tui.requestRender(true);
|
|
@@ -2485,9 +2478,10 @@ function singleToolCallSummary(
|
|
|
2485
2478
|
if (AGENT_FAMILY_TOOL_NAMES.has(toolName) && args.agent_id) {
|
|
2486
2479
|
return { main: `${title} ${oneLine(args.agent_id, Infinity)}`, detail: "" };
|
|
2487
2480
|
}
|
|
2488
|
-
|
|
2481
|
+
// Agents still uses the ccstyle wrapper; Agent keeps its dedicated renderer.
|
|
2482
|
+
if (name === "agents") {
|
|
2489
2483
|
return {
|
|
2490
|
-
main: value(
|
|
2484
|
+
main: value("launch agents", "description", "prompt"),
|
|
2491
2485
|
detail: "",
|
|
2492
2486
|
};
|
|
2493
2487
|
}
|
|
@@ -2797,6 +2791,8 @@ const AGENT_FAMILY_TOOL_NAMES = new Set([
|
|
|
2797
2791
|
"get_subagent_result",
|
|
2798
2792
|
"steer_subagent",
|
|
2799
2793
|
]);
|
|
2794
|
+
// pi-subagents 等扩展为 Agent 提供专用渲染器(displayName/运行统计),ccstyle 必须保留,不能 wrap。
|
|
2795
|
+
const DEDICATED_RENDERER_TOOLS = new Set(["Agent"]);
|
|
2800
2796
|
|
|
2801
2797
|
type ToolRenderMethods = {
|
|
2802
2798
|
hasRendererDefinition: (...args: any[]) => boolean;
|
|
@@ -2885,6 +2881,7 @@ function shouldGloballyStyleTool(component: any, patch: GlobalToolRenderPatch):
|
|
|
2885
2881
|
const toolName = String(component.toolName || definition?.name || "");
|
|
2886
2882
|
const useCcstyle =
|
|
2887
2883
|
patch.mode() === "on" &&
|
|
2884
|
+
!DEDICATED_RENDERER_TOOLS.has(toolName) &&
|
|
2888
2885
|
!preservesOriginalRenderer(extensionDefinition, toolName, builtInDefinition);
|
|
2889
2886
|
component[COMPONENT_TOOL_RENDER_MODE] = useCcstyle;
|
|
2890
2887
|
return useCcstyle;
|
|
@@ -3129,6 +3126,18 @@ export default function (
|
|
|
3129
3126
|
// The optional override keeps integration tests independent from the user's global config.
|
|
3130
3127
|
if (configOverride) config = normalizeConfig({ ...config, ...configOverride });
|
|
3131
3128
|
const fixedEditorController = installFixedEditor(pi, config.fixedEditorFeatures);
|
|
3129
|
+
// Unwrap mouse doRender before compositor construction captures the chain.
|
|
3130
|
+
setBeforeFixedEditorStart(() => {
|
|
3131
|
+
restoreToolMouseRenderPatch();
|
|
3132
|
+
});
|
|
3133
|
+
// Footer/compositor rebuild disposes the old chain; re-wrap the live doRender and repaint.
|
|
3134
|
+
fixedEditorController.onRebuild(() => {
|
|
3135
|
+
const tui = toolMouseTui;
|
|
3136
|
+
if (!tui) return;
|
|
3137
|
+
if (toolMouseFixedFeaturesEnabled) patchToolMouseInputCapture(tui);
|
|
3138
|
+
patchToolMouseMotionAfterRender(tui);
|
|
3139
|
+
tui.requestRender?.(true);
|
|
3140
|
+
});
|
|
3132
3141
|
const writeExecutionMetadata = installWriteOverride(pi, new WriteExecutionMetadataStore());
|
|
3133
3142
|
let installation:
|
|
3134
3143
|
| {
|