pi-cc-extensions 0.8.30 → 0.8.32

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.
Files changed (53) hide show
  1. package/README.en.md +164 -174
  2. package/README.md +113 -115
  3. package/extensions/claude-code-style.ts +371 -208
  4. package/extensions/compact-thinking.ts +58 -0
  5. package/extensions/fixed-editor.ts +185 -0
  6. package/extensions/index.ts +10 -4
  7. package/extensions/subagent-notification.ts +30 -23
  8. package/node_modules/@tifan/pi-fixed-editor/README.md +36 -0
  9. package/node_modules/@tifan/pi-fixed-editor/package.json +46 -0
  10. package/node_modules/@tifan/pi-fixed-editor/src/cluster.ts +113 -0
  11. package/node_modules/@tifan/pi-fixed-editor/src/index.ts +259 -0
  12. package/node_modules/@tifan/pi-fixed-editor/src/terminal-split.ts +1337 -0
  13. package/package.json +9 -5
  14. package/extensions/ask-user-question/LICENSE +0 -21
  15. package/extensions/ask-user-question/README.md +0 -63
  16. package/extensions/ask-user-question/ask-user-question.ts +0 -266
  17. package/extensions/ask-user-question/config.ts +0 -79
  18. package/extensions/ask-user-question/events.ts +0 -55
  19. package/extensions/ask-user-question/index.ts +0 -17
  20. package/extensions/ask-user-question/reconcile.ts +0 -47
  21. package/extensions/ask-user-question/rpc-fallback.ts +0 -172
  22. package/extensions/ask-user-question/state/build-questionnaire.ts +0 -314
  23. package/extensions/ask-user-question/state/dialog-height.ts +0 -16
  24. package/extensions/ask-user-question/state/key-router.ts +0 -291
  25. package/extensions/ask-user-question/state/questionnaire-session.ts +0 -193
  26. package/extensions/ask-user-question/state/row-intent.ts +0 -143
  27. package/extensions/ask-user-question/state/selectors/contract.ts +0 -24
  28. package/extensions/ask-user-question/state/selectors/derivations.ts +0 -40
  29. package/extensions/ask-user-question/state/selectors/focus.ts +0 -17
  30. package/extensions/ask-user-question/state/selectors/projections.ts +0 -102
  31. package/extensions/ask-user-question/state/state-reducer.ts +0 -299
  32. package/extensions/ask-user-question/state/state.ts +0 -49
  33. package/extensions/ask-user-question/tool/format-answer.ts +0 -29
  34. package/extensions/ask-user-question/tool/response-envelope.ts +0 -50
  35. package/extensions/ask-user-question/tool/types.ts +0 -149
  36. package/extensions/ask-user-question/tool/validate-questionnaire.ts +0 -64
  37. package/extensions/ask-user-question/view/component-binding.ts +0 -45
  38. package/extensions/ask-user-question/view/components/inline-input.ts +0 -109
  39. package/extensions/ask-user-question/view/components/multi-select-view.ts +0 -198
  40. package/extensions/ask-user-question/view/components/option-list-view.ts +0 -77
  41. package/extensions/ask-user-question/view/components/preview/markdown-content-cache.ts +0 -76
  42. package/extensions/ask-user-question/view/components/preview/preview-block-renderer.ts +0 -114
  43. package/extensions/ask-user-question/view/components/preview/preview-box-renderer.ts +0 -86
  44. package/extensions/ask-user-question/view/components/preview/preview-layout-decider.ts +0 -219
  45. package/extensions/ask-user-question/view/components/preview/preview-pane.ts +0 -230
  46. package/extensions/ask-user-question/view/components/submit-picker.ts +0 -66
  47. package/extensions/ask-user-question/view/components/tab-bar.ts +0 -57
  48. package/extensions/ask-user-question/view/components/wrapping-select.ts +0 -307
  49. package/extensions/ask-user-question/view/dialog-builder.ts +0 -225
  50. package/extensions/ask-user-question/view/props-adapter.ts +0 -123
  51. package/extensions/ask-user-question/view/stateful-view.ts +0 -24
  52. package/extensions/ask-user-question/view/tab-components.ts +0 -16
  53. package/extensions/ask-user-question/view/tab-content-strategy.ts +0 -264
@@ -5,7 +5,13 @@ import {
5
5
  type CompactStyleHooks,
6
6
  type CompactStyleMode,
7
7
  } from "./compact-style.ts";
8
+ import type { CompactThinkingConfig, CompactThinkingController } from "./compact-thinking.ts";
8
9
  import { showTextPreview } from "./context.ts";
