pi-cc-extensions 0.7.11 → 0.8.1

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,56 +1,75 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
+ DynamicBorder,
3
4
  keyHint,
4
- getSettingsListTheme,
5
5
  ToolExecutionComponent,
6
6
  } from "@earendil-works/pi-coding-agent";
7
- import { Container, SettingsList, Text, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
7
+ import {
8
+ installCompactStyle,
9
+ type CompactStyleHooks,
10
+ type CompactStyleMode,
11
+ } from "./compact-style.ts";
12
+ import { sanitizeToolResultText } from "./tool-result-sanitize.ts";
13
+ import { Container, SelectList, Text, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
8
14
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
9
15
  import { homedir } from "node:os";
10
16
  import { join } from "node:path";
11
17
 
12
18
  /**
13
- * Claude Code Style for pi
14
- *
15
- * Tool calls and results use a compact fallback renderer by default. Renderers
16
- * listed in excludeRenderers remain untouched. A collapsed tool result can be
17
- * clicked on its Ctrl+O hint to expand only that tool.
18
- *
19
- * Dynamic commands:
20
- * /ccstyle open interactive settings
21
- * /ccstyle on enable
22
- * /ccstyle off disable
23
- * /ccstyle status show current state
19
+ * Claude Code Style for pi.
24
20
  *
25
- * Shortcut:
26
- * ctrl+shift+o toggle on/off
21
+ * This is the package's only entry point. Compact transcript rendering lives in
22
+ * the internal compact-style module and is routed by the mode below.
27
23
  */
28
24
 
29
- type Config = {
30
- enabled: boolean;
25
+ export type Config = {
26
+ mode: CompactStyleMode;
31
27
  excludeRenderers: string[];
32
28
  };
33
29
 
34
30
  const CONFIG_PATH = join(homedir(), ".pi", "agent", "claude-code-style.json");
35
31
 
32
+ export function normalizeConfig(input: unknown): Config {
33
+ const source = (input && typeof input === "object" ? input : {}) as Record<string, unknown>;
34
+ const mode = source.mode;
35
+ const migratedMode: CompactStyleMode =
36
+ mode === "on" || mode === "off" || mode === "compact"
37
+ ? mode
38
+ : typeof source.enabled === "boolean"
39
+ ? (source.enabled ? "on" : "off")
40
+ : "on";
41
+ const excludeRenderers = Array.isArray(source.excludeRenderers)
42
+ ? [...new Set(source.excludeRenderers.filter((name): name is string => typeof name === "string" && name.length > 0))]
43
+ : [];
44
+ return { mode: migratedMode, excludeRenderers };
45
+ }
46
+
36
47
  let config: Config = loadConfig();
37
48
 
38
49
  function loadConfig(): Config {
39
50
  try {
40
51
  if (existsSync(CONFIG_PATH)) {
41
- const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Partial<Config>;
42
- const excludeRenderers = Array.isArray(parsed.excludeRenderers)
43
- ? [...new Set(parsed.excludeRenderers.filter((name): name is string => typeof name === "string" && name.length > 0))]
44
- : [];
45
- return {
46
- enabled: parsed.enabled ?? true,
47
- excludeRenderers,
48
- };
52
+ const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as unknown;
53
+ const normalized = normalizeConfig(parsed);
54
+ const source = parsed as Record<string, unknown>;
55
+ // Persist the one-time enabled:boolean -> mode migration while retaining
56
+ // the existing exclusion list.
57
+ if (
58
+ typeof source.enabled === "boolean"
59
+ && (source.mode !== "on" && source.mode !== "off" && source.mode !== "compact")
60
+ ) {
61
+ try {
62
+ writeFileSync(CONFIG_PATH, JSON.stringify(normalized, null, 2));
63
+ } catch {
64
+ // A read-only config still uses the migrated in-memory value.
65
+ }
66
+ }
67
+ return normalized;
49
68
  }
50
69
  } catch {
51
70
  // Ignore bad config and fall back to defaults.
52
71
  }
53
- return { enabled: true, excludeRenderers: [] };
72
+ return { mode: "on", excludeRenderers: [] };
54
73
  }
55
74
 
56
75
  function saveConfig() {
@@ -64,24 +83,6 @@ function oneLine(value: unknown, max = 72): string {
64
83
  return text.length > max ? `${text.slice(0, max - 1)}…` : text;
65
84
  }
66
85
 
67
- const UNSAFE_TERMINAL_ESCAPE = new RegExp(
68
- "\\u001B\\][\\s\\S]*?(?:\\u0007|\\u001B\\x5C)"
69
- + "|\\u001B[PX^_][\\s\\S]*?\\u001B\\x5C"
70
- + "|(?:\\u001B\\[|\\u009B)[0-?]*[ -/]*[@-~]"
71
- + "|\\u001B[@-_]",
72
- "g",
73
- );
74
-
75
- /** Prevent captured terminal control responses from being replayed by tool renderers. */
76
- function sanitizeToolResultText(value: string): string {
77
- return value
78
- .replace(UNSAFE_TERMINAL_ESCAPE, "")
79
- .replace(/\x1B/g, "")
80
- .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, "")
81
- .replace(/\r\n/g, "\n")
82
- .replace(/\r/g, "\n");
83
- }
84
-
85
86
  function textFromResult(result: any): string {
86
87
  const item = result?.content?.find?.((c: any) => c.type === "text");
87
88
  return item?.type === "text" ? sanitizeToolResultText(item.text ?? "") : "";
@@ -253,7 +254,7 @@ export class ExpandedToolResultText {
253
254
  }
254
255
 
255
256
  function renderCollapsedToolResult(body: string, collapsedHint = ""): string {
256
- return ` ⎿ ${body}${collapsedHint}`;
257
+ return `\n ⎿ ${body}${collapsedHint}`;
257
258
  }
258
259
 
259
260
  function renderExpandedToolResult(
@@ -938,49 +939,57 @@ function scheduleSessionRender(): void {
938
939
  const BRIGHT_GREEN = "\x1b[38;2;80;220;100m";
939
940
  const ANSI_RESET = "\x1b[0m";
940
941
 
941
- function applyEnabledState(enabled: boolean, ctx: any): void {
942
- config.enabled = enabled;
942
+ function refreshCurrentTranscript(compactStyle: CompactStyleHooks, ctx?: any): void {
943
+ compactStyle.refresh();
944
+ toolMouseTui?.requestRender?.(true);
945
+ ctx?.ui?.requestRender?.(true);
946
+ }
947
+
948
+ function applyStyleMode(mode: CompactStyleMode, ctx: any, compactStyle: CompactStyleHooks): void {
949
+ config.mode = mode;
943
950
  saveConfig();
944
- ctx.ui.notify(`Claude Code style: ${enabled ? "on" : "off"}`, "info");
951
+ refreshCurrentTranscript(compactStyle, ctx);
952
+ ctx.ui.notify(`Claude Code style: ${mode}`, "info");
945
953
  }
946
954
 
947
- async function showCcstylePanel(ctx: any): Promise<void> {
955
+ async function showCcstylePanel(ctx: any, compactStyle: CompactStyleHooks): Promise<void> {
948
956
  if (ctx?.mode !== "tui" || !ctx?.hasUI || typeof ctx.ui?.custom !== "function") {
949
957
  ctx.ui?.notify?.("/ccstyle requires TUI mode", "warning");
950
958
  return;
951
959
  }
952
960
 
953
- await ctx.ui.custom((tui: any, theme: any, _keybindings: any, done: (value?: void) => void) => {
961
+ const modes: Array<{ value: CompactStyleMode; label: string; description: string }> = [
962
+ { value: "on", label: "on", description: "Claude Code style" },
963
+ { value: "off", label: "off", description: "Pi native output" },
964
+ { value: "compact", label: "compact", description: "Compact transcript" },
965
+ ];
966
+ const selectedMode = await (ctx.ui.custom((tui: any, theme: any, _keybindings: any, done: (value?: CompactStyleMode) => void) => {
954
967
  const container = new Container();
955
- container.addChild(new Text(theme.fg("accent", theme.bold("Claude Code Style")), 1, 1));
956
- const settingsList = new SettingsList(
957
- [{
958
- id: "enabled",
959
- label: "Output style",
960
- description: "Use compact Claude Code-like tool rendering",
961
- currentValue: config.enabled ? "on" : "off",
962
- values: ["on", "off"],
963
- }],
964
- 3,
965
- getSettingsListTheme(),
966
- (id: string, newValue: string) => {
967
- if (id !== "enabled" || (newValue !== "on" && newValue !== "off")) return;
968
- applyEnabledState(newValue === "on", ctx);
969
- settingsList.updateValue(id, newValue);
970
- tui.requestRender();
971
- },
972
- () => done(),
973
- );
974
- container.addChild(settingsList);
968
+ container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
969
+ container.addChild(new Text(theme.fg("accent", theme.bold("Claude Code Style")), 1, 0));
970
+ const selectList = new SelectList(modes, modes.length, {
971
+ selectedPrefix: (text: string) => theme.fg("accent", text),
972
+ selectedText: (text: string) => theme.fg("accent", text),
973
+ description: (text: string) => theme.fg("muted", text),
974
+ scrollInfo: (text: string) => theme.fg("dim", text),
975
+ noMatch: (text: string) => theme.fg("warning", text),
976
+ });
977
+ selectList.setSelectedIndex(modes.findIndex((item) => item.value === config.mode));
978
+ selectList.onSelect = (item: { value: string }) => done(item.value as CompactStyleMode);
979
+ selectList.onCancel = () => done();
980
+ container.addChild(selectList);
981
+ container.addChild(new Text(theme.fg("dim", "↑↓ navigate · enter select · esc cancel"), 1, 0));
982
+ container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
975
983
  return {
976
984
  render: (width: number) => container.render(width),
977
985
  invalidate: () => container.invalidate(),
978
986
  handleInput(data: string) {
979
- settingsList.handleInput?.(data);
987
+ selectList.handleInput(data);
980
988
  tui.requestRender();
981
989
  },
982
990
  };
983
- });
991
+ }) as Promise<CompactStyleMode | undefined>);
992
+ if (selectedMode) applyStyleMode(selectedMode, ctx, compactStyle);
984
993
  }
985
994
 
986
995
  function renderDefault(tool: any, slot: "renderCall" | "renderResult", args: any[], fallback = "") {
@@ -1027,7 +1036,7 @@ function createCcstyleTool(originalTool: any): any {
1027
1036
  ...originalTool,
1028
1037
  renderShell: "self",
1029
1038
  renderCall(args: any, theme: any, context: any) {
1030
- if (!config.enabled) {
1039
+ if (config.mode !== "on") {
1031
1040
  return renderDefault(originalTool, "renderCall", [args, theme, context], String(toolName));
1032
1041
  }
1033
1042
 
@@ -1046,7 +1055,7 @@ function createCcstyleTool(originalTool: any): any {
1046
1055
  return new Text(title, 0, 0);
1047
1056
  },
1048
1057
  renderResult(result: any, options: any, theme: any, context: any) {
1049
- if (!config.enabled) {
1058
+ if (config.mode !== "on") {
1050
1059
  return renderDefault(originalTool, "renderResult", [result, options, theme, context], textFromResult(result));
1051
1060
  }
1052
1061
 
@@ -1082,19 +1091,31 @@ const GLOBAL_TOOL_RENDER_PATCH = Symbol.for("pi.ccstyle.global-tool-render-patch
1082
1091
  const COMPONENT_TOOL_RENDER_MODE = Symbol.for("pi.ccstyle.component-tool-render-mode");
1083
1092
  const COMPONENT_TOOL_SELF_SHELL_MODE = Symbol.for("pi.ccstyle.component-tool-self-shell-mode");
1084
1093
  const SUBAGENT_TOOL_NAMES = new Set(["Agent"]);
1085
- const globalToolRenderOwner = {};
1094
+
1095
+ type ToolRenderMethods = {
1096
+ hasRendererDefinition: (...args: any[]) => boolean;
1097
+ getRenderShell: (...args: any[]) => "default" | "self";
1098
+ getCallRenderer: (...args: any[]) => any;
1099
+ getResultRenderer: (...args: any[]) => any;
1100
+ };
1086
1101
 
1087
1102
  type GlobalToolRenderPatch = {
1103
+ version: 2;
1088
1104
  prototype: any;
1089
1105
  owner: object;
1106
+ active: boolean;
1090
1107
  enabled: () => boolean;
1108
+ mode: () => CompactStyleMode;
1091
1109
  wrap: (tool: any) => any;
1092
1110
  byDefinition: WeakMap<object, any>;
1093
1111
  byName: Map<string, any>;
1094
- originalHasRendererDefinition: (...args: any[]) => boolean;
1095
- originalGetRenderShell: (...args: any[]) => "default" | "self";
1096
- originalGetCallRenderer: (...args: any[]) => any;
1097
- originalGetResultRenderer: (...args: any[]) => any;
1112
+ downstream: ToolRenderMethods;
1113
+ installed: ToolRenderMethods;
1114
+ // Keep the legacy aliases so an older extension build can read this Symbol.
1115
+ originalHasRendererDefinition: ToolRenderMethods["hasRendererDefinition"];
1116
+ originalGetRenderShell: ToolRenderMethods["getRenderShell"];
1117
+ originalGetCallRenderer: ToolRenderMethods["getCallRenderer"];
1118
+ originalGetResultRenderer: ToolRenderMethods["getResultRenderer"];
1098
1119
  };
1099
1120
 
1100
1121
  function isMcpToolDefinition(definition: any, toolName: string): boolean {
@@ -1118,33 +1139,43 @@ export function preservesOriginalRenderer(
1118
1139
  );
1119
1140
  }
1120
1141
 
1121
- function shouldGloballyStyleTool(component: any, patch: GlobalToolRenderPatch): boolean {
1122
- const selectedMode = component[COMPONENT_TOOL_RENDER_MODE];
1123
- if (typeof selectedMode === "boolean") return selectedMode;
1142
+ function syncToolShell(component: any, shell: "default" | "self"): void {
1143
+ const target = shell === "self" ? component.selfRenderContainer : component.contentBox;
1144
+ if (!target || !Array.isArray(component.children)) return;
1145
+ const candidates = new Set([component.contentText, component.contentBox, component.selfRenderContainer].filter(Boolean));
1146
+ const indexes = component.children
1147
+ .map((child: any, index: number) => candidates.has(child) ? index : -1)
1148
+ .filter((index: number) => index >= 0);
1149
+ const targetIndex = indexes[0];
1150
+ // During construction getRenderShell() runs immediately before Pi mounts the
1151
+ // selected shell. Do not mount it here or the constructor will add it twice.
1152
+ if (targetIndex === undefined) return;
1153
+ component.children[targetIndex] = target;
1154
+ for (const index of indexes.sort((left: number, right: number) => right - left)) {
1155
+ if (index !== targetIndex) component.children.splice(index, 1);
1156
+ }
1157
+ }
1124
1158
 
1159
+ function shouldGloballyStyleTool(component: any, patch: GlobalToolRenderPatch): boolean {
1125
1160
  const extensionDefinition = component.toolDefinition;
1126
1161
  const builtInDefinition = component.builtInToolDefinition;
1127
1162
  const definition = extensionDefinition ?? builtInDefinition;
1128
1163
  const toolName = String(component.toolName || definition?.name || "");
1129
1164
  const useCcstyle =
1130
- patch.enabled() &&
1165
+ patch.mode() === "on" &&
1131
1166
  !preservesOriginalRenderer(extensionDefinition, toolName, builtInDefinition);
1132
- // ToolExecutionComponent chooses its child shell in the constructor. Keep that
1133
- // choice stable for this row so toggling ccstyle cannot switch containers later.
1134
1167
  component[COMPONENT_TOOL_RENDER_MODE] = useCcstyle;
1135
1168
  return useCcstyle;
1136
1169
  }
1137
1170
 
1138
1171
  function shouldUseSelfShell(component: any, patch: GlobalToolRenderPatch): boolean {
1139
- const selectedMode = component[COMPONENT_TOOL_SELF_SHELL_MODE];
1140
- if (typeof selectedMode === "boolean") return selectedMode;
1141
-
1142
1172
  const definition = component.toolDefinition ?? component.builtInToolDefinition;
1143
1173
  const toolName = String(component.toolName || definition?.name || "");
1144
1174
  const useSelfShell =
1145
- patch.enabled() &&
1175
+ patch.mode() !== "off" &&
1146
1176
  SUBAGENT_TOOL_NAMES.has(toolName) &&
1147
- definition?.renderShell !== "self";
1177
+ definition != null &&
1178
+ definition.renderShell === undefined;
1148
1179
  component[COMPONENT_TOOL_SELF_SHELL_MODE] = useSelfShell;
1149
1180
  return useSelfShell;
1150
1181
  }
@@ -1169,61 +1200,171 @@ function getGloballyStyledTool(component: any, patch: GlobalToolRenderPatch): an
1169
1200
  return wrapped;
1170
1201
  }
1171
1202
 
1172
- function installGlobalToolRendering() {
1173
- const prototype = (ToolExecutionComponent as any).prototype;
1174
- const host = globalThis as any;
1175
- let patch = host[GLOBAL_TOOL_RENDER_PATCH] as GlobalToolRenderPatch | undefined;
1176
-
1177
- if (!patch || patch.prototype !== prototype) {
1178
- patch = {
1179
- prototype,
1180
- owner: globalToolRenderOwner,
1181
- enabled: () => config.enabled,
1182
- wrap: createCcstyleTool,
1183
- byDefinition: new WeakMap(),
1184
- byName: new Map(),
1185
- originalHasRendererDefinition: prototype.hasRendererDefinition,
1186
- originalGetRenderShell: prototype.getRenderShell,
1187
- originalGetCallRenderer: prototype.getCallRenderer,
1188
- originalGetResultRenderer: prototype.getResultRenderer,
1189
- };
1203
+ function prototypeToolRenderMethods(prototype: any): ToolRenderMethods {
1204
+ return {
1205
+ hasRendererDefinition: prototype.hasRendererDefinition,
1206
+ getRenderShell: prototype.getRenderShell,
1207
+ getCallRenderer: prototype.getCallRenderer,
1208
+ getResultRenderer: prototype.getResultRenderer,
1209
+ };
1210
+ }
1190
1211
 
1191
- host[GLOBAL_TOOL_RENDER_PATCH] = patch;
1212
+ function isOwnershipAwarePatch(value: any): value is GlobalToolRenderPatch {
1213
+ if (!value || value.version !== 2 || !value.installed || !value.downstream) return false;
1214
+ return ["hasRendererDefinition", "getRenderShell", "getCallRenderer", "getResultRenderer"]
1215
+ .every((name) => typeof value.installed[name] === "function" && typeof value.downstream[name] === "function");
1216
+ }
1217
+
1218
+ function isLegacyInstalledWrapper(method: unknown, downstreamField: string): boolean {
1219
+ if (typeof method !== "function") return false;
1220
+ try {
1221
+ const source = Function.prototype.toString.call(method);
1222
+ return source.includes(downstreamField)
1223
+ && (
1224
+ source.includes("shouldGloballyStyleTool")
1225
+ || source.includes("shouldUseSelfShell")
1226
+ || source.includes("getGloballyStyledTool")
1227
+ );
1228
+ } catch {
1229
+ return false;
1192
1230
  }
1231
+ }
1193
1232
 
1194
- patch.owner = globalToolRenderOwner;
1195
- patch.enabled = () => config.enabled;
1196
- patch.wrap = createCcstyleTool;
1197
- patch.byDefinition = new WeakMap();
1198
- patch.byName.clear();
1199
-
1200
- // Rebind on every load. /reload preserves the shared prototype and global
1201
- // patch object, so leaving these closures installed only in the initialization
1202
- // branch would keep the previous extension version's rendering logic alive.
1203
- prototype.hasRendererDefinition = function (...args: any[]) {
1204
- if (shouldGloballyStyleTool(this, patch!)) return true;
1205
- return patch!.originalHasRendererDefinition.apply(this, args);
1206
- };
1207
- prototype.getRenderShell = function (...args: any[]) {
1208
- if (shouldUseSelfShell(this, patch!) || shouldGloballyStyleTool(this, patch!)) return "self";
1209
- return patch!.originalGetRenderShell.apply(this, args);
1210
- };
1211
- prototype.getCallRenderer = function (...args: any[]) {
1212
- if (shouldGloballyStyleTool(this, patch!)) return getGloballyStyledTool(this, patch!).renderCall;
1213
- return patch!.originalGetCallRenderer.apply(this, args);
1233
+ function downstreamForGlobalToolInstall(prototype: any, previous: any): ToolRenderMethods {
1234
+ const current = prototypeToolRenderMethods(prototype);
1235
+ if (!previous || previous.prototype !== prototype) return current;
1236
+ if (isOwnershipAwarePatch(previous)) {
1237
+ return {
1238
+ hasRendererDefinition: current.hasRendererDefinition === previous.installed.hasRendererDefinition
1239
+ ? previous.downstream.hasRendererDefinition
1240
+ : current.hasRendererDefinition,
1241
+ getRenderShell: current.getRenderShell === previous.installed.getRenderShell
1242
+ ? previous.downstream.getRenderShell
1243
+ : current.getRenderShell,
1244
+ getCallRenderer: current.getCallRenderer === previous.installed.getCallRenderer
1245
+ ? previous.downstream.getCallRenderer
1246
+ : current.getCallRenderer,
1247
+ getResultRenderer: current.getResultRenderer === previous.installed.getResultRenderer
1248
+ ? previous.downstream.getResultRenderer
1249
+ : current.getResultRenderer,
1250
+ };
1251
+ }
1252
+
1253
+ // Pre-v2 Symbol state did not retain wrapper references. Recognize its known
1254
+ // wrappers when possible; otherwise preserve the current method as external.
1255
+ const legacyDownstream = (method: Function, field: string): Function => {
1256
+ const saved = previous[field];
1257
+ return typeof saved === "function" && isLegacyInstalledWrapper(method, field) ? saved : method;
1214
1258
  };
1215
- prototype.getResultRenderer = function (...args: any[]) {
1216
- if (shouldGloballyStyleTool(this, patch!)) return getGloballyStyledTool(this, patch!).renderResult;
1217
- return patch!.originalGetResultRenderer.apply(this, args);
1259
+ return {
1260
+ hasRendererDefinition: legacyDownstream(
1261
+ current.hasRendererDefinition,
1262
+ "originalHasRendererDefinition",
1263
+ ) as ToolRenderMethods["hasRendererDefinition"],
1264
+ getRenderShell: legacyDownstream(
1265
+ current.getRenderShell,
1266
+ "originalGetRenderShell",
1267
+ ) as ToolRenderMethods["getRenderShell"],
1268
+ getCallRenderer: legacyDownstream(
1269
+ current.getCallRenderer,
1270
+ "originalGetCallRenderer",
1271
+ ) as ToolRenderMethods["getCallRenderer"],
1272
+ getResultRenderer: legacyDownstream(
1273
+ current.getResultRenderer,
1274
+ "originalGetResultRenderer",
1275
+ ) as ToolRenderMethods["getResultRenderer"],
1218
1276
  };
1219
1277
  }
1220
1278
 
1221
- function deactivateGlobalToolRendering() {
1222
- const patch = (globalThis as any)[GLOBAL_TOOL_RENDER_PATCH] as GlobalToolRenderPatch | undefined;
1223
- if (patch?.owner !== globalToolRenderOwner) return;
1279
+ function disconnectGlobalToolRenderPatch(patch: any): void {
1280
+ if (!patch || typeof patch !== "object") return;
1281
+ patch.active = false;
1224
1282
  patch.enabled = () => false;
1283
+ patch.mode = () => "off";
1284
+ patch.wrap = (tool: any) => tool;
1225
1285
  patch.byDefinition = new WeakMap();
1226
- patch.byName.clear();
1286
+ if (patch.byName && typeof patch.byName.clear === "function") patch.byName.clear();
1287
+ else patch.byName = new Map();
1288
+ }
1289
+
1290
+ function installGlobalToolRendering(): GlobalToolRenderPatch {
1291
+ const prototype = (ToolExecutionComponent as any).prototype;
1292
+ const host = globalThis as any;
1293
+ const previous = host[GLOBAL_TOOL_RENDER_PATCH];
1294
+ const downstream = downstreamForGlobalToolInstall(prototype, previous);
1295
+ // Any wrapper retained by an external owner must become a callback-free
1296
+ // pass-through before the new installation is linked above it.
1297
+ disconnectGlobalToolRenderPatch(previous);
1298
+
1299
+ const patch: GlobalToolRenderPatch = {
1300
+ version: 2,
1301
+ prototype,
1302
+ owner: {},
1303
+ active: true,
1304
+ enabled: () => config.mode === "on",
1305
+ mode: () => config.mode,
1306
+ wrap: createCcstyleTool,
1307
+ byDefinition: new WeakMap(),
1308
+ byName: new Map(),
1309
+ downstream,
1310
+ installed: undefined as any,
1311
+ originalHasRendererDefinition: downstream.hasRendererDefinition,
1312
+ originalGetRenderShell: downstream.getRenderShell,
1313
+ originalGetCallRenderer: downstream.getCallRenderer,
1314
+ originalGetResultRenderer: downstream.getResultRenderer,
1315
+ };
1316
+
1317
+ patch.installed = {
1318
+ hasRendererDefinition: function (this: any, ...args: any[]) {
1319
+ if (patch.active && shouldGloballyStyleTool(this, patch)) return true;
1320
+ return patch.downstream.hasRendererDefinition.apply(this, args);
1321
+ },
1322
+ getRenderShell: function (this: any, ...args: any[]) {
1323
+ if (!patch.active) return patch.downstream.getRenderShell.apply(this, args);
1324
+ const shell = shouldUseSelfShell(this, patch) || shouldGloballyStyleTool(this, patch)
1325
+ ? "self"
1326
+ : patch.downstream.getRenderShell.apply(this, args);
1327
+ syncToolShell(this, shell);
1328
+ return shell;
1329
+ },
1330
+ getCallRenderer: function (this: any, ...args: any[]) {
1331
+ if (patch.active && shouldGloballyStyleTool(this, patch)) {
1332
+ return getGloballyStyledTool(this, patch).renderCall;
1333
+ }
1334
+ return patch.downstream.getCallRenderer.apply(this, args);
1335
+ },
1336
+ getResultRenderer: function (this: any, ...args: any[]) {
1337
+ if (patch.active && shouldGloballyStyleTool(this, patch)) {
1338
+ return getGloballyStyledTool(this, patch).renderResult;
1339
+ }
1340
+ return patch.downstream.getResultRenderer.apply(this, args);
1341
+ },
1342
+ };
1343
+
1344
+ prototype.hasRendererDefinition = patch.installed.hasRendererDefinition;
1345
+ prototype.getRenderShell = patch.installed.getRenderShell;
1346
+ prototype.getCallRenderer = patch.installed.getCallRenderer;
1347
+ prototype.getResultRenderer = patch.installed.getResultRenderer;
1348
+ host[GLOBAL_TOOL_RENDER_PATCH] = patch;
1349
+ return patch;
1350
+ }
1351
+
1352
+ function deactivateGlobalToolRendering(patch: GlobalToolRenderPatch): void {
1353
+ if (!patch.active) return;
1354
+ disconnectGlobalToolRenderPatch(patch);
1355
+ const prototype = patch.prototype;
1356
+ if (prototype.hasRendererDefinition === patch.installed.hasRendererDefinition) {
1357
+ prototype.hasRendererDefinition = patch.downstream.hasRendererDefinition;
1358
+ }
1359
+ if (prototype.getRenderShell === patch.installed.getRenderShell) {
1360
+ prototype.getRenderShell = patch.downstream.getRenderShell;
1361
+ }
1362
+ if (prototype.getCallRenderer === patch.installed.getCallRenderer) {
1363
+ prototype.getCallRenderer = patch.downstream.getCallRenderer;
1364
+ }
1365
+ if (prototype.getResultRenderer === patch.installed.getResultRenderer) {
1366
+ prototype.getResultRenderer = patch.downstream.getResultRenderer;
1367
+ }
1227
1368
  }
1228
1369
 
1229
1370
  const GLOBAL_COMPACTION_RENDER_PATCH = Symbol.for("pi.ccstyle.compaction-render-patch");
@@ -1251,15 +1392,20 @@ function notePendingScrollMessage(role: unknown): void {
1251
1392
  }
1252
1393
 
1253
1394
  export default function (pi: ExtensionAPI) {
1254
- installGlobalToolRendering();
1395
+ const globalToolRendering = installGlobalToolRendering();
1255
1396
  deactivateLegacyCompactionRendering();
1397
+ const compactStyle: CompactStyleHooks = installCompactStyle(pi, {
1398
+ getMode: () => config.mode,
1399
+ getExcludeRenderers: () => config.excludeRenderers,
1400
+ });
1256
1401
 
1257
1402
  pi.registerCommand("ccstyle", {
1258
1403
  description: "Configure Claude Code style",
1259
1404
  getArgumentCompletions: (prefix) => {
1260
1405
  const topLevel = [
1261
1406
  { value: "on", label: "on", description: "Enable Claude Code style" },
1262
- { value: "off", label: "off", description: "Disable Claude Code style" },
1407
+ { value: "off", label: "off", description: "Use Pi's native renderer" },
1408
+ { value: "compact", label: "compact", description: "Use compact transcript rendering" },
1263
1409
  { value: "status", label: "status", description: "Show current state" },
1264
1410
  ];
1265
1411
  return topLevel.filter((item) => item.value.startsWith(prefix));
@@ -1267,31 +1413,23 @@ export default function (pi: ExtensionAPI) {
1267
1413
  handler: async (args, ctx) => {
1268
1414
  const arg = args.trim().toLowerCase();
1269
1415
  if (!arg) {
1270
- await showCcstylePanel(ctx);
1416
+ await showCcstylePanel(ctx, compactStyle);
1271
1417
  return;
1272
1418
  }
1273
- if (arg === "on" || arg === "off") {
1274
- applyEnabledState(arg === "on", ctx);
1419
+ if (arg === "on" || arg === "off" || arg === "compact") {
1420
+ applyStyleMode(arg, ctx, compactStyle);
1275
1421
  return;
1276
1422
  }
1277
1423
  if (arg === "status") {
1278
- ctx.ui.notify(`Claude Code style: ${config.enabled ? "on" : "off"}`, "info");
1424
+ ctx.ui.notify(`Claude Code style: ${config.mode}`, "info");
1279
1425
  return;
1280
1426
  }
1281
- ctx.ui.notify("Usage: /ccstyle [on|off|status]", "warning");
1427
+ ctx.ui.notify("Usage: /ccstyle [on|off|compact|status]", "warning");
1282
1428
  },
1283
1429
  });
1284
1430
 
1285
- pi.registerShortcut("ctrl+shift+o", {
1286
- description: "Toggle Claude Code-like output style",
1287
- handler: async (ctx) => {
1288
- config.enabled = !config.enabled;
1289
- saveConfig();
1290
- ctx.ui.notify(`Claude Code style: ${config.enabled ? "on" : "off"}`, "info");
1291
- },
1292
- });
1293
-
1294
- pi.on("session_start", async (_event, ctx) => {
1431
+ pi.on("session_start", async (event, ctx) => {
1432
+ compactStyle?.onSessionStart(event, ctx);
1295
1433
  pendingScrollMessages = 0;
1296
1434
  assistantMessageActive = false;
1297
1435
  ctx.ui.setStatus("ccstyle", undefined);
@@ -1303,7 +1441,8 @@ export default function (pi: ExtensionAPI) {
1303
1441
  notePendingScrollMessage(event?.message?.role);
1304
1442
  });
1305
1443
 
1306
- pi.on("message_update", async (event) => {
1444
+ pi.on("message_update", async (event, ctx) => {
1445
+ compactStyle?.onMessageUpdate(event, ctx);
1307
1446
  if (event?.message?.role === "assistant") notePendingScrollMessage("assistant");
1308
1447
  });
1309
1448
 
@@ -1311,9 +1450,34 @@ export default function (pi: ExtensionAPI) {
1311
1450
  if (event?.message?.role === "assistant") assistantMessageActive = false;
1312
1451
  });
1313
1452
 
1314
- pi.on("session_shutdown", async () => {
1453
+ pi.on("agent_start", async (event, ctx) => {
1454
+ compactStyle?.onAgentStart(event, ctx);
1455
+ });
1456
+
1457
+ pi.on("agent_end", async (event, ctx) => {
1458
+ compactStyle?.onAgentEnd(event, ctx);
1459
+ });
1460
+
1461
+ pi.on("turn_start", async (event, ctx) => {
1462
+ compactStyle?.onTurnStart(event, ctx);
1463
+ });
1464
+
1465
+ pi.on("tool_execution_start", async (event, ctx) => {
1466
+ compactStyle?.onToolExecutionStart(event, ctx);
1467
+ });
1468
+
1469
+ pi.on("tool_execution_update", async (event, ctx) => {
1470
+ compactStyle?.onToolExecutionUpdate(event, ctx);
1471
+ });
1472
+
1473
+ pi.on("tool_execution_end", async (event, ctx) => {
1474
+ compactStyle?.onToolExecutionEnd(event, ctx);
1475
+ });
1476
+
1477
+ pi.on("session_shutdown", async (event, ctx) => {
1478
+ compactStyle?.onSessionShutdown(event, ctx);
1315
1479
  teardownToolMouseInteraction();
1316
- deactivateGlobalToolRendering();
1480
+ deactivateGlobalToolRendering(globalToolRendering);
1317
1481
  deactivateLegacyCompactionRendering();
1318
1482
  clearAllAnimations();
1319
1483
  });