pi-cc-extensions 0.8.45 → 0.8.47

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.
@@ -1,20 +1,8 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { getSettingsListTheme, ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
3
3
  import { isFullscreenUi, isLazyProxyTui } from "./fullscreen-detect.ts";
4
- import {
5
- installCompactStyle,
6
- type CompactStyleHooks,
7
- type CompactStyleMode,
8
- } from "./compact-style.ts";
9
4
  import type { CompactThinkingConfig, CompactThinkingController } from "./compact-thinking.ts";
10
- import { showTextPreview } from "./context.ts";
11
- import {
12
- getFixedEditorScrollButtonHitbox,
13
- getFixedEditorViewport,
14
- installFixedEditor,
15
- setBeforeFixedEditorStart,
16
- type FixedEditorController,
17
- } from "./fixed-editor.ts";
5
+ import { hasActiveTextPreview, showTextPreview } from "./context.ts";
18
6
  import {
19
7
  installToolGrouping,
20
8
  ToolGroupComponent,
@@ -34,6 +22,7 @@ import {
34
22
  import {
35
23
  SettingsList,
36
24
  Text,
25
+ getKeybindings,
37
26
  matchesKey,
38
27
  truncateToWidth,
39
28
  visibleWidth,
@@ -47,14 +36,15 @@ import { inspect } from "node:util";
47
36
  /**
48
37
  * Claude Code Style for pi.
49
38
  *
50
- * This is the package's only entry point. Compact transcript rendering lives in
51
- * the internal compact-style module and is routed by the mode below.
39
+ * This is the package's only entry point. Tool rendering (summaries, rich
40
+ * edit/write diffs, expand/collapse) is applied via prototype-level patches.
52
41
  */
53
42
 
43
+ export type CompactStyleMode = "on" | "off";
44
+
54
45
  export type Config = {
55
46
  mode: CompactStyleMode;
56
47
  excludeRenderers: string[];
57
- fixedEditorFeatures: boolean;
58
48
  diffViewMode: DiffViewMode;
59
49
  diffIndicatorMode: DiffIndicatorMode;
60
50
  diffSplitMinWidth: number;
@@ -93,7 +83,6 @@ const EXCLUDE_RENDERER_CANDIDATES = [
93
83
  export const DEFAULT_CONFIG: Config = {
94
84
  mode: "on",
95
85
  excludeRenderers: [],
96
- fixedEditorFeatures: true,
97
86
  diffViewMode: DEFAULT_TOOL_DISPLAY_CONFIG.diffViewMode,
98
87
  diffIndicatorMode: DEFAULT_TOOL_DISPLAY_CONFIG.diffIndicatorMode,
99
88
  diffSplitMinWidth: DEFAULT_TOOL_DISPLAY_CONFIG.diffSplitMinWidth,
@@ -141,8 +130,9 @@ function nearestPreset(value: number, presets: readonly string[]): string {
141
130
  export function normalizeConfig(input: unknown): Config {
142
131
  const source = (input && typeof input === "object" ? input : {}) as Record<string, unknown>;
143
132
  const mode = source.mode;
133
+ // mode=compact 已日落:旧配置回退到 on(Claude Code 风格)。
144
134
  const migratedMode: CompactStyleMode =
145
- mode === "on" || mode === "off" || mode === "compact"
135
+ mode === "on" || mode === "off"
146
136
  ? mode
147
137
  : typeof source.enabled === "boolean"
148
138
  ? source.enabled
@@ -158,11 +148,9 @@ export function normalizeConfig(input: unknown): Config {
158
148
  ),
159
149
  ]
160
150
  : [];
161
- const fixedEditorFeatures = source.fixedEditorFeatures !== false;
162
151
  return {
163
152
  mode: migratedMode,
164
153
  excludeRenderers,
165
- fixedEditorFeatures,
166
154
  diffViewMode: pickEnum(source.diffViewMode, DIFF_VIEW_MODES, DEFAULT_CONFIG.diffViewMode),
167
155
  diffIndicatorMode: pickEnum(
168
156
  source.diffIndicatorMode,
@@ -249,12 +237,7 @@ function loadConfig(): Config {
249
237
  ? (JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Record<string, unknown>)
250
238
  : {};
251
239
  const normalized = normalizeConfig(source);
252
- if (
253
- typeof source.enabled === "boolean" &&
254
- source.mode !== "on" &&
255
- source.mode !== "off" &&
256
- source.mode !== "compact"
257
- ) {
240
+ if (typeof source.enabled === "boolean" && source.mode !== "on" && source.mode !== "off") {
258
241
  try {
259
242
  writeFileSync(CONFIG_PATH, JSON.stringify(normalized, null, 2));
260
243
  } catch {
@@ -987,18 +970,9 @@ const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
987
970
  const ZENTUI_PAGE_UP_INPUT = /^\x1b\[5;9(?::[12])?~$|^\x1b\[57421;9(?::[12])?u$|^\x1b\[1;6A$/;
988
971
  const ZENTUI_PAGE_DOWN_INPUT = /^\x1b\[6;9(?::[12])?~$|^\x1b\[57422;9(?::[12])?u$|^\x1b\[1;6B$/;
989
972
  const SCROLL_BOTTOM_SHORTCUT = "ctrl+end";
990
- const ZENTUI_WHEEL_ROWS = 3;
991
- const FIXED_EDITOR_WHEEL_ROWS = 5;
992
973
  let toolMouseTui: any = null;
993
974
  let toolMouseUi: any = null;
994
- let toolMouseFixedFeaturesEnabled = false;
995
- let wheelExtraRowRemainder = 0;
996
- let lastWheelDirection: "up" | "down" | null = null;
997
- let collapseCompensationRemainder = 0;
998
975
  let toolMouseInputUnsubscribe: (() => void) | null = null;
999
- let toolMouseInputPatchTui: any = null;
1000
- let toolMouseInputPatchOriginalHandle: ((...args: any[]) => any) | null = null;
1001
- let toolMouseInputPatchWrapper: ((...args: any[]) => any) | null = null;
1002
976
  let toolMouseRenderPatchTui: any = null;
1003
977
  let toolMouseRenderPatchOriginal: ((...args: any[]) => any) | null = null;
1004
978
  let toolMouseRenderPatchWrapper: ((...args: any[]) => any) | null = null;
@@ -1007,8 +981,6 @@ let toolMouseRawWrite: ((data: string) => unknown) | null = null;
1007
981
  let scrollButtonVisible = false;
1008
982
  let scrollButtonHovered = false;
1009
983
  let scrollButtonWidget: any = null;
1010
- let pendingScrollMessages = 0;
1011
- let assistantMessageActive = false;
1012
984
  let scrollButtonSyncScheduled = false;
1013
985
  let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
1014
986
  let hoveredToolCallId: string | null = null;
@@ -1113,24 +1085,10 @@ function extractToolFramePlacements(
1113
1085
  /** Summary markers used by Pi and ccstyle; unlike the trailing hint, these survive truncation. */
1114
1086
  const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿|●|✓|✗|…)/;
1115
1087
 
1116
- function isFixedEditorTui(tui: any): boolean {
1117
- const terminal = tui?.terminal;
1118
- if (!terminal) return false;
1119
- const ownRows = Object.getOwnPropertyDescriptor(terminal, "rows");
1120
- const prototype = Object.getPrototypeOf(terminal);
1121
- const inheritedRows = prototype ? Object.getOwnPropertyDescriptor(prototype, "rows") : undefined;
1122
- return typeof ownRows?.get === "function" && ownRows.get !== inheritedRows?.get;
1123
- }
1124
-
1125
- function useFixedEditorFeatures(tui: any): boolean {
1126
- return toolMouseFixedFeaturesEnabled && isFixedEditorTui(tui);
1127
- }
1128
-
1129
- /** Mouse hover/click affordances need ccstyle rendering AND the fixed editor. Without
1130
- * the fixed editor, Pi's native TUI has no mouse handling, so any reporting mode leaves
1131
- * the wheel in a dead zone — native mode must leave reporting off entirely. */
1132
1088
  function toolMouseInteractionActive(): boolean {
1133
- return config.mode !== "off" && toolMouseFixedFeaturesEnabled;
1089
+ if (config.mode === "off") return false;
1090
+ if (isLazyProxyTui(toolMouseTui)) return true;
1091
+ return true;
1134
1092
  }
1135
1093
 
1136
1094
  function formatShortcut(shortcut: string): string {
@@ -1157,24 +1115,23 @@ function wheelDirection(data: string): "up" | "down" | null {
1157
1115
  return null;
1158
1116
  }
1159
1117
 
1160
- /** Return how often Zentui's 3-row wheel handler should receive this event. */
1161
- export function fixedEditorWheelDispatchCount(direction: "up" | "down"): number {
1162
- if (lastWheelDirection !== direction) {
1163
- lastWheelDirection = direction;
1164
- wheelExtraRowRemainder = 0;
1165
- }
1166
- wheelExtraRowRemainder += FIXED_EDITOR_WHEEL_ROWS - ZENTUI_WHEEL_ROWS;
1167
- if (wheelExtraRowRemainder < ZENTUI_WHEEL_ROWS) return 1;
1168
- wheelExtraRowRemainder -= ZENTUI_WHEEL_ROWS;
1169
- return 2;
1170
- }
1171
-
1172
1118
  function isScrollNavigationInput(data: string): boolean {
1173
1119
  if (
1174
1120
  matchesKey(data, "pageUp") ||
1175
1121
  matchesKey(data, "pageDown") ||
1176
1122
  ZENTUI_PAGE_UP_INPUT.test(data) ||
1177
- ZENTUI_PAGE_DOWN_INPUT.test(data)
1123
+ ZENTUI_PAGE_DOWN_INPUT.test(data) ||
1124
+ // 官方 fullscreen viewport 的可滚动键(half-page/prompt/top/bottom)。
1125
+ [
1126
+ "tui.altScreen.pageUp",
1127
+ "tui.altScreen.pageDown",
1128
+ "tui.altScreen.halfPageUp",
1129
+ "tui.altScreen.halfPageDown",
1130
+ "tui.altScreen.previousPrompt",
1131
+ "tui.altScreen.nextPrompt",
1132
+ "tui.altScreen.top",
1133
+ "tui.altScreen.bottom",
1134
+ ].some((key) => getKeybindings().matches(data, key))
1178
1135
  ) {
1179
1136
  return true;
1180
1137
  }
@@ -1187,77 +1144,22 @@ function isScrollNavigationInput(data: string): boolean {
1187
1144
  );
1188
1145
  }
1189
1146
 
1190
- function directRenderLines(component: any, width: number): string[] {
1191
- try {
1192
- const lines = component?.render?.(width);
1193
- return Array.isArray(lines) ? lines : [];
1194
- } catch {
1195
- return [];
1196
- }
1197
- }
1198
-
1199
- /** Index just before the fixed editor cluster in the TUI child list. */
1200
- function fixedScrollableRootEnd(tui: any): number {
1201
- const children = Array.isArray(tui?.children) ? tui.children : [];
1202
- const editorIndex = children.findIndex((child: any) =>
1203
- containsEditorLike(child, tui.focusedComponent),
1204
- );
1205
- return editorIndex >= 2 ? editorIndex - 2 : children.length;
1206
- }
1207
-
1208
- /**
1209
- * Last N stripped lines of the scrollable root (after trimming trailing blanks).
1210
- * Walks children backwards and stops once the tail is fully determined, so long
1211
- * transcripts with many sibling nodes do not re-render the whole tree on scroll.
1212
- */
1213
- function renderFixedScrollableRootTail(tui: any, width: number, matchLength: number): string[] {
1214
- const children = Array.isArray(tui?.children) ? tui.children : [];
1215
- const end = fixedScrollableRootEnd(tui);
1216
- const collected: string[] = [];
1217
- for (let index = end - 1; index >= 0; index--) {
1218
- const lines = directRenderLines(children[index], width).map((line) =>
1219
- stripTerminalSequences(String(line)),
1220
- );
1221
- collected.unshift(...lines);
1222
- let meaningful = collected.length;
1223
- while (meaningful > 0 && collected[meaningful - 1] === "") meaningful--;
1224
- if (meaningful >= matchLength) break;
1225
- }
1226
- while (collected.length > 0 && collected[collected.length - 1] === "") collected.pop();
1227
- if (collected.length === 0) return [];
1228
- return collected.slice(-Math.min(matchLength, collected.length));
1229
- }
1230
-
1231
- function isFixedEditorAtBottom(tui: any): boolean {
1232
- if (!useFixedEditorFeatures(tui)) return true;
1233
- const visibleLines = Array.isArray(tui?.previousLines) ? tui.previousLines : [];
1234
- if (visibleLines.length === 0) return true;
1235
- const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
1236
- const expected = renderFixedScrollableRootTail(tui, width, 3);
1237
- if (expected.length === 0) return true;
1238
-
1239
- // previousLines contains both the scrollable root and Zentui's fixed cluster.
1240
- // Locate the root tail within that full frame instead of requiring it to be
1241
- // the frame suffix; otherwise status/editor/footer rows keep the button alive.
1242
- const visible = visibleLines.map((line: unknown) => stripTerminalSequences(String(line)));
1243
- const matchLength = expected.length;
1244
- for (let end = matchLength; end <= visible.length; end++) {
1245
- if (expected.every((line, index) => line === visible[end - matchLength + index])) return true;
1246
- }
1247
- return false;
1147
+ function isAtTranscriptBottom(tui: any): boolean {
1148
+ // 惰性 Proxy fullscreen:官方 viewport 以 isFollowingOutput 判定是否在底部。
1149
+ if (fullscreenLazyTui(tui)) return isFullscreenAtBottom(tui);
1150
+ return true;
1248
1151
  }
1249
1152
 
1250
1153
  function hideScrollButton(tui: any): void {
1251
- const changed = scrollButtonVisible || scrollButtonHovered || pendingScrollMessages > 0;
1154
+ const changed = scrollButtonVisible || scrollButtonHovered;
1252
1155
  scrollButtonVisible = false;
1253
1156
  scrollButtonHovered = false;
1254
- pendingScrollMessages = 0;
1255
1157
  if (changed) tui.requestRender?.();
1256
1158
  }
1257
1159
 
1258
1160
  function scheduleScrollButtonSync(tui: any, data: string): void {
1259
1161
  if (
1260
- !useFixedEditorFeatures(tui) ||
1162
+ !fullscreenLazyTui(tui) ||
1261
1163
  !toolMouseInteractionActive() ||
1262
1164
  !isScrollNavigationInput(data) ||
1263
1165
  scrollButtonSyncScheduled
@@ -1271,7 +1173,8 @@ function scheduleScrollButtonSync(tui: any, data: string): void {
1271
1173
  // Pi renders on its own frame timer. Inspect the resulting viewport before
1272
1174
  // showing the button so empty or non-scrollable transcripts never flash it.
1273
1175
  const rendered = tui.previousLines !== previousLines;
1274
- if (!rendered && attempt < 4) {
1176
+ // fullscreen 下 isFollowingOutput 是即时状态,无需等待官方帧渲染。
1177
+ if (!rendered && attempt < 4 && !fullscreenLazyTui(tui)) {
1275
1178
  scrollButtonSyncScheduled = true;
1276
1179
  const timer = setTimeout(() => check(attempt + 1), 16);
1277
1180
  if (typeof timer === "object" && timer !== null && "unref" in timer) {
@@ -1279,8 +1182,7 @@ function scheduleScrollButtonSync(tui: any, data: string): void {
1279
1182
  }
1280
1183
  return;
1281
1184
  }
1282
- const nextVisible = !isFixedEditorAtBottom(tui);
1283
- if (!nextVisible) pendingScrollMessages = 0;
1185
+ const nextVisible = !isAtTranscriptBottom(tui);
1284
1186
  if (nextVisible !== scrollButtonVisible) {
1285
1187
  scrollButtonVisible = nextVisible;
1286
1188
  tui.requestRender?.();
@@ -1290,7 +1192,7 @@ function scheduleScrollButtonSync(tui: any, data: string): void {
1290
1192
  }
1291
1193
 
1292
1194
  function updateScrollButtonFromInput(tui: any, data: string): void {
1293
- if (!useFixedEditorFeatures(tui) || !toolMouseInteractionActive()) return;
1195
+ if (!fullscreenLazyTui(tui) || !toolMouseInteractionActive()) return;
1294
1196
  if (matchesKey(data, "enter") || matchesKey(data, "return")) hideScrollButton(tui);
1295
1197
  }
1296
1198
 
@@ -1306,70 +1208,6 @@ function renderComponentTree(component: any, width: number): string[] {
1306
1208
  return component.children.flatMap((child: any) => renderComponentTree(child, width));
1307
1209
  }
1308
1210
 
1309
- function containsEditorLike(component: any, focused: any, seen = new Set<any>()): boolean {
1310
- if (!component || typeof component !== "object" || seen.has(component)) return false;
1311
- seen.add(component);
1312
- if (component === focused) return true;
1313
- if (
1314
- typeof component.getText === "function" &&
1315
- typeof component.setText === "function" &&
1316
- typeof component.handleInput === "function"
1317
- )
1318
- return true;
1319
- return (
1320
- Array.isArray(component.children) &&
1321
- component.children.some((child: any) => containsEditorLike(child, focused, seen))
1322
- );
1323
- }
1324
-
1325
- function isScrollButtonAtScreenRow(_tui: any, packet: SgrMousePacket): boolean {
1326
- return interactionRegionAt(packet)?.kind === "scroll-bottom";
1327
- }
1328
-
1329
- function jumpToBottomWithoutSubmit(tui: any): boolean {
1330
- const originalHandle = toolMouseInputPatchTui === tui ? toolMouseInputPatchOriginalHandle : null;
1331
- if (!originalHandle) return false;
1332
-
1333
- // Route Enter through Pi's normal listener chain so pi-zentui can update its
1334
- // private scroll offset, but suppress the focused editor for this synthetic
1335
- // dispatch so clicking the button never submits the current input.
1336
- const focused = tui.focusedComponent;
1337
- try {
1338
- tui.focusedComponent = null;
1339
- Reflect.apply(originalHandle, tui, ["\r"]);
1340
- } finally {
1341
- tui.focusedComponent = focused;
1342
- }
1343
- hideScrollButton(tui);
1344
- return true;
1345
- }
1346
-
1347
- function handleScrollButtonClick(tui: any, packet: SgrMousePacket): boolean {
1348
- if (!isScrollButtonAtScreenRow(tui, packet)) return false;
1349
- return jumpToBottomWithoutSubmit(tui);
1350
- }
1351
-
1352
- function scheduleCollapseViewportCompensation(
1353
- tui: any,
1354
- removedRows: number,
1355
- packet: SgrMousePacket,
1356
- ): void {
1357
- if (removedRows <= 0 || !useFixedEditorFeatures(tui)) return;
1358
- const originalHandle = toolMouseInputPatchTui === tui ? toolMouseInputPatchOriginalHandle : null;
1359
- if (!originalHandle) return;
1360
-
1361
- process.nextTick(() => {
1362
- if (toolMouseTui !== tui || toolMouseInputPatchOriginalHandle !== originalHandle) return;
1363
- const targetRows = removedRows + collapseCompensationRemainder;
1364
- const dispatches = Math.max(0, Math.round(targetRows / ZENTUI_WHEEL_ROWS));
1365
- collapseCompensationRemainder = targetRows - dispatches * ZENTUI_WHEEL_ROWS;
1366
- const wheelDown = `\x1b[<65;${packet.col};${packet.row}M`;
1367
- for (let index = 0; index < dispatches; index++) {
1368
- Reflect.apply(originalHandle, tui, [wheelDown]);
1369
- }
1370
- });
1371
- }
1372
-
1373
1211
  const ioViewInvalidators = new WeakMap<ExpandedToolIoView, () => void>();
1374
1212
  let hoveredToolIoView: ExpandedToolIoView | null = null;
1375
1213
  let hoveredToolIoSection: ToolIoSection | null = null;
@@ -1419,16 +1257,19 @@ function tryOpenToolIoShowMore(region: InteractionRegion): boolean {
1419
1257
  }
1420
1258
 
1421
1259
  function setHoveredToolIo(view: ExpandedToolIoView | null, section: ToolIoSection | null): boolean {
1422
- if (view === hoveredToolIoView && section === hoveredToolIoSection) return false;
1423
- if (hoveredToolIoView) {
1260
+ // resultRendererComponent 可能是 Text/第三方 renderer;reload 后也可能残留旧实例。
1261
+ const nextView = isExpandedToolIoView(view) ? view : null;
1262
+ const nextSection = nextView ? section : null;
1263
+ if (nextView === hoveredToolIoView && nextSection === hoveredToolIoSection) return false;
1264
+ if (isExpandedToolIoView(hoveredToolIoView)) {
1424
1265
  hoveredToolIoView.setHoveredSection(null);
1425
1266
  ioViewInvalidators.get(hoveredToolIoView)?.();
1426
1267
  }
1427
- hoveredToolIoView = view;
1428
- hoveredToolIoSection = section;
1429
- if (view) {
1430
- view.setHoveredSection(section);
1431
- ioViewInvalidators.get(view)?.();
1268
+ hoveredToolIoView = nextView;
1269
+ hoveredToolIoSection = nextSection;
1270
+ if (nextView) {
1271
+ nextView.setHoveredSection(nextSection);
1272
+ ioViewInvalidators.get(nextView)?.();
1432
1273
  }
1433
1274
  return true;
1434
1275
  }
@@ -1466,21 +1307,18 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
1466
1307
  function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
1467
1308
  const region = interactionRegionAt(packet);
1468
1309
  if (!region) return false;
1469
- if (region.kind === "scroll-bottom") return jumpToBottomWithoutSubmit(tui);
1310
+ if (region.kind === "scroll-bottom") return false;
1470
1311
  if (region.kind === "show-more") return tryOpenToolIoShowMore(region);
1471
1312
  const component = region.component;
1472
1313
  if (!component) return false;
1473
1314
  const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
1474
1315
  if (region.kind === "expanded-card") {
1475
- const previousHeight = renderComponentTree(component, width).length;
1476
1316
  component.setExpanded(false);
1477
1317
  hoveredToolCallId = null;
1478
1318
  setHoveredToolGroup(null);
1479
1319
  setHoveredToolIo(null, null);
1480
1320
  component.invalidate?.();
1481
- const nextHeight = renderComponentTree(component, width).length;
1482
1321
  tui.requestRender?.();
1483
- scheduleCollapseViewportCompensation(tui, previousHeight - nextHeight, packet);
1484
1322
  return true;
1485
1323
  }
1486
1324
  component.setExpanded(true);
@@ -1493,90 +1331,392 @@ function toggleToolAtMouseClick(tui: any, packet: SgrMousePacket): boolean {
1493
1331
  }
1494
1332
 
1495
1333
  function renderScrollButton(width: number, theme: any): string[] {
1496
- if (!scrollButtonVisible || !useFixedEditorFeatures(toolMouseTui)) return [];
1334
+ if (!scrollButtonVisible || !fullscreenLazyTui(toolMouseTui)) return [];
1497
1335
  const shortcut = formatShortcut(SCROLL_BOTTOM_SHORTCUT);
1498
- const messageText =
1499
- pendingScrollMessages > 0
1500
- ? `${pendingScrollMessages} new message${pendingScrollMessages === 1 ? "" : "s"}`
1501
- : "Back to bottom";
1502
1336
  const label = theme.fg(
1503
1337
  scrollButtonHovered ? "text" : "accent",
1504
- `[ ↓ ${messageText} · ${shortcut} ]`,
1338
+ `[ ↓ Back to bottom · ${shortcut} ]`,
1505
1339
  );
1506
1340
  const leftPad = Math.max(0, Math.floor((width - visibleWidth(label)) / 2));
1507
1341
  return [`${" ".repeat(leftPad)}${truncateToWidth(label, width, "…")}`];
1508
1342
  }
1509
1343
 
1344
+ /** 惰性 Proxy 官方 fullscreen(TuiAltScreen)判定。 */
1345
+ function fullscreenLazyTui(tui: any): boolean {
1346
+ return isLazyProxyTui(tui) && tui.mode === "fullscreen";
1347
+ }
1348
+
1349
+ /** 官方 fullscreen:是否已跟随 transcript 底部(按钮隐藏条件)。 */
1350
+ function isFullscreenAtBottom(tui: any): boolean {
1351
+ const following = tui.isFollowingOutput ?? tui.getPrimaryScrollView?.()?.isFollowingEnd ?? true;
1352
+ return Boolean(following);
1353
+ }
1354
+
1355
+ const FULLSCREEN_VIEWPORT_PATCH = Symbol("ccstyle.fullscreen-viewport-patch");
1356
+ const FULLSCREEN_WHEEL_SCROLL_ORIGINAL = Symbol("ccstyle.fullscreen-wheel-scroll-original");
1357
+ const FULLSCREEN_WHEEL_SCROLL_LINES = 3;
1358
+
1359
+ /** 布局树点查询:返回 (x,y) 处最深含行 leaf box(屏幕行 → 组件局部行)。 */
1360
+ function fullscreenLeafAt(
1361
+ layout: any,
1362
+ x: number,
1363
+ y: number,
1364
+ ): { box: any; localRow: number } | null {
1365
+ const root = layout?.root;
1366
+ if (!root) return null;
1367
+ let best: { box: any; localRow: number } | null = null;
1368
+ let bestDepth = -1;
1369
+ const visit = (box: any, depth: number) => {
1370
+ if (!box) return;
1371
+ const clip = box.clip;
1372
+ if (!clip || x < clip.x || x >= clip.x + clip.width || y < clip.y || y >= clip.y + clip.height)
1373
+ return;
1374
+ const isLeaf = !Array.isArray(box.children) || box.children.length === 0;
1375
+ if (
1376
+ isLeaf &&
1377
+ y >= box.rect.y &&
1378
+ y < box.rect.y + Math.max(1, box.rect.height) &&
1379
+ depth > bestDepth
1380
+ ) {
1381
+ best = { box, localRow: Math.max(0, y - box.rect.y) };
1382
+ bestDepth = depth;
1383
+ }
1384
+ for (const child of box.children ?? []) visit(child, depth + 1);
1385
+ };
1386
+ visit(root, 0);
1387
+ return best;
1388
+ }
1389
+
1390
+ /** 点击列是否为官方滚动条列(放行官方拖动)。 */
1391
+ function isScrollbarColumnAt(layout: any, x: number): boolean {
1392
+ let hit = false;
1393
+ const visit = (box: any) => {
1394
+ if (hit || !box) return;
1395
+ if (box.scrollView?.isScrollbarVisible && x === box.rect.x + box.rect.width - 1) {
1396
+ hit = true;
1397
+ return;
1398
+ }
1399
+ for (const child of box.children ?? []) visit(child);
1400
+ };
1401
+ visit(layout?.root);
1402
+ return hit;
1403
+ }
1404
+
1510
1405
  /**
1511
- * pi-zentui consumes left-button presses for text selection. Intercept only a
1512
- * tool-row click at the TUI input boundary, before extension listeners run.
1513
- * Keyboard, wheel, drag, release, and non-tool clicks continue through Pi's
1514
- * original dispatcher, preserving pi-zentui's scroll-to-bottom behavior.
1406
+ * 布局 leaf box 的组件通常是容器(documentContainer/dock 容器等),工具卡与
1407
+ * widget 在其 children 内。按局部行遍历组件树,定位实际命中的子组件。
1515
1408
  */
1516
- function patchToolMouseInputCapture(tui: any): void {
1517
- if (toolMouseInputPatchTui === tui) return;
1518
- // 0.84+ 惰性 Proxy:捕获 handleInput 会解析到 wrapper 自身(无限递归),跳过。
1519
- if (isLazyProxyTui(tui)) return;
1409
+ function componentAtLocalRow(
1410
+ component: any,
1411
+ localRow: number,
1412
+ width: number,
1413
+ ): { component: any; row: number } | null {
1414
+ if (
1415
+ isToolExecutionComponent(component) ||
1416
+ component instanceof ToolGroupComponent ||
1417
+ component === scrollButtonWidget
1418
+ ) {
1419
+ return { component, row: localRow };
1420
+ }
1421
+ if (!Array.isArray(component.children)) return null;
1422
+ let offset = 0;
1423
+ for (const child of component.children) {
1424
+ let lines: string[] = [];
1425
+ try {
1426
+ const rendered = child.render?.(width);
1427
+ if (Array.isArray(rendered)) lines = rendered.map((line) => String(line));
1428
+ } catch {
1429
+ lines = [];
1430
+ }
1431
+ if (localRow < offset + lines.length) {
1432
+ return (
1433
+ componentAtLocalRow(child, localRow - offset, width) ?? {
1434
+ component: child,
1435
+ row: localRow - offset,
1436
+ }
1437
+ );
1438
+ }
1439
+ offset += lines.length;
1440
+ }
1441
+ return null;
1442
+ }
1520
1443
 
1521
- restoreToolMouseInputCapture();
1522
- const originalHandle = tui?.handleInput;
1523
- if (typeof originalHandle !== "function") return;
1524
-
1525
- const wrapper = function (this: any, ...args: any[]): any {
1526
- const data = args[0];
1527
- if (typeof data === "string") {
1528
- updateScrollButtonFromInput(this, data);
1529
- // Capture the current viewport before Pi/Zentui applies the scroll input.
1530
- scheduleScrollButtonSync(this, data);
1531
- if (
1532
- useFixedEditorFeatures(this) &&
1533
- isScrollBottomInput(data) &&
1534
- jumpToBottomWithoutSubmit(this)
1535
- )
1536
- return;
1444
+ /** fullscreen single-expand:group 作为整体,不继续递归其内部工具。 */
1445
+ function collectFullscreenToolCards(component: any, out: any[], seen = new Set<any>()): void {
1446
+ if (!component || typeof component !== "object" || seen.has(component)) return;
1447
+ seen.add(component);
1448
+ if (isToolExecutionComponent(component) || component instanceof ToolGroupComponent) {
1449
+ out.push(component);
1450
+ return;
1451
+ }
1452
+ if (!Array.isArray(component.children)) return;
1453
+ for (const child of component.children) collectFullscreenToolCards(child, out, seen);
1454
+ }
1455
+
1456
+ /**
1457
+ * 官方 fullscreen 工具卡点击:collapsed 整卡点击展开
1458
+ * (有且仅保持一个展开:展开前收起其他工具卡),expanded 整卡二次点击收起,
1459
+ * 截断头 [show more] 打开全量预览;回到底部按钮 scrollToBottom。
1460
+ * 滚动条列、含 OSC8 链接行、非工具区域放行官方。
1461
+ */
1462
+ function handleFullscreenToolClick(tui: any, packet: SgrMousePacket): boolean {
1463
+ const layout = tui.currentLayout;
1464
+ if (!layout?.root) return false;
1465
+ // 官方事件坐标 0-based;SGR packet 1-based。
1466
+ const x = packet.col - 1;
1467
+ const y = packet.row - 1;
1468
+ if (isScrollbarColumnAt(layout, x)) return false;
1469
+ const hit = fullscreenLeafAt(layout, x, y);
1470
+ if (!hit) return false;
1471
+ const width = Math.max(1, Number(tui.terminal?.columns) || 80);
1472
+ // 布局树用 scroll 的 contentWidth 渲染内容(滚动条占用时 = width-1);
1473
+ // 行号定位必须用同一宽度,否则换行差异导致组件行错位。
1474
+ const contentWidth =
1475
+ typeof hit.box.scrollView?.getContentWidth === "function"
1476
+ ? Math.max(1, hit.box.scrollView.getContentWidth(width))
1477
+ : width;
1478
+ const target = componentAtLocalRow(hit.box.component, hit.localRow, contentWidth);
1479
+ if (!target) return false;
1480
+ const component = target.component;
1481
+ // 回到底部按钮:按组件引用命中,不依赖渲染行缓存。
1482
+ if (scrollButtonVisible && component === scrollButtonWidget) {
1483
+ tui.scrollToBottom?.();
1484
+ hideScrollButton(tui);
1485
+ return true;
1486
+ }
1487
+ const line = hit.box.lines?.[hit.localRow];
1488
+ if (typeof line !== "string" || /\x1b]8;[^;]*;/.test(line)) return false;
1489
+ const isTool = isToolExecutionComponent(component);
1490
+ const isGroup = component instanceof ToolGroupComponent;
1491
+ if (!isTool && !isGroup) return false;
1492
+ if (!component.expanded) {
1493
+ // single-expand:展开前收起其他已展开工具卡/group。
1494
+ const others: any[] = [];
1495
+ collectFullscreenToolCards(hit.box.component, others);
1496
+ for (const other of others) {
1497
+ if (other !== component && other.expanded) {
1498
+ other.setExpanded(false);
1499
+ other.invalidate?.();
1500
+ }
1501
+ }
1502
+ component.setExpanded(true);
1503
+ } else {
1504
+ // 普通工具截断头 [show more]:打开全量预览(不收起)。
1505
+ const view = isTool ? component.resultRendererComponent : null;
1506
+ if (isExpandedToolIoView(view)) {
1507
+ const plain = stripTerminalSequencesPreservingLayout(line);
1508
+ const section = view.matchShowMoreLine(plain);
1509
+ if (section) {
1510
+ const box = view.showMoreHitbox(plain);
1511
+ if (box && x + 1 >= box.startCol && x + 1 <= box.endCol) {
1512
+ return tryOpenToolIoShowMore({
1513
+ kind: "show-more",
1514
+ row: 0,
1515
+ startCol: box.startCol,
1516
+ endCol: box.endCol,
1517
+ component,
1518
+ view,
1519
+ section,
1520
+ });
1521
+ }
1522
+ }
1523
+ }
1524
+ // 整卡二次点击:收起(与展开语义对称)。
1525
+ component.setExpanded(false);
1526
+ }
1527
+ // 点击后清 hover 高亮。
1528
+ hoveredToolCallId = null;
1529
+ setHoveredToolGroup(null);
1530
+ setHoveredToolIo(null, null);
1531
+ component.invalidate?.();
1532
+ tui.requestRender?.();
1533
+ return true;
1534
+ }
1535
+
1536
+ type ComponentRowHit = { component: any; row: number };
1537
+
1538
+ /** hover 与点击共用组件定位;同一布局下按容器/宽度/行缓存,避免 motion 重复渲染。 */
1539
+ let fullscreenHoverCacheLayout: unknown = null;
1540
+ let fullscreenHoverComponentCache = new WeakMap<object, Map<string, ComponentRowHit | null>>();
1541
+
1542
+ function cachedFullscreenComponentAtRow(
1543
+ layout: any,
1544
+ container: any,
1545
+ row: number,
1546
+ width: number,
1547
+ ): ComponentRowHit | null {
1548
+ if (!container || typeof container !== "object") return null;
1549
+ if (fullscreenHoverCacheLayout !== layout) {
1550
+ fullscreenHoverCacheLayout = layout;
1551
+ fullscreenHoverComponentCache = new WeakMap();
1552
+ }
1553
+ let rows = fullscreenHoverComponentCache.get(container);
1554
+ if (!rows) {
1555
+ rows = new Map();
1556
+ fullscreenHoverComponentCache.set(container, rows);
1557
+ }
1558
+ const key = `${width}:${row}`;
1559
+ if (rows.has(key)) return rows.get(key) ?? null;
1560
+ const hit = componentAtLocalRow(container, row, width);
1561
+ rows.set(key, hit);
1562
+ return hit;
1563
+ }
1564
+
1565
+ /** fullscreen 鼠标悬停目标。 */
1566
+ type FullscreenHoverTarget =
1567
+ | { kind: "button" }
1568
+ | { kind: "group"; component: ToolGroupComponent }
1569
+ | {
1570
+ kind: "tool";
1571
+ component: any;
1572
+ view: ExpandedToolIoView | null;
1573
+ section: ToolIoSection | null;
1574
+ };
1575
+
1576
+ /**
1577
+ * fullscreen 悬停高亮:collapsed 卡 [click to show more] hint、
1578
+ * expanded 卡截断头 [show more]、回到底部按钮。motion 不 consume,官方链照常。
1579
+ */
1580
+ function handleFullscreenToolHover(tui: any, packet: SgrMousePacket): void {
1581
+ if (packet.final !== "M") return;
1582
+ const layout = tui.currentLayout;
1583
+ if (!layout?.root) return;
1584
+ const x = packet.col - 1;
1585
+ const y = packet.row - 1;
1586
+ let target: FullscreenHoverTarget | null = null;
1587
+ const hit = fullscreenLeafAt(layout, x, y);
1588
+ if (hit) {
1589
+ const line = hit.box.lines?.[hit.localRow];
1590
+ // 回到底部按钮:渲染行文本 + 列区间识别(零组件树开销)。
1591
+ if (typeof line === "string" && scrollButtonVisible && line.includes("[ ↓")) {
1592
+ const plain = stripTerminalSequencesPreservingLayout(line);
1593
+ const idx = plain.indexOf("[ ↓");
1594
+ if (idx >= 0 && x >= idx && x <= idx + plain.length - 1) {
1595
+ target = { kind: "button" };
1596
+ }
1597
+ } else if (typeof line === "string" && !/\x1b]8;/.test(line)) {
1598
+ const width = Math.max(1, Number(tui.terminal?.columns) || 80);
1599
+ const contentWidth =
1600
+ typeof hit.box.scrollView?.getContentWidth === "function"
1601
+ ? Math.max(1, hit.box.scrollView.getContentWidth(width))
1602
+ : width;
1603
+ // 与点击共用同一定位算法,避免 hover 自建行段与真实组件树错位。
1604
+ const componentHit = cachedFullscreenComponentAtRow(
1605
+ layout,
1606
+ hit.box.component,
1607
+ hit.localRow,
1608
+ contentWidth,
1609
+ );
1610
+ const component = componentHit?.component;
1611
+ const hintBox = collapsedHintHitbox(line);
1612
+ const overHint = Boolean(
1613
+ hintBox && packet.col >= hintBox.startCol && packet.col <= hintBox.endCol,
1614
+ );
1615
+ if (component instanceof ToolGroupComponent) {
1616
+ if (overHint) target = { kind: "group", component };
1617
+ } else if (isToolExecutionComponent(component)) {
1618
+ let view: ExpandedToolIoView | null = null;
1619
+ let section: ToolIoSection | null = null;
1620
+ if (component.expanded) {
1621
+ const resultView = component.resultRendererComponent;
1622
+ if (isExpandedToolIoView(resultView)) {
1623
+ view = resultView;
1624
+ const plain = stripTerminalSequencesPreservingLayout(line);
1625
+ const candidate = view.matchShowMoreLine(plain);
1626
+ if (candidate) {
1627
+ const box = view.showMoreHitbox(plain);
1628
+ if (box && x + 1 >= box.startCol && x + 1 <= box.endCol) {
1629
+ section = candidate;
1630
+ }
1631
+ }
1632
+ }
1633
+ target = { kind: "tool", component, view, section };
1634
+ } else if (overHint) {
1635
+ target = { kind: "tool", component, view, section };
1636
+ }
1637
+ }
1638
+ }
1639
+ }
1640
+ applyFullscreenHover(tui, target);
1641
+ }
1642
+
1643
+ /** 悬停状态变化才触发渲染(motion 事件密集,状态不变跳过)。 */
1644
+ function applyFullscreenHover(tui: any, target: FullscreenHoverTarget | null): void {
1645
+ let changed = false;
1646
+ const nextCallId =
1647
+ target?.kind === "tool" && !target.component.expanded
1648
+ ? (target.component.toolCallId ?? null)
1649
+ : null;
1650
+ if (nextCallId !== hoveredToolCallId) {
1651
+ hoveredToolCallId = nextCallId;
1652
+ changed = true;
1653
+ }
1654
+ const nextGroup = target?.kind === "group" ? target.component : null;
1655
+ if (setHoveredToolGroup(nextGroup)) changed = true;
1656
+ const nextView = target?.kind === "tool" ? target.view : null;
1657
+ const nextSection = target?.kind === "tool" ? target.section : null;
1658
+ if (setHoveredToolIo(nextView, nextSection)) changed = true;
1659
+ const nextButton = target?.kind === "button";
1660
+ if (nextButton !== scrollButtonHovered) {
1661
+ scrollButtonHovered = nextButton;
1662
+ changed = true;
1663
+ }
1664
+ if (changed) tui.requestRender?.();
1665
+ }
1666
+
1667
+ /**
1668
+ * 实例级包装 TuiAltScreen.handleViewportInput(惰性 Proxy 安全):
1669
+ * 原型方法取 original(绕开 proxy 函数包装),实例 own property 装 wrapper
1670
+ * (constructor arrow 动态查找命中)。仅在 fullscreen 且无 overlay 时先消费
1671
+ * 工具卡左键点击,其余全部放行官方 selection/scrollbar/URL/键盘链。
1672
+ */
1673
+ function patchFullscreenViewportInput(tui: any): void {
1674
+ if (tui[FULLSCREEN_VIEWPORT_PATCH] || !isLazyProxyTui(tui)) return;
1675
+ const proto = Object.getPrototypeOf(tui);
1676
+ const original = proto?.handleViewportInput;
1677
+ if (typeof original !== "function") return;
1678
+ // 官方原生 routeWheel 已完整处理嵌套 ScrollView;只调整默认步进 1 → 3。
1679
+ if (typeof tui.wheelScrollLines === "number") {
1680
+ tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL] = tui.wheelScrollLines;
1681
+ tui.wheelScrollLines = FULLSCREEN_WHEEL_SCROLL_LINES;
1682
+ }
1683
+ tui[FULLSCREEN_VIEWPORT_PATCH] = true;
1684
+ tui.handleViewportInput = function (this: any, data: string) {
1685
+ if (toolMouseInteractionActive() && tui.mode === "fullscreen") {
1686
+ // 滚动输入(wheel/pageUp/end 等)后同步回到底部按钮显隐;
1687
+ // 官方 viewport 会消费键盘,扩展监听器无法补偿,必须在这里调度。
1688
+ scheduleScrollButtonSync(tui, data);
1537
1689
  const packets = parseSgrMousePackets(data);
1538
- if (packets && toolMouseInteractionActive()) {
1690
+ // 官方 fullscreen 会消费全部鼠标;文本预览 overlay 活动时放行给 focused
1691
+ // custom component,使 [esc] 点击和滚轮可用。
1692
+ if (packets && tui.hasOverlay?.() && hasActiveTextPreview()) return undefined;
1693
+ if (packets && !tui.hasOverlay?.()) {
1539
1694
  for (const packet of packets) {
1540
- // Fixed-editor owners may consume motion before extension listeners run,
1541
- // so hover must be handled at the root input boundary too.
1542
- updateToolSummaryHover(this, packet);
1543
- if (!isSgrLeftPress(packet)) continue;
1544
- if (handleScrollButtonClick(this, packet) || toggleToolAtMouseClick(this, packet)) return;
1695
+ if (isSgrLeftPress(packet) && handleFullscreenToolClick(tui, packet)) {
1696
+ return { consume: true };
1697
+ }
1698
+ if ((packet.code & 32) !== 0 && packet.final === "M") {
1699
+ handleFullscreenToolHover(tui, packet);
1700
+ }
1545
1701
  }
1546
1702
  }
1547
1703
  }
1548
- const direction =
1549
- typeof data === "string" && useFixedEditorFeatures(this) ? wheelDirection(data) : null;
1550
- const dispatchCount = direction ? fixedEditorWheelDispatchCount(direction) : 1;
1551
- let result = Reflect.apply(originalHandle, this, args);
1552
- for (let index = 1; index < dispatchCount; index++) {
1553
- result = Reflect.apply(originalHandle, this, args);
1554
- }
1555
- if (typeof data === "string") scheduleScrollButtonSync(this, data);
1556
- return result;
1704
+ return Reflect.apply(original, this, [data]);
1557
1705
  };
1558
-
1559
- try {
1560
- tui.handleInput = wrapper;
1561
- } catch {
1562
- return;
1563
- }
1564
- toolMouseInputPatchTui = tui;
1565
- toolMouseInputPatchOriginalHandle = originalHandle;
1566
- toolMouseInputPatchWrapper = wrapper;
1567
1706
  }
1568
1707
 
1569
- function restoreToolMouseInputCapture(): void {
1570
- if (
1571
- toolMouseInputPatchTui &&
1572
- toolMouseInputPatchOriginalHandle &&
1573
- toolMouseInputPatchTui.handleInput === toolMouseInputPatchWrapper
1574
- ) {
1575
- toolMouseInputPatchTui.handleInput = toolMouseInputPatchOriginalHandle;
1708
+ function restoreFullscreenViewportInput(tui: any): void {
1709
+ if (!tui || !tui[FULLSCREEN_VIEWPORT_PATCH]) return;
1710
+ const proto = Object.getPrototypeOf(tui);
1711
+ if (typeof proto?.handleViewportInput === "function") {
1712
+ tui.handleViewportInput = proto.handleViewportInput;
1576
1713
  }
1577
- toolMouseInputPatchTui = null;
1578
- toolMouseInputPatchOriginalHandle = null;
1579
- toolMouseInputPatchWrapper = null;
1714
+ const originalWheelScrollLines = tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL];
1715
+ if (typeof originalWheelScrollLines === "number") {
1716
+ tui.wheelScrollLines = originalWheelScrollLines;
1717
+ tui[FULLSCREEN_WHEEL_SCROLL_ORIGINAL] = undefined;
1718
+ }
1719
+ tui[FULLSCREEN_VIEWPORT_PATCH] = false;
1580
1720
  }
1581
1721
 
1582
1722
  function restoreToolMouseRenderPatch(): void {
@@ -1602,16 +1742,10 @@ function buildInteractionFrame(
1602
1742
  placements: FrameToolPlacement[],
1603
1743
  ): InteractionFrame {
1604
1744
  const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
1605
- const fixed = useFixedEditorFeatures(tui);
1606
- const viewport = fixed ? getFixedEditorViewport(tui) : null;
1607
- // fixed-editor: tui.render already returned the visible root slice (screen rows).
1608
1745
  // native: full buffer; map with the post-doRender previousViewportTop.
1609
1746
  const lineIndexToScreenRow = (lineIndex: number) =>
1610
- fixed ? lineIndex + 1 : lineIndex - (Number(tui?.previousViewportTop) || 0) + 1;
1611
- const visibleRows = fixed
1612
- ? (viewport?.visibleLines.length ??
1613
- (Array.isArray(tui?.previousLines) ? tui.previousLines.length : Number.POSITIVE_INFINITY))
1614
- : Math.max(1, Number(tui?.terminal?.rows) || Number.POSITIVE_INFINITY);
1747
+ lineIndex - (Number(tui?.previousViewportTop) || 0) + 1;
1748
+ const visibleRows = Math.max(1, Number(tui?.terminal?.rows) || Number.POSITIVE_INFINITY);
1615
1749
  const regions: InteractionRegion[] = [];
1616
1750
  const renderedByComponent = new Map<any, FrameToolRender>();
1617
1751
  for (const rendered of renderedTools) renderedByComponent.set(rendered.component, rendered);
@@ -1675,13 +1809,74 @@ function buildInteractionFrame(
1675
1809
  }
1676
1810
  }
1677
1811
  }
1678
- const scrollHitbox = getFixedEditorScrollButtonHitbox();
1679
- if (scrollButtonVisible && useFixedEditorFeatures(tui) && scrollHitbox) {
1680
- regions.push({ kind: "scroll-bottom", ...scrollHitbox });
1681
- }
1682
1812
  return { regions };
1683
1813
  }
1684
1814
 
1815
+ /**
1816
+ * 临时包装 outermost 工具/组件的 render 注入零宽 marker,返回待 restore 列表。
1817
+ * 调用方必须用 restoreRenderOverride 立即还原(同一次渲染内有效)。
1818
+ */
1819
+ function wrapToolRendersForFrame(
1820
+ outermost: any[],
1821
+ renderedTools: FrameToolRender[],
1822
+ idToComponent: Map<number, any>,
1823
+ ): Array<{ target: any; descriptor?: PropertyDescriptor }> {
1824
+ const restores: Array<{ target: any; descriptor?: PropertyDescriptor }> = [];
1825
+ let nextId = 0;
1826
+ try {
1827
+ for (const component of outermost) {
1828
+ const originalRender = component.render;
1829
+ if (typeof originalRender !== "function") continue;
1830
+ const id = nextId++;
1831
+ idToComponent.set(id, component);
1832
+ const wrappedRender = function (this: any, ...renderArgs: any[]) {
1833
+ let contentBoxLines = 0;
1834
+ const box = component.contentBox;
1835
+ let boxRestore: { target: any; descriptor?: PropertyDescriptor } | undefined;
1836
+ if (
1837
+ box &&
1838
+ Array.isArray(component.children) &&
1839
+ component.children.includes(box) &&
1840
+ typeof box.render === "function"
1841
+ ) {
1842
+ const boxOriginal = box.render;
1843
+ const boxWrapped = function (this: any, ...boxArgs: any[]) {
1844
+ const boxLines = Reflect.apply(boxOriginal, this, boxArgs);
1845
+ if (Array.isArray(boxLines)) contentBoxLines = boxLines.length;
1846
+ return boxLines;
1847
+ };
1848
+ const boxDescriptor = defineRenderOverride(box, boxWrapped);
1849
+ if (boxDescriptor !== undefined || box.render === boxWrapped) {
1850
+ boxRestore = { target: box, descriptor: boxDescriptor };
1851
+ }
1852
+ }
1853
+ try {
1854
+ const lines = Reflect.apply(originalRender, this, renderArgs);
1855
+ if (!Array.isArray(lines)) return lines;
1856
+ renderedTools.push({
1857
+ component,
1858
+ lines: lines.map((line) => String(line)),
1859
+ contentBoxLines,
1860
+ });
1861
+ return lines.map((line, row) => `${line}${toolFrameMarker(id, row)}`);
1862
+ } finally {
1863
+ if (boxRestore) restoreRenderOverride(boxRestore.target, boxRestore.descriptor);
1864
+ }
1865
+ };
1866
+ const descriptor = defineRenderOverride(component, wrappedRender);
1867
+ if (descriptor !== undefined || component.render === wrappedRender) {
1868
+ restores.push({ target: component, descriptor });
1869
+ }
1870
+ }
1871
+ return restores;
1872
+ } catch (error) {
1873
+ for (const { target, descriptor } of restores.reverse()) {
1874
+ restoreRenderOverride(target, descriptor);
1875
+ }
1876
+ throw error;
1877
+ }
1878
+ }
1879
+
1685
1880
  function defineRenderOverride(
1686
1881
  target: any,
1687
1882
  wrapped: (...args: any[]) => any,
@@ -1743,54 +1938,9 @@ function patchToolMouseMotionAfterRender(tui: any): void {
1743
1938
  idToView: new Map(),
1744
1939
  nextId: 0,
1745
1940
  };
1746
- const restores: Array<{ target: any; descriptor?: PropertyDescriptor }> = [];
1747
1941
  const outermost: any[] = [];
1748
1942
  collectToolComponents(this, outermost);
1749
- let nextId = 0;
1750
- for (const component of outermost) {
1751
- const originalRender = component.render;
1752
- if (typeof originalRender !== "function") continue;
1753
- const id = nextId++;
1754
- idToComponent.set(id, component);
1755
- const wrappedRender = function (this: any, ...renderArgs: any[]) {
1756
- let contentBoxLines = 0;
1757
- const box = component.contentBox;
1758
- let boxRestore: { target: any; descriptor?: PropertyDescriptor } | undefined;
1759
- if (
1760
- box &&
1761
- Array.isArray(component.children) &&
1762
- component.children.includes(box) &&
1763
- typeof box.render === "function"
1764
- ) {
1765
- const boxOriginal = box.render;
1766
- const boxWrapped = function (this: any, ...boxArgs: any[]) {
1767
- const boxLines = Reflect.apply(boxOriginal, this, boxArgs);
1768
- if (Array.isArray(boxLines)) contentBoxLines = boxLines.length;
1769
- return boxLines;
1770
- };
1771
- const boxDescriptor = defineRenderOverride(box, boxWrapped);
1772
- if (boxDescriptor !== undefined || box.render === boxWrapped) {
1773
- boxRestore = { target: box, descriptor: boxDescriptor };
1774
- }
1775
- }
1776
- try {
1777
- const lines = Reflect.apply(originalRender, this, renderArgs);
1778
- if (!Array.isArray(lines)) return lines;
1779
- renderedTools.push({
1780
- component,
1781
- lines: lines.map((line) => String(line)),
1782
- contentBoxLines,
1783
- });
1784
- return lines.map((line, row) => `${line}${toolFrameMarker(id, row)}`);
1785
- } finally {
1786
- if (boxRestore) restoreRenderOverride(boxRestore.target, boxRestore.descriptor);
1787
- }
1788
- };
1789
- const descriptor = defineRenderOverride(component, wrappedRender);
1790
- if (descriptor !== undefined || component.render === wrappedRender) {
1791
- restores.push({ target: component, descriptor });
1792
- }
1793
- }
1943
+ const restores = wrapToolRendersForFrame(outermost, renderedTools, idToComponent);
1794
1944
  let placements: FrameToolPlacement[] = [];
1795
1945
  const originalTuiRender = typeof this.render === "function" ? this.render : null;
1796
1946
  let tuiRenderDescriptor: PropertyDescriptor | undefined;
@@ -1826,8 +1976,7 @@ function patchToolMouseMotionAfterRender(tui: any): void {
1826
1976
  this.previousLines = extracted.lines;
1827
1977
  placements = extracted.placements;
1828
1978
  }
1829
- if (!useFixedEditorFeatures(this) && toolMouseInteractionActive())
1830
- toolMouseRawWrite?.(TOOL_MOUSE_MOTION_ENABLE);
1979
+ if (toolMouseInteractionActive()) toolMouseRawWrite?.(TOOL_MOUSE_MOTION_ENABLE);
1831
1980
  return result;
1832
1981
  } finally {
1833
1982
  activeIoViewFrame = previousFrame;
@@ -1850,31 +1999,23 @@ function patchToolMouseMotionAfterRender(tui: any): void {
1850
1999
  toolMouseRenderPatchOriginal = original;
1851
2000
  toolMouseRenderPatchWrapper = wrapper;
1852
2001
  toolMouseRenderPatchState = patchState;
1853
- if (!useFixedEditorFeatures(tui) && toolMouseInteractionActive())
1854
- toolMouseRawWrite?.(TOOL_MOUSE_MOTION_ENABLE);
2002
+ if (toolMouseInteractionActive()) toolMouseRawWrite?.(TOOL_MOUSE_MOTION_ENABLE);
1855
2003
  }
1856
2004
 
1857
2005
  function handleToolMouseInput(data: string): { consume: true } | undefined {
1858
2006
  if (!toolMouseTui) return undefined;
1859
- if (
1860
- toolMouseInputPatchTui === toolMouseTui &&
1861
- toolMouseTui.handleInput === toolMouseInputPatchWrapper
1862
- )
1863
- return undefined;
1864
- updateScrollButtonFromInput(toolMouseTui, data);
1865
- if (isScrollBottomInput(data)) {
1866
- if (useFixedEditorFeatures(toolMouseTui) && jumpToBottomWithoutSubmit(toolMouseTui)) {
1867
- return { consume: true };
1868
- }
1869
- if (!toolMouseFixedFeaturesEnabled) {
1870
- // Native Pi scrolls through terminal history rather than an internal
1871
- // viewport. A harmless terminal write makes Ctrl+End snap that history
1872
- // to the active cursor without enabling mouse reporting.
1873
- toolMouseTui.terminal?.write?.("\x1b[0m");
1874
- toolMouseTui.requestRender?.();
2007
+ // 惰性 Proxy fullscreen:鼠标由 handleViewportInput 包装消费(官方链之前),
2008
+ // 此处只处理键盘(鼠标事件在官方 listener 已被 consume,到不了这里)。
2009
+ if (fullscreenLazyTui(toolMouseTui)) {
2010
+ scheduleScrollButtonSync(toolMouseTui, data);
2011
+ if (isScrollBottomInput(data)) {
2012
+ toolMouseTui.scrollToBottom?.();
2013
+ hideScrollButton(toolMouseTui);
1875
2014
  return { consume: true };
1876
2015
  }
2016
+ return undefined;
1877
2017
  }
2018
+ updateScrollButtonFromInput(toolMouseTui, data);
1878
2019
  // Off mode restores native input: wheel keeps scrolling through Pi's normal
1879
2020
  // dispatcher, while hover/click affordances are entirely inactive.
1880
2021
  if (!toolMouseInteractionActive()) return undefined;
@@ -1888,10 +2029,7 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
1888
2029
  for (const packet of packets) {
1889
2030
  updateToolSummaryHover(toolMouseTui, packet);
1890
2031
  if (!isSgrLeftPress(packet)) continue;
1891
- if (
1892
- handleScrollButtonClick(toolMouseTui, packet) ||
1893
- toggleToolAtMouseClick(toolMouseTui, packet)
1894
- ) {
2032
+ if (toggleToolAtMouseClick(toolMouseTui, packet)) {
1895
2033
  consumed = true;
1896
2034
  }
1897
2035
  }
@@ -1902,7 +2040,7 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
1902
2040
  return consumed ? { consume: true } : undefined;
1903
2041
  }
1904
2042
 
1905
- function teardownToolMouseInteraction(nextFixedEditorFeatures = false): void {
2043
+ function teardownToolMouseInteraction(): void {
1906
2044
  if (sessionRenderTimer) {
1907
2045
  clearTimeout(sessionRenderTimer);
1908
2046
  sessionRenderTimer = null;
@@ -1913,7 +2051,8 @@ function teardownToolMouseInteraction(nextFixedEditorFeatures = false): void {
1913
2051
  setHoveredToolGroup(null);
1914
2052
  setHoveredToolIo(null, null);
1915
2053
  try {
1916
- if (!nextFixedEditorFeatures) {
2054
+ // 惰性 Proxy 不启用任何 reporting(regular 会破坏终端回滚,fullscreen 归官方)。
2055
+ if (!isLazyProxyTui(toolMouseTui)) {
1917
2056
  toolMouseTui?.terminal?.write?.(TOOL_MOUSE_DISABLE);
1918
2057
  }
1919
2058
  } catch {
@@ -1924,54 +2063,45 @@ function teardownToolMouseInteraction(nextFixedEditorFeatures = false): void {
1924
2063
  } catch {
1925
2064
  // The UI context may already have been reset during /reload.
1926
2065
  }
1927
- restoreToolMouseInputCapture();
1928
2066
  restoreToolMouseRenderPatch();
2067
+ restoreFullscreenViewportInput(toolMouseTui);
1929
2068
  scrollButtonVisible = false;
1930
2069
  scrollButtonHovered = false;
1931
2070
  scrollButtonWidget = null;
1932
- pendingScrollMessages = 0;
1933
- assistantMessageActive = false;
1934
2071
  scrollButtonSyncScheduled = false;
1935
2072
  toolMouseTui = null;
1936
2073
  toolMouseUi = null;
1937
- toolMouseFixedFeaturesEnabled = false;
1938
- wheelExtraRowRemainder = 0;
1939
- lastWheelDirection = null;
1940
- collapseCompensationRemainder = 0;
1941
2074
  }
1942
2075
 
1943
- export function installToolMouseInteraction(
1944
- ctx: any,
1945
- fixedEditorFeatures = config.fixedEditorFeatures,
1946
- ): void {
1947
- teardownToolMouseInteraction(fixedEditorFeatures);
2076
+ export function installToolMouseInteraction(ctx: any): void {
2077
+ teardownToolMouseInteraction();
1948
2078
  if (ctx?.mode !== "tui" || !ctx?.hasUI) return;
1949
2079
  if (typeof ctx.ui?.onTerminalInput !== "function" || typeof ctx.ui?.setWidget !== "function")
1950
2080
  return;
1951
2081
 
1952
2082
  toolMouseUi = ctx.ui;
1953
- toolMouseFixedFeaturesEnabled = fixedEditorFeatures;
1954
- // 0.84+ 的 tui 是惰性 Proxy:行级 patch(doRender/handleInput)无法安全捕获,
1955
- // fixed-editor compositor 亦停用 —— 整个工具鼠标交互体系(hover/点击/滚动按钮)
1956
- // 让位给官方,连 onTerminalInput 监听都不注册。
1957
- let proxyTui = false;
2083
+ // 0.84+ 的 tui 是惰性 Proxy:行级 patch(doRender/handleInput)无法安全捕获。
2084
+ // regular 模式不启用鼠标上报(保终端回滚);fullscreen 由官方 viewport 接管。
1958
2085
  ctx.ui.setWidget(TOOL_MOUSE_WIDGET_KEY, (tui: any, theme: any) => {
1959
2086
  toolMouseTui = tui;
1960
2087
  if (isLazyProxyTui(tui)) {
1961
- proxyTui = true;
1962
- return { render: () => 0, invalidate() {} };
2088
+ // 惰性 Proxy:不启用任何 mouse reporting(regular 会破坏终端回滚,
2089
+ // fullscreen 官方已启用)。fullscreen 工具卡点击/回到底部按钮由
2090
+ // handleViewportInput 实例包装提供;widget render 时重检查 renderer
2091
+ // 替换(switchTuiMode 新建实例后重新安装包装)。
2092
+ patchFullscreenViewportInput(tui);
2093
+ scrollButtonWidget = {
2094
+ render: (width: number) => {
2095
+ patchFullscreenViewportInput(tui);
2096
+ return renderScrollButton(width, theme);
2097
+ },
2098
+ invalidate() {},
2099
+ };
2100
+ return scrollButtonWidget;
1963
2101
  }
1964
- // Root input capture follows the live compositor; onRebuild re-applies it
1965
- // once the fixed-editor compositor actually owns the terminal.
1966
- if (useFixedEditorFeatures(tui)) patchToolMouseInputCapture(tui);
1967
- // Wrap doRender unconditionally: the fixed-editor rebuild replaces the
1968
- // chain (setBeforeFixedEditorStart restores it first) and onRebuild
1969
- // re-applies this wrapper once the compositor owns the terminal.
2102
+ // Wrap doRender to capture the live frame for tool click/hover mapping.
1970
2103
  patchToolMouseMotionAfterRender(tui);
1971
- // The fixed-editor compositor owns its mouse mode; native Pi gets motion only
1972
- // while mouse interaction is active (rendering mode on + fixed editor).
1973
- if (!fixedEditorFeatures && toolMouseInteractionActive())
1974
- tui?.terminal?.write?.(TOOL_MOUSE_MOTION_ENABLE);
2104
+ if (toolMouseInteractionActive()) tui?.terminal?.write?.(TOOL_MOUSE_MOTION_ENABLE);
1975
2105
  const widget = {
1976
2106
  render: (width: number) => renderScrollButton(width, theme),
1977
2107
  invalidate() {},
@@ -1979,10 +2109,6 @@ export function installToolMouseInteraction(
1979
2109
  scrollButtonWidget = widget;
1980
2110
  return widget;
1981
2111
  });
1982
- if (proxyTui) {
1983
- teardownToolMouseInteraction(fixedEditorFeatures);
1984
- return;
1985
- }
1986
2112
  toolMouseInputUnsubscribe = ctx.ui.onTerminalInput(handleToolMouseInput);
1987
2113
  }
1988
2114
 
@@ -2002,10 +2128,7 @@ function scheduleSessionRender(refresh?: () => void): void {
2002
2128
  sessionRenderTimer = setTimeout(() => {
2003
2129
  sessionRenderTimer = null;
2004
2130
  if (toolMouseTui !== tui) return;
2005
- // Do not pre-wrap while fixed-editor compositor install is still pending.
2006
- if (!toolMouseFixedFeaturesEnabled || useFixedEditorFeatures(tui)) {
2007
- patchToolMouseMotionAfterRender(tui);
2008
- }
2131
+ patchToolMouseMotionAfterRender(tui);
2009
2132
  refreshToolRendererComponents(tui);
2010
2133
  refresh?.();
2011
2134
  tui.requestRender(true);
@@ -2016,48 +2139,34 @@ function scheduleSessionRender(refresh?: () => void): void {
2016
2139
  const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
2017
2140
  const ANSI_FG_RESET = "\x1b[39m";
2018
2141
 
2019
- function refreshCurrentTranscript(
2020
- compactStyle: CompactStyleHooks,
2021
- ctx?: any,
2022
- toolGrouping?: ToolGroupingHooks,
2023
- ): void {
2142
+ function refreshCurrentTranscript(ctx?: any, toolGrouping?: ToolGroupingHooks): void {
2024
2143
  toolGrouping?.refresh();
2025
- compactStyle.refresh();
2026
2144
  toolMouseTui?.requestRender?.(true);
2027
2145
  ctx?.ui?.requestRender?.(true);
2028
2146
  }
2029
2147
 
2030
- function applyStyleMode(
2031
- mode: CompactStyleMode,
2032
- ctx: any,
2033
- compactStyle: CompactStyleHooks,
2034
- toolGrouping?: ToolGroupingHooks,
2035
- ): void {
2148
+ function applyStyleMode(mode: CompactStyleMode, ctx: any, toolGrouping?: ToolGroupingHooks): void {
2036
2149
  config.mode = mode;
2037
2150
  saveConfig();
2038
2151
  if (mode === "off") {
2039
2152
  // Native rendering mode: drop hover/click state and fully disable mouse
2040
2153
  // reporting so the terminal restores its default scrollback wheel scrolling.
2041
- // (The fixed-editor compositor owns its own reporting when enabled.)
2042
2154
  hoveredToolCallId = null;
2043
2155
  setHoveredToolGroup(null);
2044
2156
  setHoveredToolIo(null, null);
2045
2157
  scrollButtonVisible = false;
2046
2158
  scrollButtonHovered = false;
2047
- pendingScrollMessages = 0;
2048
- assistantMessageActive = false;
2049
- if (toolMouseTui && !useFixedEditorFeatures(toolMouseTui)) {
2159
+ // 惰性 Proxy(regular/fullscreen)不持有 reporting:regular 保终端回滚,
2160
+ // fullscreen 归官方所有,均不能在此关闭。
2161
+ if (toolMouseTui && !isLazyProxyTui(toolMouseTui)) {
2050
2162
  toolMouseTui.terminal?.write?.(TOOL_MOUSE_DISABLE);
2051
2163
  }
2052
2164
  }
2053
- refreshCurrentTranscript(compactStyle, ctx, toolGrouping);
2165
+ refreshCurrentTranscript(ctx, toolGrouping);
2054
2166
  ctx.ui.notify(`Claude Code style: ${mode}`, "info");
2055
2167
  }
2056
2168
 
2057
2169
  function modeSettingDescription(mode: CompactStyleMode): string {
2058
- if (mode === "compact") {
2059
- return "Compact transcript summaries. Diff options below still apply independently.";
2060
- }
2061
2170
  if (mode === "off") {
2062
2171
  return "Pi native tool rendering. Diff options below still apply independently.";
2063
2172
  }
@@ -2099,7 +2208,7 @@ function buildExcludeRenderersSubmenu(
2099
2208
  description:
2100
2209
  name === "Agent"
2101
2210
  ? "Agent always uses its dedicated renderer and cannot be forced through ccstyle."
2102
- : `Use Pi native renderer for ${name} instead of Claude Code / compact styling.`,
2211
+ : `Use Pi native renderer for ${name} instead of Claude Code styling.`,
2103
2212
  currentValue: config.excludeRenderers.includes(name) ? "exclude" : "style",
2104
2213
  values: ["style", "exclude"],
2105
2214
  }));
@@ -2171,7 +2280,6 @@ function renderSectionTabBar(
2171
2280
 
2172
2281
  async function showCcstylePanel(
2173
2282
  ctx: any,
2174
- compactStyle: CompactStyleHooks,
2175
2283
  toolGrouping?: ToolGroupingHooks,
2176
2284
  compactThinking?: CompactThinkingController,
2177
2285
  ): Promise<void> {
@@ -2186,7 +2294,7 @@ async function showCcstylePanel(
2186
2294
  label: "Mode",
2187
2295
  description: modeSettingDescription(config.mode),
2188
2296
  currentValue: config.mode,
2189
- values: ["on", "off", "compact"],
2297
+ values: ["on", "off"],
2190
2298
  };
2191
2299
  // Tracks whether the Exclude-tools submenu is open so Tab switches sections
2192
2300
  // only at the top level (mirrors Zentui settings: Tab = switch sections).
@@ -2208,7 +2316,7 @@ async function showCcstylePanel(
2208
2316
  () => {
2209
2317
  excludeSetting.currentValue = formatExcludeRenderers(config.excludeRenderers);
2210
2318
  excludeSetting.description = excludeRenderersDescription(config.excludeRenderers);
2211
- refreshCurrentTranscript(compactStyle, ctx);
2319
+ refreshCurrentTranscript(ctx);
2212
2320
  },
2213
2321
  );
2214
2322
  },
@@ -2287,7 +2395,7 @@ async function showCcstylePanel(
2287
2395
  switch (id) {
2288
2396
  case "mode":
2289
2397
  modeSetting.description = modeSettingDescription(value as CompactStyleMode);
2290
- applyStyleMode(value as CompactStyleMode, ctx, compactStyle, toolGrouping);
2398
+ applyStyleMode(value as CompactStyleMode, ctx, toolGrouping);
2291
2399
  return;
2292
2400
  case "excludeRenderers":
2293
2401
  excludeSetting.currentValue = formatExcludeRenderers(config.excludeRenderers);
@@ -2348,7 +2456,7 @@ async function showCcstylePanel(
2348
2456
  }
2349
2457
  saveConfig();
2350
2458
  compactThinking?.updateConfig(getCompactThinkingConfig());
2351
- refreshCurrentTranscript(compactStyle, ctx);
2459
+ refreshCurrentTranscript(ctx);
2352
2460
  ctx.ui.notify(`Updated ${id}: ${value}`, "info");
2353
2461
  };
2354
2462
 
@@ -2761,8 +2869,9 @@ function createCcstyleTool(
2761
2869
  ? `${outputLines} ${lineWord} ${action}`
2762
2870
  : "Done";
2763
2871
  const expandable = !expanded && (tasks.length > 0 || hasExpandableDetail(text, args));
2764
- const hint = expandable ? theme.fg("muted", " • click to show more") : "";
2765
- const hoveredHint = expandable ? theme.fg("text", " • click to show more") : "";
2872
+ const hintPrefix = expandable ? theme.fg("muted", " • ") : "";
2873
+ const hint = expandable ? hintPrefix + theme.fg("muted", "click to show more") : "";
2874
+ const hoveredHint = expandable ? hintPrefix + theme.fg("text", "click to show more") : "";
2766
2875
  if (expanded) {
2767
2876
  return renderExpandedToolResult(
2768
2877
  text || "",
@@ -2801,7 +2910,7 @@ function createCcstyleTool(
2801
2910
  /**
2802
2911
  * Apart from the write override used to capture pre-write content, renderers are
2803
2912
  * applied through ToolExecutionComponent. Patch its lookup once so tools use the
2804
- * same compact fallback shell by default. Tools named in excludeRenderers keep
2913
+ * same ccstyle fallback shell by default. Tools named in excludeRenderers keep
2805
2914
  * their original renderer.
2806
2915
  */
2807
2916
  const GLOBAL_TOOL_RENDER_PATCH = Symbol.for("pi.ccstyle.global-tool-render-patch");
@@ -3128,18 +3237,6 @@ function deactivateLegacyCompactionRendering() {
3128
3237
  if (patch) patch.enabled = () => false;
3129
3238
  }
3130
3239
 
3131
- function notePendingScrollMessage(role: unknown): void {
3132
- if (!toolMouseTui || !useFixedEditorFeatures(toolMouseTui) || !scrollButtonVisible) return;
3133
- if (role === "assistant") {
3134
- if (assistantMessageActive) return;
3135
- assistantMessageActive = true;
3136
- } else if (role !== "toolResult") {
3137
- return;
3138
- }
3139
- pendingScrollMessages += 1;
3140
- toolMouseTui.requestRender?.();
3141
- }
3142
-
3143
3240
  export default function (
3144
3241
  pi: ExtensionAPI,
3145
3242
  configOverride?: Partial<Config>,
@@ -3147,55 +3244,31 @@ export default function (
3147
3244
  ) {
3148
3245
  // The optional override keeps integration tests independent from the user's global config.
3149
3246
  if (configOverride) config = normalizeConfig({ ...config, ...configOverride });
3150
- const fixedEditorController = installFixedEditor(pi, config.fixedEditorFeatures);
3151
- // Unwrap mouse doRender before compositor construction captures the chain.
3152
- setBeforeFixedEditorStart(() => {
3153
- restoreToolMouseRenderPatch();
3154
- });
3155
- // Footer/compositor rebuild disposes the old chain; re-wrap the live doRender and repaint.
3156
- fixedEditorController.onRebuild(() => {
3157
- const tui = toolMouseTui;
3158
- if (!tui) return;
3159
- if (toolMouseFixedFeaturesEnabled) patchToolMouseInputCapture(tui);
3160
- // compositor.install captured the current chain as originalDoRender, so an
3161
- // already-installed wrapper keeps receiving calls through it — re-wrapping
3162
- // only when no wrapper is active avoids stacking one per rebuild/reload.
3163
- if (!(toolMouseRenderPatchTui === tui && toolMouseRenderPatchState?.active)) {
3164
- patchToolMouseMotionAfterRender(tui);
3165
- }
3166
- tui.requestRender?.(true);
3167
- });
3168
- const writeExecutionMetadata = installWriteOverride(pi, new WriteExecutionMetadataStore());
3247
+ const writeExecutionMetadata = new WriteExecutionMetadataStore();
3169
3248
  let installation:
3170
3249
  | {
3171
3250
  globalToolRendering: GlobalToolRenderPatch;
3172
3251
  toolGrouping: ToolGroupingHooks;
3173
- compactStyle: CompactStyleHooks;
3174
3252
  }
3175
3253
  | undefined;
3176
3254
  const ensureTuiInstallation = (ctx: any) => {
3177
3255
  if (ctx?.mode !== "tui" || !ctx?.hasUI) return undefined;
3178
- // fullscreen 复用官方布局,插件渲染层(工具样式/紧凑模式/分组)让位
3179
- if (isFullscreenUi(ctx)) return undefined;
3256
+ // 渲染层(工具样式/分组)是原型与组件级 patch,fullscreen 官方布局
3257
+ // 同样渲染这些组件,因此两种模式都安装。
3180
3258
  if (installation) return installation;
3181
3259
  const globalToolRendering = installGlobalToolRendering(writeExecutionMetadata);
3182
3260
  const toolGrouping = installToolGrouping(() => config.mode === "on");
3183
3261
  deactivateLegacyCompactionRendering();
3184
- const compactStyle = installCompactStyle(pi, {
3185
- getMode: () => config.mode,
3186
- getExcludeRenderers: () => config.excludeRenderers,
3187
- });
3188
- installation = { globalToolRendering, toolGrouping, compactStyle };
3262
+ installation = { globalToolRendering, toolGrouping };
3189
3263
  return installation;
3190
3264
  };
3191
3265
 
3192
3266
  pi.registerCommand("ccstyle", {
3193
- description: "Configure Claude Code style, fixed editor, and rich diff options",
3267
+ description: "Configure Claude Code style and rich diff options",
3194
3268
  getArgumentCompletions: (prefix) => {
3195
3269
  const topLevel = [
3196
3270
  { value: "on", label: "on", description: "Enable Claude Code style" },
3197
3271
  { value: "off", label: "off", description: "Use Pi's native renderer" },
3198
- { value: "compact", label: "compact", description: "Use compact transcript rendering" },
3199
3272
  { value: "status", label: "status", description: "Show full configuration" },
3200
3273
  { value: "panel", label: "panel", description: "Open interactive settings panel" },
3201
3274
  ];
@@ -3203,24 +3276,17 @@ export default function (
3203
3276
  },
3204
3277
  handler: async (args, ctx) => {
3205
3278
  const arg = args.trim().toLowerCase();
3206
- if (!arg || arg === "panel" || arg === "on" || arg === "off" || arg === "compact") {
3279
+ if (!arg || arg === "panel" || arg === "on" || arg === "off") {
3207
3280
  if (ctx?.mode !== "tui" || !ctx?.hasUI) {
3208
3281
  ctx.ui?.notify?.("/ccstyle requires TUI mode", "warning");
3209
3282
  return;
3210
3283
  }
3211
- if (isFullscreenUi(ctx)) {
3212
- ctx.ui.notify(
3213
- "ccstyle 渲染已在 fullscreen 模式下让位给官方 TUI;请切换到 regular 模式后使用 /ccstyle",
3214
- "warning",
3215
- );
3216
- return;
3217
- }
3218
3284
  const hooks = ensureTuiInstallation(ctx);
3219
3285
  if (!hooks) return;
3220
3286
  if (!arg || arg === "panel") {
3221
- await showCcstylePanel(ctx, hooks.compactStyle, hooks.toolGrouping, compactThinking);
3287
+ await showCcstylePanel(ctx, hooks.toolGrouping, compactThinking);
3222
3288
  } else {
3223
- applyStyleMode(arg, ctx, hooks.compactStyle, hooks.toolGrouping);
3289
+ applyStyleMode(arg, ctx, hooks.toolGrouping);
3224
3290
  }
3225
3291
  return;
3226
3292
  }
@@ -3228,73 +3294,44 @@ export default function (
3228
3294
  ctx.ui.notify(`Claude Code style: ${formatConfigStatus(config)}`, "info");
3229
3295
  return;
3230
3296
  }
3231
- ctx.ui.notify("Usage: /ccstyle [on|off|compact|status|panel]", "warning");
3297
+ ctx.ui.notify("Usage: /ccstyle [on|off|status|panel]", "warning");
3232
3298
  },
3233
3299
  });
3234
3300
 
3235
3301
  pi.on("session_start", async (event, ctx) => {
3302
+ // 延迟到 session_start 注册 write override:加载阶段 getAllTools 不可用且其他扩展
3303
+ // 尚未注册工具,无法检测外部 write 所有者(如 pi-spark),直接注册会与对方撞名。
3304
+ // session_start 时所有扩展已加载完毕,installWriteOverride 内部会检测并让位。
3305
+ installWriteOverride(pi, writeExecutionMetadata);
3236
3306
  const hooks = ensureTuiInstallation(ctx);
3307
+ // 鼠标交互独立于渲染层:fullscreen 渲染层让位(hooks undefined)但
3308
+ // 工具点击/回到底部适配仍需安装;保持在渲染层安装之后以维持原顺序。
3309
+ if (ctx?.mode === "tui" && ctx?.hasUI) installToolMouseInteraction(ctx);
3237
3310
  if (!hooks) return;
3238
3311
  hooks.toolGrouping.setTheme(ctx.ui.theme);
3239
- hooks.compactStyle.onSessionStart(event, ctx);
3240
- pendingScrollMessages = 0;
3241
- assistantMessageActive = false;
3242
3312
  ctx.ui.setStatus("ccstyle", undefined);
3243
- installToolMouseInteraction(ctx);
3244
- scheduleSessionRender(hooks.compactStyle.refresh);
3313
+ scheduleSessionRender();
3245
3314
  });
3246
3315
 
3247
3316
  pi.on("session_compact", async (event, ctx) => {
3248
3317
  const hooks = ensureTuiInstallation(ctx);
3249
- if (!hooks) return;
3250
- hooks.toolGrouping.setTheme(ctx.ui.theme);
3251
- hooks.compactStyle.onSessionCompact(event, ctx);
3252
3318
  // Compaction rebuilds the transcript without session_start. Rebind after
3253
3319
  // other TUI extensions may have replaced the root input dispatcher.
3254
- installToolMouseInteraction(ctx);
3255
- scheduleSessionRender(hooks.compactStyle.refresh);
3256
- });
3257
-
3258
- pi.on("message_start", async (event) => {
3259
- if (installation) notePendingScrollMessage(event?.message?.role);
3260
- });
3261
-
3262
- pi.on("message_update", async (event, ctx) => {
3263
- installation?.compactStyle.onMessageUpdate(event, ctx);
3264
- if (installation && event?.message?.role === "assistant") notePendingScrollMessage("assistant");
3265
- });
3266
-
3267
- pi.on("message_end", async (event) => {
3268
- if (installation && event?.message?.role === "assistant") assistantMessageActive = false;
3269
- });
3270
-
3271
- pi.on("agent_start", async (event, ctx) => {
3272
- installation?.compactStyle.onAgentStart(event, ctx);
3273
- });
3274
-
3275
- pi.on("agent_end", async (event, ctx) => {
3276
- installation?.compactStyle.onAgentEnd(event, ctx);
3277
- });
3278
-
3279
- pi.on("turn_start", async (event, ctx) => {
3280
- installation?.compactStyle.onTurnStart(event, ctx);
3320
+ if (ctx?.mode === "tui" && ctx?.hasUI) installToolMouseInteraction(ctx);
3321
+ if (!hooks) return;
3322
+ hooks.toolGrouping.setTheme(ctx.ui.theme);
3323
+ scheduleSessionRender();
3281
3324
  });
3282
3325
 
3283
3326
  pi.on("tool_execution_start", async (event, ctx) => {
3284
3327
  installation?.toolGrouping.setTheme(ctx.ui.theme);
3285
- installation?.compactStyle.onToolExecutionStart(event, ctx);
3286
- });
3287
-
3288
- pi.on("tool_execution_update", async (event, ctx) => {
3289
- installation?.compactStyle.onToolExecutionUpdate(event, ctx);
3290
- });
3291
-
3292
- pi.on("tool_execution_end", async (event, ctx) => {
3293
- installation?.compactStyle.onToolExecutionEnd(event, ctx);
3294
3328
  });
3295
3329
 
3296
3330
  pi.on("session_shutdown", async (event, ctx) => {
3297
3331
  writeExecutionMetadata.clear();
3332
+ // 鼠标交互独立于渲染层:fullscreen 下 installation 为 undefined,
3333
+ // 但 onTerminalInput 监听与 handleViewportInput 包装仍需释放。
3334
+ teardownToolMouseInteraction();
3298
3335
  const current = installation;
3299
3336
  if (
3300
3337
  !current ||
@@ -3302,8 +3339,6 @@ export default function (
3302
3339
  !current.globalToolRendering.active
3303
3340
  )
3304
3341
  return;
3305
- current.compactStyle.onSessionShutdown(event, ctx);
3306
- teardownToolMouseInteraction();
3307
3342
  deactivateGlobalToolRendering(current.globalToolRendering);
3308
3343
  current.toolGrouping.shutdown();
3309
3344
  deactivateLegacyCompactionRendering();