10
+ import {
11
+ getFixedEditorScrollButtonHitbox,
12
+ installFixedEditor,
13
+ type FixedEditorController,
14
+ } from "./fixed-editor.ts";
9
15
  import {
10
16
  installToolGrouping,
11
17
  ToolGroupComponent,
@@ -52,9 +58,14 @@ export type Config = {
52
58
  diffCollapsedLines: number;
53
59
  diffWordWrap: boolean;
54
60
  expandedPreviewMaxLines: number;
61
+ useSummaryTitlesAsThinkingTitle: boolean;
62
+ previewLines: number;
63
+ animationIntervalMs: number;
55
64
  };
56
65
 
57
- const CONFIG_PATH = join(homedir(), ".pi", "agent", "claude-code-style.json");
66
+ const AGENT_DIR = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
67
+ const CONFIG_PATH = join(AGENT_DIR, "claude-code-style.json");
68
+ const LEGACY_COMPACT_THINKING_CONFIG_PATH = join(AGENT_DIR, "compact-thinking.json");
58
69
 
59
70
  const DIFF_VIEW_MODES: DiffViewMode[] = ["auto", "split", "unified"];
60
71
  const DIFF_INDICATOR_MODES: DiffIndicatorMode[] = ["bars", "classic", "none"];
@@ -62,6 +73,8 @@ const DIFF_SPLIT_MIN_WIDTH_VALUES = ["80", "100", "120", "140", "160", "180"];
62
73
  const DIFF_COLLAPSED_LINES_VALUES = ["12", "24", "36", "48", "80", "120"];
63
74
  /** Presets for expanded body height — keep low options first so cycling stays TUI-friendly. */
64
75
  const EXPANDED_PREVIEW_MAX_LINES_VALUES = ["40", "60", "80", "120", "200", "500", "2000"];
76
+ const THINKING_PREVIEW_LINES_VALUES = ["0", "1", "3", "5", "10"];
77
+ const THINKING_ANIMATION_INTERVAL_VALUES = ["30", "60", "90", "120", "180"];
65
78
  /** Tools commonly toggled in excludeRenderers via the settings panel. */
66
79
  const EXCLUDE_RENDERER_CANDIDATES = [
67
80
  "bash",
@@ -85,6 +98,9 @@ export const DEFAULT_CONFIG: Config = {
85
98
  diffCollapsedLines: DEFAULT_TOOL_DISPLAY_CONFIG.diffCollapsedLines,
86
99
  diffWordWrap: DEFAULT_TOOL_DISPLAY_CONFIG.diffWordWrap,
87
100
  expandedPreviewMaxLines: DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines,
101
+ useSummaryTitlesAsThinkingTitle: true,
102
+ previewLines: 3,
103
+ animationIntervalMs: 90,
88
104
  };
89
105
 
90
106
  function pickEnum<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {
@@ -99,6 +115,11 @@ function pickPositiveInt(value: unknown, fallback: number, min = 1, max = 100_00
99
115
  return Math.min(max, Math.max(min, Math.floor(n)));
100
116
  }
101
117
 
118
+ function pickPositiveNumber(value: unknown, fallback: number, min = 1): number {
119
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
120
+ return Number.isFinite(n) ? Math.max(min, n) : fallback;
121
+ }
122
+
102
123
  function nearestPreset(value: number, presets: readonly string[]): string {
103
124
  const numeric = presets.map((p) => Number(p));
104
125
  let best = presets[0] ?? String(value);
@@ -165,6 +186,25 @@ export function normalizeConfig(input: unknown): Config {
165
186
  10,
166
187
  50_000,
167
188
  ),
189
+ useSummaryTitlesAsThinkingTitle: source.useSummaryTitlesAsThinkingTitle !== false,
190
+ previewLines: pickPositiveInt(
191
+ source.previewLines,
192
+ DEFAULT_CONFIG.previewLines,
193
+ 0,
194
+ Number.MAX_SAFE_INTEGER,
195
+ ),
196
+ animationIntervalMs: pickPositiveNumber(
197
+ source.animationIntervalMs,
198
+ DEFAULT_CONFIG.animationIntervalMs,
199
+ ),
200
+ };
201
+ }
202
+
203
+ export function getCompactThinkingConfig(source: Config = config): CompactThinkingConfig {
204
+ return {
205
+ useSummaryTitlesAsThinkingTitle: source.useSummaryTitlesAsThinkingTitle,
206
+ previewLines: source.previewLines,
207
+ animationIntervalMs: source.animationIntervalMs,
168
208
  };
169
209
  }
170
210
 
@@ -194,6 +234,9 @@ export function formatConfigStatus(source: Config = config): string {
194
234
  `diffCollapsed=${source.diffCollapsedLines}`,
195
235
  `diffWordWrap=${source.diffWordWrap ? "on" : "off"}`,
196
236
  `expandedMax=${source.expandedPreviewMaxLines}`,
237
+ `thinkingTitle=${source.useSummaryTitlesAsThinkingTitle ? "summary" : "default"}`,
238
+ `thinkingPreview=${source.previewLines}`,
239
+ `thinkingAnimation=${source.animationIntervalMs}ms`,
197
240
  ].join(" · ");
198
241
  }
199
242
 
@@ -201,26 +244,45 @@ let config: Config = loadConfig();
201
244
 
202
245
  function loadConfig(): Config {
203
246
  try {
204
- if (existsSync(CONFIG_PATH)) {
205
- const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as unknown;
206
- const normalized = normalizeConfig(parsed);
207
- const source = parsed as Record<string, unknown>;
208
- // Persist the one-time enabled:boolean -> mode migration while retaining
209
- // the existing exclusion list.
210
- if (
211
- typeof source.enabled === "boolean" &&
247
+ const source = existsSync(CONFIG_PATH)
248
+ ? (JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Record<string, unknown>)
249
+ : {};
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
+ const normalized = normalizeConfig(source);
271
+ // Persist one-time migrations while retaining existing ccstyle values.
272
+ if (
273
+ migrated ||
274
+ (typeof source.enabled === "boolean" &&
212
275
  source.mode !== "on" &&
213
276
  source.mode !== "off" &&
214
- source.mode !== "compact"
215
- ) {
216
- try {
217
- writeFileSync(CONFIG_PATH, JSON.stringify(normalized, null, 2));
218
- } catch {
219
- // A read-only config still uses the migrated in-memory value.
220
- }
277
+ source.mode !== "compact")
278
+ ) {
279
+ try {
280
+ writeFileSync(CONFIG_PATH, JSON.stringify(normalized, null, 2));
281
+ } catch {
282
+ // A read-only config still uses the migrated in-memory value.
221
283
  }
222
- return normalized;
223
284
  }
285
+ return normalized;
224
286
  } catch {
225
287
  // Ignore bad config and fall back to defaults.
226
288
  }
@@ -541,6 +603,7 @@ export class ExpandedToolIoView {
541
603
  }
542
604
 
543
605
  render(width: number): string[] {
606
+ recordRenderedIoView(this);
544
607
  if (this.cachedLines !== undefined && this.cachedWidth === width) return this.cachedLines;
545
608
 
546
609
  const theme = this.theme;
@@ -861,6 +924,8 @@ type ToolRenderHit = {
861
924
  row: number;
862
925
  };
863
926
 
927
+ type FrameToolRender = { component: any; lines: string[] };
928
+
864
929
  const TOOL_MOUSE_WIDGET_KEY = "ccstyle-tool-mouse";
865
930
  const TOOL_MOUSE_ENABLE = "\x1b[?1000h\x1b[?1003h\x1b[?1006h";
866
931
  const TOOL_MOUSE_MOTION_ENABLE = "\x1b[?1003h\x1b[?1006h";
@@ -883,8 +948,10 @@ let toolMouseInputPatchWrapper: ((...args: any[]) => any) | null = null;
883
948
  let toolMouseRenderPatchTui: any = null;
884
949
  let toolMouseRenderPatchOriginal: ((...args: any[]) => any) | null = null;
885
950
  let toolMouseRenderPatchWrapper: ((...args: any[]) => any) | null = null;
951
+ let toolMouseRenderPatchState: { active: boolean } | null = null;
886
952
  let toolMouseRawWrite: ((data: string) => unknown) | null = null;
887
953
  let scrollButtonVisible = false;
954
+ let scrollButtonHovered = false;
888
955
  let scrollButtonWidget: any = null;
889
956
  let pendingScrollMessages = 0;
890
957
  let assistantMessageActive = false;
@@ -892,6 +959,7 @@ let scrollButtonSyncScheduled = false;
892
959
  let sessionRenderTimer: ReturnType<typeof setTimeout> | null = null;
893
960
  let hoveredToolCallId: string | null = null;
894
961
  let hoveredToolGroup: ToolGroupComponent | null = null;
962
+ let frameToolRenders: FrameToolRender[] = [];
895
963
 
896
964
  function parseSgrMousePackets(data: string): SgrMousePacket[] | null {
897
965
  const pattern = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
@@ -1018,8 +1086,6 @@ function fixedEditorContextScore(
1018
1086
 
1019
1087
  /** Summary markers used by Pi and ccstyle; unlike the trailing hint, these survive truncation. */
1020
1088
  const COLLAPSED_TOOL_SUMMARY = /^\s*(?:↳|└|⎿|●|✓|✗|…)/;
1021
- /** fixedEditorContextScore max ≈ 100 + 2*(5+4+3+2); stop once we hit a near-perfect match. */
1022
- const TOOL_CLICK_EARLY_EXIT_SCORE = 120;
1023
1089
 
1024
1090
  function findToolAtFixedEditorRow(
1025
1091
  tui: any,
@@ -1034,20 +1100,25 @@ function findToolAtFixedEditorRow(
1034
1100
  const hasBackground = /\x1b\[(?:4[0-8]|10[0-7])(?:;[0-9;:]*)?m/.test(rawClickedLine);
1035
1101
  if (!clickedLine && !hasBackground) return null;
1036
1102
 
1037
- const tools: any[] = [];
1038
- collectToolComponents(tui, tools);
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
+ }
1039
1112
  const clickedCollapsedSummary = COLLAPSED_TOOL_SUMMARY.test(clickedLine);
1040
1113
  let best: { hit: ToolRenderHit; score: number } | null = null;
1041
- for (const component of tools) {
1114
+ for (const { component, lines } of candidates) {
1042
1115
  // Collapsed tools open only from their summary row; expanded tools collapse from any body row.
1043
1116
  if (!component.expanded && !clickedCollapsedSummary) continue;
1044
- const renderedLines = renderComponentTree(component, width).map((line) =>
1045
- stripTerminalSequences(String(line)),
1046
- );
1117
+ const renderedLines = lines.map((line) => stripTerminalSequences(String(line)));
1047
1118
  for (let renderedRow = 0; renderedRow < renderedLines.length; renderedRow++) {
1048
1119
  if (!fixedEditorLineMatch(renderedLines[renderedRow] ?? "", clickedLine)) continue;
1049
1120
  const score = fixedEditorContextScore(renderedLines, renderedRow, visibleLines, visibleRow);
1050
- if (!best || score > best.score) {
1121
+ if (!best || score >= best.score) {
1051
1122
  best = {
1052
1123
  hit: {
1053
1124
  component,
@@ -1057,7 +1128,6 @@ function findToolAtFixedEditorRow(
1057
1128
  },
1058
1129
  score,
1059
1130
  };
1060
- if (score >= TOOL_CLICK_EARLY_EXIT_SCORE) return best.hit;
1061
1131
  }
1062
1132
  }
1063
1133
  }
@@ -1220,8 +1290,9 @@ function isFixedEditorAtBottom(tui: any): boolean {
1220
1290
  }
1221
1291
 
1222
1292
  function hideScrollButton(tui: any): void {
1223
- const changed = scrollButtonVisible || pendingScrollMessages > 0;
1293
+ const changed = scrollButtonVisible || scrollButtonHovered || pendingScrollMessages > 0;
1224
1294
  scrollButtonVisible = false;
1295
+ scrollButtonHovered = false;
1225
1296
  pendingScrollMessages = 0;
1226
1297
  if (changed) tui.requestRender?.();
1227
1298
  }
@@ -1257,9 +1328,7 @@ function scheduleScrollButtonSync(tui: any, data: string): void {
1257
1328
 
1258
1329
  function updateScrollButtonFromInput(tui: any, data: string): void {
1259
1330
  if (!useFixedEditorFeatures(tui)) return;
1260
- if (matchesKey(data, "enter") || matchesKey(data, "return") || isScrollBottomInput(data)) {
1261
- hideScrollButton(tui);
1262
- }
1331
+ if (matchesKey(data, "enter") || matchesKey(data, "return")) hideScrollButton(tui);
1263
1332
  }
1264
1333
 
1265
1334
  function renderComponentTree(component: any, width: number): string[] {
@@ -1274,58 +1343,6 @@ function renderComponentTree(component: any, width: number): string[] {
1274
1343
  return component.children.flatMap((child: any) => renderComponentTree(child, width));
1275
1344
  }
1276
1345
 
1277
- function renderTreeWithTarget(
1278
- component: any,
1279
- target: any,
1280
- width: number,
1281
- seen = new Set<any>(),
1282
- ): { lines: string[]; targetStart: number | null } {
1283
- if (!component || typeof component !== "object" || seen.has(component)) {
1284
- return { lines: [], targetStart: null };
1285
- }
1286
- seen.add(component);
1287
- if (component === target) {
1288
- return { lines: renderComponentTree(component, width), targetStart: 0 };
1289
- }
1290
-
1291
- if (Array.isArray(component.children)) {
1292
- const lines: string[] = [];
1293
- let targetStart: number | null = null;
1294
- for (const child of component.children) {
1295
- const result = renderTreeWithTarget(child, target, width, seen);
1296
- if (result.targetStart !== null) targetStart = lines.length + result.targetStart;
1297
- lines.push(...result.lines);
1298
- }
1299
- if (targetStart !== null) return { lines, targetStart };
1300
- }
1301
-
1302
- return { lines: renderComponentTree(component, width), targetStart: null };
1303
- }
1304
-
1305
- function normalizedClusterLines(component: any, width: number): string[] {
1306
- if (!component) return [];
1307
- const lines = renderComponentTree(component, width);
1308
- let end = lines.length;
1309
- while (end > 0 && visibleWidth(lines[end - 1] ?? "") === 0) end--;
1310
- return lines.slice(0, Math.max(end, 1));
1311
- }
1312
-
1313
- function rawTerminalRows(tui: any): number {
1314
- const terminal = tui?.terminal;
1315
- if (!terminal) return 0;
1316
- const prototype = Object.getPrototypeOf(terminal);
1317
- const rows = prototype ? Object.getOwnPropertyDescriptor(prototype, "rows") : undefined;
1318
- if (typeof rows?.get === "function") {
1319
- try {
1320
- const value = rows.get.call(terminal);
1321
- if (typeof value === "number" && Number.isFinite(value)) return value;
1322
- } catch {
1323
- // Fall through to the current terminal value.
1324
- }
1325
- }
1326
- return typeof terminal.rows === "number" && Number.isFinite(terminal.rows) ? terminal.rows : 0;
1327
- }
1328
-
1329
1346
  function containsEditorLike(component: any, focused: any, seen = new Set<any>()): boolean {
1330
1347
  if (!component || typeof component !== "object" || seen.has(component)) return false;
1331
1348
  seen.add(component);
@@ -1342,70 +1359,15 @@ function containsEditorLike(component: any, focused: any, seen = new Set<any>())
1342
1359
  );
1343
1360
  }
1344
1361
 
1345
- function scrollButtonScreenRow(tui: any, width: number): number | null {
1346
- if (!scrollButtonVisible || !useFixedEditorFeatures(tui) || !scrollButtonWidget) return null;
1347
- const children = Array.isArray(tui?.children) ? tui.children : [];
1348
- const editorIndex = children.findIndex((child: any) =>
1349
- containsEditorLike(child, tui.focusedComponent),
1350
- );
1351
- if (editorIndex < 2 || editorIndex + 2 >= children.length) return null;
1352
-
1353
- const above = children[editorIndex - 1];
1354
- const widthValue = Math.max(1, width || Number(tui?.terminal?.columns) || 80);
1355
- const target = renderTreeWithTarget(above, scrollButtonWidget, widthValue);
1356
- if (target.targetStart === null) return null;
1357
-
1358
- const rawRows = rawTerminalRows(tui);
1359
- if (rawRows <= 0) return null;
1360
- const maxRows = Math.max(1, rawRows - 1);
1361
- const status = normalizedClusterLines(children[editorIndex - 2], widthValue);
1362
- const editor = normalizedClusterLines(children[editorIndex], widthValue);
1363
- const below = normalizedClusterLines(children[editorIndex + 1], widthValue);
1364
- const footer = normalizedClusterLines(children[editorIndex + 2], widthValue);
1365
- const aboveLines =
1366
- target.lines.length > 0 ? target.lines : normalizedClusterLines(above, widthValue);
1367
-
1368
- const takeLast = (lines: string[], count: number): string[] =>
1369
- count > 0 ? lines.slice(-count) : [];
1370
- const editorVisible = takeLast(editor, Math.min(editor.length, maxRows));
1371
- let remaining = Math.max(0, maxRows - editorVisible.length);
1372
- const footerVisible = takeLast(footer, remaining);
1373
- remaining -= footerVisible.length;
1374
- const belowVisible = takeLast(below, remaining);
1375
- remaining -= belowVisible.length;
1376
- const aboveVisible = takeLast(aboveLines, remaining);
1377
- const statusVisible = takeLast(status, Math.max(0, remaining - aboveVisible.length));
1378
- const aboveStart = aboveLines.length - aboveVisible.length;
1379
- const targetRow = target.targetStart - aboveStart;
1380
- if (targetRow < 0 || targetRow >= aboveVisible.length) return null;
1381
-
1382
- const allLines = [
1383
- ...statusVisible,
1384
- ...aboveVisible,
1385
- ...editorVisible,
1386
- ...belowVisible,
1387
- ...footerVisible,
1388
- ];
1389
- let leadingBlank = 0;
1390
- while (leadingBlank < allLines.length - 1 && visibleWidth(allLines[leadingBlank] ?? "") === 0) {
1391
- leadingBlank++;
1392
- }
1393
- const clusterRow = statusVisible.length + targetRow - leadingBlank;
1394
- if (clusterRow < 0 || clusterRow >= allLines.length - leadingBlank) return null;
1395
- return rawRows - allLines.length + clusterRow + 1;
1396
- }
1397
-
1398
1362
  function isScrollButtonAtScreenRow(tui: any, packet: SgrMousePacket): boolean {
1399
- const width = Math.max(1, Number(tui?.terminal?.columns) || 80);
1400
- if (scrollButtonScreenRow(tui, width) !== packet.row || !scrollButtonWidget) return false;
1401
- const rendered = scrollButtonWidget.render?.(width)?.[0];
1402
- if (typeof rendered !== "string") return false;
1403
- const plain = rendered
1404
- .replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
1405
- .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
1406
- const leading = plain.length - plain.trimStart().length;
1407
- const end = visibleWidth(plain.trimEnd());
1408
- return packet.col >= leading + 1 && packet.col <= end;
1363
+ if (!scrollButtonVisible || !useFixedEditorFeatures(tui) || !scrollButtonWidget) return false;
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
+ );
1409
1371
  }
1410
1372
 
1411
1373
  function jumpToBottomWithoutSubmit(tui: any): boolean {
@@ -1454,11 +1416,19 @@ function scheduleCollapseViewportCompensation(
1454
1416
 
1455
1417
  /** toolCallId → latest expanded IO view (survives context/state identity quirks). */
1456
1418
  const ioViewsByToolCallId = new Map<string, ExpandedToolIoView>();
1419
+ const ioViewInvalidators = new WeakMap<ExpandedToolIoView, () => void>();
1420
+ const ioViewCollectors = new Set<Set<ExpandedToolIoView>>();
1421
+ let frameIoViews = new Set<ExpandedToolIoView>();
1457
1422
  let hoveredToolIoView: ExpandedToolIoView | null = null;
1458
1423
  let hoveredToolIoSection: ToolIoSection | null = null;
1459
1424
 
1425
+ function recordRenderedIoView(view: ExpandedToolIoView): void {
1426
+ for (const collector of ioViewCollectors) collector.add(view);
1427
+ }
1428
+
1460
1429
  function rememberIoView(context: any, view: ExpandedToolIoView): void {
1461
1430
  if (!context || typeof context !== "object") return;
1431
+ if (typeof context.invalidate === "function") ioViewInvalidators.set(view, context.invalidate);
1462
1432
  if (!context.state || typeof context.state !== "object") context.state = {};
1463
1433
  const state = context.state as Record<string, unknown>;
1464
1434
  state.ccstyleIoView = view;
@@ -1494,6 +1464,50 @@ function resolveIoViewFromTool(component: any): ExpandedToolIoView | null {
1494
1464
  return null;
1495
1465
  }
1496
1466
 
1467
+ function resolveShowMoreAt(
1468
+ tui: any,
1469
+ packet: SgrMousePacket,
1470
+ component?: any,
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)),
1487
+ );
1488
+ const visibleRow = useFixedEditorFeatures(tui)
1489
+ ? packet.row - 1
1490
+ : (Number.isFinite(tui?.previousViewportTop) ? tui.previousViewportTop : 0) + packet.row - 1;
1491
+ let best: { view: ExpandedToolIoView; section: ToolIoSection; score: number } | null = null;
1492
+ for (const view of candidates) {
1493
+ const section = view.matchShowMoreLine(plain);
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
+
1497
1511
  /**
1498
1512
  * If the click lands on a truncated section's [show more], open the /context-style
1499
1513
  * full-text preview instead of toggling expand/collapse.
@@ -1520,21 +1534,9 @@ function isCollapsedHintAtColumn(line: string, col: number): boolean {
1520
1534
 
1521
1535
  function tryOpenToolIoShowMore(tui: any, packet: SgrMousePacket, hit: ToolRenderHit): boolean {
1522
1536
  if (!Boolean(hit.component.expanded)) return false;
1523
- const ioView = resolveIoViewFromTool(hit.component);
1524
- if (!ioView) return false;
1525
-
1526
- const previousLines = Array.isArray(tui?.previousLines) ? tui.previousLines : [];
1527
- const visibleRow = useFixedEditorFeatures(tui)
1528
- ? packet.row - 1
1529
- : (Number.isFinite(tui?.previousViewportTop) ? tui.previousViewportTop : 0) + packet.row - 1;
1530
- if (visibleRow < 0 || visibleRow >= previousLines.length) return false;
1531
-
1532
- const plain = stripTerminalSequencesPreservingLayout(String(previousLines[visibleRow] ?? ""));
1533
- const section = ioView.matchShowMoreLine(plain);
1534
- if (!section) return false;
1535
-
1536
- const box = ioView.showMoreHitbox(plain);
1537
- if (!box || packet.col < box.startCol || packet.col > box.endCol) return false;
1537
+ const match = resolveShowMoreAt(tui, packet, hit.component);
1538
+ if (!match) return false;
1539
+ const { view: ioView, section } = match;
1538
1540
 
1539
1541
  const ui = toolMouseUi;
1540
1542
  if (!ui || typeof ui.custom !== "function") {
@@ -1551,10 +1553,16 @@ function tryOpenToolIoShowMore(tui: any, packet: SgrMousePacket, hit: ToolRender
1551
1553
 
1552
1554
  function setHoveredToolIo(view: ExpandedToolIoView | null, section: ToolIoSection | null): boolean {
1553
1555
  if (view === hoveredToolIoView && section === hoveredToolIoSection) return false;
1554
- hoveredToolIoView?.setHoveredSection(null);
1556
+ if (hoveredToolIoView) {
1557
+ hoveredToolIoView.setHoveredSection(null);
1558
+ ioViewInvalidators.get(hoveredToolIoView)?.();
1559
+ }
1555
1560
  hoveredToolIoView = view;
1556
1561
  hoveredToolIoSection = section;
1557
- view?.setHoveredSection(section);
1562
+ if (view) {
1563
+ view.setHoveredSection(section);
1564
+ ioViewInvalidators.get(view)?.();
1565
+ }
1558
1566
  return true;
1559
1567
  }
1560
1568
 
@@ -1568,6 +1576,9 @@ function setHoveredToolGroup(group: ToolGroupComponent | null): boolean {
1568
1576
 
1569
1577
  function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
1570
1578
  if ((packet.code & 32) === 0 || packet.final !== "M") return;
1579
+ const nextScrollButtonHovered = isScrollButtonAtScreenRow(tui, packet);
1580
+ const scrollButtonChanged = nextScrollButtonHovered !== scrollButtonHovered;
1581
+ scrollButtonHovered = nextScrollButtonHovered;
1571
1582
  const visibleLine = visibleMouseLine(tui, packet);
1572
1583
  let nextToolCallId: string | null = null;
1573
1584
  let nextGroup: ToolGroupComponent | null = null;
@@ -1583,18 +1594,21 @@ function updateToolSummaryHover(tui: any, packet: SgrMousePacket): void {
1583
1594
  nextGroup = hit?.component instanceof ToolGroupComponent ? hit.component : null;
1584
1595
  } else if (visibleLine.includes(SHOW_MORE_LABEL)) {
1585
1596
  const hit = findToolAtScreenRow(tui, packet.row);
1586
- const view = hit && hit.component.expanded ? resolveIoViewFromTool(hit.component) : null;
1587
- const section = view?.matchShowMoreLine(visibleLine) ?? null;
1588
- const box = view?.showMoreHitbox(visibleLine);
1589
- if (view && section && box && packet.col >= box.startCol && packet.col <= box.endCol) {
1590
- nextIoView = view;
1591
- nextIoSection = section;
1597
+ const match = resolveShowMoreAt(tui, packet, hit?.component);
1598
+ if (match) {
1599
+ nextIoView = match.view;
1600
+ nextIoSection = match.section;
1592
1601
  }
1593
1602
  }
1594
1603
 
1595
1604
  const changed = nextToolCallId !== hoveredToolCallId;
1596
1605
  hoveredToolCallId = nextToolCallId;
1597
- if (setHoveredToolIo(nextIoView, nextIoSection) || setHoveredToolGroup(nextGroup) || changed)
1606
+ if (
1607
+ scrollButtonChanged ||
1608
+ setHoveredToolIo(nextIoView, nextIoSection) ||
1609
+ setHoveredToolGroup(nextGroup) ||
1610
+ changed
1611
+ )
1598
1612
  tui.requestRender?.();
1599
1613
  }
1600
1614
 
@@ -1652,7 +1666,10 @@ function renderScrollButton(width: number, theme: any): string[] {
1652
1666
  pendingScrollMessages > 0
1653
1667
  ? `${pendingScrollMessages} new message${pendingScrollMessages === 1 ? "" : "s"}`
1654
1668
  : "Back to bottom";
1655
- const label = theme.fg("accent", `[ ↓ ${messageText} · ${shortcut} ]`);
1669
+ const label = theme.fg(
1670
+ scrollButtonHovered ? "text" : "accent",
1671
+ `[ ↓ ${messageText} · ${shortcut} ]`,
1672
+ );
1656
1673
  const leftPad = Math.max(0, Math.floor((width - visibleWidth(label)) / 2));
1657
1674
  return [`${" ".repeat(leftPad)}${truncateToWidth(label, width, "…")}`];
1658
1675
  }
@@ -1728,6 +1745,7 @@ function restoreToolMouseInputCapture(): void {
1728
1745
  }
1729
1746
 
1730
1747
  function restoreToolMouseRenderPatch(): void {
1748
+ if (toolMouseRenderPatchState) toolMouseRenderPatchState.active = false;
1731
1749
  if (
1732
1750
  toolMouseRenderPatchTui &&
1733
1751
  toolMouseRenderPatchOriginal &&
@@ -1738,7 +1756,11 @@ function restoreToolMouseRenderPatch(): void {
1738
1756
  toolMouseRenderPatchTui = null;
1739
1757
  toolMouseRenderPatchOriginal = null;
1740
1758
  toolMouseRenderPatchWrapper = null;
1759
+ toolMouseRenderPatchState = null;
1741
1760
  toolMouseRawWrite = null;
1761
+ ioViewCollectors.clear();
1762
+ frameIoViews = new Set();
1763
+ frameToolRenders = [];
1742
1764
  }
1743
1765
 
1744
1766
  function patchToolMouseMotionAfterRender(tui: any): void {
@@ -1756,10 +1778,63 @@ function patchToolMouseMotionAfterRender(tui: any): void {
1756
1778
  if (typeof original !== "function" || !rawWrite) return;
1757
1779
 
1758
1780
  toolMouseRawWrite = (data) => Reflect.apply(rawWrite, terminal, [data]);
1781
+ const patchState = { active: true };
1759
1782
  const wrapper = function (this: any, ...args: any[]) {
1760
- const result = Reflect.apply(original, this, args);
1761
- toolMouseRawWrite?.(TOOL_MOUSE_MOTION_ENABLE);
1762
- return result;
1783
+ if (!patchState.active) return Reflect.apply(original, this, args);
1784
+ const renderedViews = new Set<ExpandedToolIoView>();
1785
+ const renderedTools: FrameToolRender[] = [];
1786
+ const tools: any[] = [];
1787
+ const restores: Array<{ component: any; descriptor?: PropertyDescriptor }> = [];
1788
+ collectToolComponents(this, tools);
1789
+ for (const component of tools) {
1790
+ const descriptor = Object.getOwnPropertyDescriptor(component, "render");
1791
+ const originalRender = component.render;
1792
+ if (typeof originalRender !== "function") continue;
1793
+ const wrappedRender = function (this: any, ...renderArgs: any[]) {
1794
+ const lines = Reflect.apply(originalRender, this, renderArgs);
1795
+ if (Array.isArray(lines)) renderedTools.push({ component, lines: [...lines] });
1796
+ return lines;
1797
+ };
1798
+ try {
1799
+ Object.defineProperty(
1800
+ component,
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.
1814
+ }
1815
+ }
1816
+ ioViewCollectors.add(renderedViews);
1817
+ let succeeded = false;
1818
+ try {
1819
+ const result = Reflect.apply(original, this, args);
1820
+ succeeded = true;
1821
+ toolMouseRawWrite?.(TOOL_MOUSE_MOTION_ENABLE);
1822
+ return result;
1823
+ } finally {
1824
+ for (const { component, descriptor } of restores.reverse()) {
1825
+ try {
1826
+ if (descriptor) Object.defineProperty(component, "render", descriptor);
1827
+ else delete component.render;
1828
+ } catch {
1829
+ // Keep restoring the remaining instances after a hostile descriptor change.
1830
+ }
1831
+ }
1832
+ if (succeeded) {
1833
+ frameIoViews = renderedViews;
1834
+ frameToolRenders = renderedTools;
1835
+ }
1836
+ ioViewCollectors.delete(renderedViews);
1837
+ }
1763
1838
  };
1764
1839
  try {
1765
1840
  tui.doRender = wrapper;
@@ -1770,6 +1845,7 @@ function patchToolMouseMotionAfterRender(tui: any): void {
1770
1845
  toolMouseRenderPatchTui = tui;
1771
1846
  toolMouseRenderPatchOriginal = original;
1772
1847
  toolMouseRenderPatchWrapper = wrapper;
1848
+ toolMouseRenderPatchState = patchState;
1773
1849
  toolMouseRawWrite(TOOL_MOUSE_MOTION_ENABLE);
1774
1850
  }
1775
1851
 
@@ -1841,6 +1917,7 @@ function teardownToolMouseInteraction(): void {
1841
1917
  restoreToolMouseInputCapture();
1842
1918
  restoreToolMouseRenderPatch();
1843
1919
  scrollButtonVisible = false;
1920
+ scrollButtonHovered = false;
1844
1921
  scrollButtonWidget = null;
1845
1922
  pendingScrollMessages = 0;
1846
1923
  assistantMessageActive = false;
@@ -1866,7 +1943,10 @@ export function installToolMouseInteraction(
1866
1943
  toolMouseFixedFeaturesEnabled = fixedEditorFeatures;
1867
1944
  ctx.ui.setWidget(TOOL_MOUSE_WIDGET_KEY, (tui: any, theme: any) => {
1868
1945
  toolMouseTui = tui;
1869
- if (fixedEditorFeatures) patchToolMouseInputCapture(tui);
1946
+ if (fixedEditorFeatures) {
1947
+ patchToolMouseInputCapture(tui);
1948
+ patchToolMouseMotionAfterRender(tui);
1949
+ }
1870
1950
  // Motion reporting is required for hover in both native and fixed-editor layouts.
1871
1951
  tui?.terminal?.write?.(fixedEditorFeatures ? TOOL_MOUSE_ENABLE : TOOL_MOUSE_MOTION_ENABLE);
1872
1952
  const widget = {
@@ -1941,8 +2021,8 @@ function modeSettingDescription(mode: CompactStyleMode): string {
1941
2021
 
1942
2022
  function fixedEditorSettingDescription(enabled: boolean): string {
1943
2023
  return enabled
1944
- ? "Mouse capture, 5-row wheel, tool click expand/collapse, viewport mapping, back-to-bottom button, message count, Ctrl+End."
1945
- : "Terminal-native wheel; mouse capture, tool clicks, viewport mapping, and button off. Ctrl+End remains enabled.";
2024
+ ? "Pinned editor via @tifan/pi-fixed-editor. Mouse capture, 5-row wheel, tool clicks, back-to-bottom button, message count, and Ctrl+End."
2025
+ : "Native scrolling editor; fixed-editor mouse features and button off. Ctrl+End remains enabled.";
1946
2026
  }
1947
2027
 
1948
2028
  function excludeRenderersDescription(names: readonly string[]): string {
@@ -2013,7 +2093,7 @@ function buildExcludeRenderersSubmenu(
2013
2093
 
2014
2094
  /** Section tabs for /ccstyle — matches Zentui-style "A / B / C" headers. */
2015
2095
  type CcstyleSection = {
2016
- id: "style" | "editor" | "diff";
2096
+ id: "style" | "editor" | "diff" | "thinking";
2017
2097
  label: string;
2018
2098
  items: any[];
2019
2099
  };
@@ -2053,7 +2133,9 @@ function renderSectionTabBar(
2053
2133
  async function showCcstylePanel(
2054
2134
  ctx: any,
2055
2135
  compactStyle: CompactStyleHooks,
2136
+ fixedEditorController: FixedEditorController,
2056
2137
  toolGrouping?: ToolGroupingHooks,
2138
+ compactThinking?: CompactThinkingController,
2057
2139
  ): Promise<void> {
2058
2140
  if (ctx?.mode !== "tui" || !ctx?.hasUI || typeof ctx.ui?.custom !== "function") {
2059
2141
  ctx.ui?.notify?.("/ccstyle requires TUI mode", "warning");
@@ -2148,6 +2230,27 @@ async function showCcstylePanel(
2148
2230
  ),
2149
2231
  values: [...EXPANDED_PREVIEW_MAX_LINES_VALUES],
2150
2232
  };
2233
+ const thinkingTitleSetting = {
2234
+ id: "useSummaryTitlesAsThinkingTitle",
2235
+ label: "Summary title",
2236
+ description: "Use the latest provider summary as the active thinking title.",
2237
+ currentValue: config.useSummaryTitlesAsThinkingTitle ? "on" : "off",
2238
+ values: ["on", "off"],
2239
+ };
2240
+ const thinkingPreviewSetting = {
2241
+ id: "previewLines",
2242
+ label: "Preview lines",
2243
+ description: "Thinking preview lines; 0 hides the preview body.",
2244
+ currentValue: nearestPreset(config.previewLines, THINKING_PREVIEW_LINES_VALUES),
2245
+ values: [...THINKING_PREVIEW_LINES_VALUES],
2246
+ };
2247
+ const thinkingAnimationSetting = {
2248
+ id: "animationIntervalMs",
2249
+ label: "Animation interval ms",
2250
+ description: "Thinking title animation interval for the next thinking run.",
2251
+ currentValue: nearestPreset(config.animationIntervalMs, THINKING_ANIMATION_INTERVAL_VALUES),
2252
+ values: [...THINKING_ANIMATION_INTERVAL_VALUES],
2253
+ };
2151
2254
 
2152
2255
  const onSettingChange = (id: string, value: string) => {
2153
2256
  switch (id) {
@@ -2161,6 +2264,7 @@ async function showCcstylePanel(
2161
2264
  config.fixedEditorFeatures,
2162
2265
  );
2163
2266
  saveConfig();
2267
+ fixedEditorController.setEnabled(config.fixedEditorFeatures);
2164
2268
  installToolMouseInteraction(ctx);
2165
2269
  refreshCurrentTranscript(compactStyle, ctx);
2166
2270
  ctx.ui.notify(`Fixed editor: ${value}`, "info");
@@ -2207,10 +2311,23 @@ async function showCcstylePanel(
2207
2311
  50_000,
2208
2312
  );
2209
2313
  break;
2314
+ case "useSummaryTitlesAsThinkingTitle":
2315
+ config.useSummaryTitlesAsThinkingTitle = value === "on";
2316
+ break;
2317
+ case "previewLines":
2318
+ config.previewLines = pickPositiveInt(value, DEFAULT_CONFIG.previewLines, 0);
2319
+ break;
2320
+ case "animationIntervalMs":
2321
+ config.animationIntervalMs = pickPositiveNumber(
2322
+ value,
2323
+ DEFAULT_CONFIG.animationIntervalMs,
2324
+ );
2325
+ break;
2210
2326
  default:
2211
2327
  return;
2212
2328
  }
2213
2329
  saveConfig();
2330
+ compactThinking?.updateConfig(getCompactThinkingConfig());
2214
2331
  refreshCurrentTranscript(compactStyle, ctx);
2215
2332
  ctx.ui.notify(`Updated ${id}: ${value}`, "info");
2216
2333
  };
@@ -2238,6 +2355,11 @@ async function showCcstylePanel(
2238
2355
  expandedMaxSetting,
2239
2356
  ],
2240
2357
  },
2358
+ {
2359
+ id: "thinking",
2360
+ label: "Thinking",
2361
+ items: [thinkingTitleSetting, thinkingPreviewSetting, thinkingAnimationSetting],
2362
+ },
2241
2363
  ];
2242
2364
 
2243
2365
  let activeSection = 0;
@@ -2999,17 +3121,35 @@ function notePendingScrollMessage(role: unknown): void {
2999
3121
  toolMouseTui.requestRender?.();
3000
3122
  }
3001
3123
 
3002
- export default function (pi: ExtensionAPI, configOverride?: Partial<Config>) {
3124
+ export default function (
3125
+ pi: ExtensionAPI,
3126
+ configOverride?: Partial<Config>,
3127
+ compactThinking?: CompactThinkingController,
3128
+ ) {
3003
3129
  // The optional override keeps integration tests independent from the user's global config.
3004
3130
  if (configOverride) config = normalizeConfig({ ...config, ...configOverride });
3131
+ const fixedEditorController = installFixedEditor(pi, config.fixedEditorFeatures);
3005
3132
  const writeExecutionMetadata = installWriteOverride(pi, new WriteExecutionMetadataStore());
3006
- const globalToolRendering = installGlobalToolRendering(writeExecutionMetadata);
3007
- const toolGrouping = installToolGrouping(() => config.mode === "on");
3008
- deactivateLegacyCompactionRendering();
3009
- const compactStyle: CompactStyleHooks = installCompactStyle(pi, {
3010
- getMode: () => config.mode,
3011
- getExcludeRenderers: () => config.excludeRenderers,
3012
- });
3133
+ let installation:
3134
+ | {
3135
+ globalToolRendering: GlobalToolRenderPatch;
3136
+ toolGrouping: ToolGroupingHooks;
3137
+ compactStyle: CompactStyleHooks;
3138
+ }
3139
+ | undefined;
3140
+ const ensureTuiInstallation = (ctx: any) => {
3141
+ if (ctx?.mode !== "tui" || !ctx?.hasUI) return undefined;
3142
+ if (installation) return installation;
3143
+ const globalToolRendering = installGlobalToolRendering(writeExecutionMetadata);
3144
+ const toolGrouping = installToolGrouping(() => config.mode === "on");
3145
+ deactivateLegacyCompactionRendering();
3146
+ const compactStyle = installCompactStyle(pi, {
3147
+ getMode: () => config.mode,
3148
+ getExcludeRenderers: () => config.excludeRenderers,
3149
+ });
3150
+ installation = { globalToolRendering, toolGrouping, compactStyle };
3151
+ return installation;
3152
+ };
3013
3153
 
3014
3154
  pi.registerCommand("ccstyle", {
3015
3155
  description: "Configure Claude Code style, fixed editor, and rich diff options",
@@ -3026,11 +3166,22 @@ export default function (pi: ExtensionAPI, configOverride?: Partial<Config>) {
3026
3166
  handler: async (args, ctx) => {
3027
3167
  const arg = args.trim().toLowerCase();
3028
3168
  if (!arg || arg === "panel") {
3029
- await showCcstylePanel(ctx, compactStyle, toolGrouping);
3169
+ const hooks = ensureTuiInstallation(ctx);
3170
+ if (hooks)
3171
+ await showCcstylePanel(
3172
+ ctx,
3173
+ hooks.compactStyle,
3174
+ fixedEditorController,
3175
+ hooks.toolGrouping,
3176
+ compactThinking,
3177
+ );
3178
+ else ctx.ui?.notify?.("/ccstyle requires TUI mode", "warning");
3030
3179
  return;
3031
3180
  }
3032
3181
  if (arg === "on" || arg === "off" || arg === "compact") {
3033
- applyStyleMode(arg, ctx, compactStyle, toolGrouping);
3182
+ const hooks = ensureTuiInstallation(ctx);
3183
+ if (hooks) applyStyleMode(arg, ctx, hooks.compactStyle, hooks.toolGrouping);
3184
+ else ctx.ui?.notify?.("/ccstyle requires TUI mode", "warning");
3034
3185
  return;
3035
3186
  }
3036
3187
  if (arg === "status") {
@@ -3042,69 +3193,81 @@ export default function (pi: ExtensionAPI, configOverride?: Partial<Config>) {
3042
3193
  });
3043
3194
 
3044
3195
  pi.on("session_start", async (event, ctx) => {
3045
- toolGrouping.setTheme(ctx.ui.theme);
3046
- compactStyle?.onSessionStart(event, ctx);
3196
+ const hooks = ensureTuiInstallation(ctx);
3197
+ if (!hooks) return;
3198
+ hooks.toolGrouping.setTheme(ctx.ui.theme);
3199
+ hooks.compactStyle.onSessionStart(event, ctx);
3047
3200
  pendingScrollMessages = 0;
3048
3201
  assistantMessageActive = false;
3049
3202
  ctx.ui.setStatus("ccstyle", undefined);
3050
3203
  installToolMouseInteraction(ctx);
3051
- scheduleSessionRender(compactStyle.refresh);
3204
+ scheduleSessionRender(hooks.compactStyle.refresh);
3052
3205
  });
3053
3206
 
3054
3207
  pi.on("session_compact", async (event, ctx) => {
3055
- toolGrouping.setTheme(ctx.ui.theme);
3056
- compactStyle?.onSessionCompact(event, ctx);
3208
+ const hooks = ensureTuiInstallation(ctx);
3209
+ if (!hooks) return;
3210
+ hooks.toolGrouping.setTheme(ctx.ui.theme);
3211
+ hooks.compactStyle.onSessionCompact(event, ctx);
3057
3212
  // Compaction rebuilds the transcript without session_start. Rebind after
3058
3213
  // other TUI extensions may have replaced the root input dispatcher.
3059
3214
  installToolMouseInteraction(ctx);
3060
- scheduleSessionRender(compactStyle.refresh);
3215
+ scheduleSessionRender(hooks.compactStyle.refresh);
3061
3216
  });
3062
3217
 
3063
3218
  pi.on("message_start", async (event) => {
3064
- notePendingScrollMessage(event?.message?.role);
3219
+ if (installation) notePendingScrollMessage(event?.message?.role);
3065
3220
  });
3066
3221
 
3067
3222
  pi.on("message_update", async (event, ctx) => {
3068
- compactStyle?.onMessageUpdate(event, ctx);
3069
- if (event?.message?.role === "assistant") notePendingScrollMessage("assistant");
3223
+ installation?.compactStyle.onMessageUpdate(event, ctx);
3224
+ if (installation && event?.message?.role === "assistant") notePendingScrollMessage("assistant");
3070
3225
  });
3071
3226
 
3072
3227
  pi.on("message_end", async (event) => {
3073
- if (event?.message?.role === "assistant") assistantMessageActive = false;
3228
+ if (installation && event?.message?.role === "assistant") assistantMessageActive = false;
3074
3229
  });
3075
3230
 
3076
3231
  pi.on("agent_start", async (event, ctx) => {
3077
- compactStyle?.onAgentStart(event, ctx);
3232
+ installation?.compactStyle.onAgentStart(event, ctx);
3078
3233
  });
3079
3234
 
3080
3235
  pi.on("agent_end", async (event, ctx) => {
3081
- compactStyle?.onAgentEnd(event, ctx);
3236
+ installation?.compactStyle.onAgentEnd(event, ctx);
3082
3237
  });
3083
3238
 
3084
3239
  pi.on("turn_start", async (event, ctx) => {
3085
- compactStyle?.onTurnStart(event, ctx);
3240
+ installation?.compactStyle.onTurnStart(event, ctx);
3086
3241
  });
3087
3242
 
3088
3243
  pi.on("tool_execution_start", async (event, ctx) => {
3089
- toolGrouping.setTheme(ctx.ui.theme);
3090
- compactStyle?.onToolExecutionStart(event, ctx);
3244
+ installation?.toolGrouping.setTheme(ctx.ui.theme);
3245
+ installation?.compactStyle.onToolExecutionStart(event, ctx);
3091
3246
  });
3092
3247
 
3093
3248
  pi.on("tool_execution_update", async (event, ctx) => {
3094
- compactStyle?.onToolExecutionUpdate(event, ctx);
3249
+ installation?.compactStyle.onToolExecutionUpdate(event, ctx);
3095
3250
  });
3096
3251
 
3097
3252
  pi.on("tool_execution_end", async (event, ctx) => {
3098
- compactStyle?.onToolExecutionEnd(event, ctx);
3253
+ installation?.compactStyle.onToolExecutionEnd(event, ctx);
3099
3254
  });
3100
3255
 
3101
3256
  pi.on("session_shutdown", async (event, ctx) => {
3102
- compactStyle?.onSessionShutdown(event, ctx);
3103
3257
  writeExecutionMetadata.clear();
3258
+ const current = installation;
3259
+ if (
3260
+ !current ||
3261
+ (globalThis as any)[GLOBAL_TOOL_RENDER_PATCH] !== current.globalToolRendering ||
3262
+ !current.globalToolRendering.active
3263
+ )
3264
+ return;
3265
+ current.compactStyle.onSessionShutdown(event, ctx);
3104
3266
  teardownToolMouseInteraction();
3105
- deactivateGlobalToolRendering(globalToolRendering);
3106
- toolGrouping.shutdown();
3267
+ deactivateGlobalToolRendering(current.globalToolRendering);
3268
+ current.toolGrouping.shutdown();
3107
3269
  deactivateLegacyCompactionRendering();
3108
3270
  clearAllAnimations();
3271
+ installation = undefined;
3109
3272
  });
3110
3273
  }