pi-cc-extensions 0.7.10 → 0.8.0

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.
@@ -4,53 +4,72 @@ import {
4
4
  getSettingsListTheme,
5
5
  ToolExecutionComponent,
6
6
  } from "@earendil-works/pi-coding-agent";
7
+ import {
8
+ installCompactStyle,
9
+ type CompactStyleHooks,
10
+ type CompactStyleMode,
11
+ } from "./compact-style.ts";
12
+ import { sanitizeToolResultText } from "./tool-result-sanitize.ts";
7
13
  import { Container, SettingsList, 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 ?? "") : "";
@@ -938,13 +939,20 @@ 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;
@@ -955,17 +963,17 @@ async function showCcstylePanel(ctx: any): Promise<void> {
955
963
  container.addChild(new Text(theme.fg("accent", theme.bold("Claude Code Style")), 1, 1));
956
964
  const settingsList = new SettingsList(
957
965
  [{
958
- id: "enabled",
966
+ id: "mode",
959
967
  label: "Output style",
960
- description: "Use compact Claude Code-like tool rendering",
961
- currentValue: config.enabled ? "on" : "off",
962
- values: ["on", "off"],
968
+ description: "Claude Code, Pi native, or compact transcript rendering",
969
+ currentValue: config.mode,
970
+ values: ["on", "off", "compact"],
963
971
  }],
964
972
  3,
965
973
  getSettingsListTheme(),
966
974
  (id: string, newValue: string) => {
967
- if (id !== "enabled" || (newValue !== "on" && newValue !== "off")) return;
968
- applyEnabledState(newValue === "on", ctx);
975
+ if (id !== "mode" || (newValue !== "on" && newValue !== "off" && newValue !== "compact")) return;
976
+ applyStyleMode(newValue, ctx, compactStyle);
969
977
  settingsList.updateValue(id, newValue);
970
978
  tui.requestRender();
971
979
  },
@@ -1027,7 +1035,7 @@ function createCcstyleTool(originalTool: any): any {
1027
1035
  ...originalTool,
1028
1036
  renderShell: "self",
1029
1037
  renderCall(args: any, theme: any, context: any) {
1030
- if (!config.enabled) {
1038
+ if (config.mode !== "on") {
1031
1039
  return renderDefault(originalTool, "renderCall", [args, theme, context], String(toolName));
1032
1040
  }
1033
1041
 
@@ -1046,7 +1054,7 @@ function createCcstyleTool(originalTool: any): any {
1046
1054
  return new Text(title, 0, 0);
1047
1055
  },
1048
1056
  renderResult(result: any, options: any, theme: any, context: any) {
1049
- if (!config.enabled) {
1057
+ if (config.mode !== "on") {
1050
1058
  return renderDefault(originalTool, "renderResult", [result, options, theme, context], textFromResult(result));
1051
1059
  }
1052
1060
 
@@ -1082,19 +1090,31 @@ const GLOBAL_TOOL_RENDER_PATCH = Symbol.for("pi.ccstyle.global-tool-render-patch
1082
1090
  const COMPONENT_TOOL_RENDER_MODE = Symbol.for("pi.ccstyle.component-tool-render-mode");
1083
1091
  const COMPONENT_TOOL_SELF_SHELL_MODE = Symbol.for("pi.ccstyle.component-tool-self-shell-mode");
1084
1092
  const SUBAGENT_TOOL_NAMES = new Set(["Agent"]);
1085
- const globalToolRenderOwner = {};
1093
+
1094
+ type ToolRenderMethods = {
1095
+ hasRendererDefinition: (...args: any[]) => boolean;
1096
+ getRenderShell: (...args: any[]) => "default" | "self";
1097
+ getCallRenderer: (...args: any[]) => any;
1098
+ getResultRenderer: (...args: any[]) => any;
1099
+ };
1086
1100
 
1087
1101
  type GlobalToolRenderPatch = {
1102
+ version: 2;
1088
1103
  prototype: any;
1089
1104
  owner: object;
1105
+ active: boolean;
1090
1106
  enabled: () => boolean;
1107
+ mode: () => CompactStyleMode;
1091
1108
  wrap: (tool: any) => any;
1092
1109
  byDefinition: WeakMap<object, any>;
1093
1110
  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;
1111
+ downstream: ToolRenderMethods;
1112
+ installed: ToolRenderMethods;
1113
+ // Keep the legacy aliases so an older extension build can read this Symbol.
1114
+ originalHasRendererDefinition: ToolRenderMethods["hasRendererDefinition"];
1115
+ originalGetRenderShell: ToolRenderMethods["getRenderShell"];
1116
+ originalGetCallRenderer: ToolRenderMethods["getCallRenderer"];
1117
+ originalGetResultRenderer: ToolRenderMethods["getResultRenderer"];
1098
1118
  };
1099
1119
 
1100
1120
  function isMcpToolDefinition(definition: any, toolName: string): boolean {
@@ -1118,33 +1138,43 @@ export function preservesOriginalRenderer(
1118
1138
  );
1119
1139
  }
1120
1140
 
1121
- function shouldGloballyStyleTool(component: any, patch: GlobalToolRenderPatch): boolean {
1122
- const selectedMode = component[COMPONENT_TOOL_RENDER_MODE];
1123
- if (typeof selectedMode === "boolean") return selectedMode;
1141
+ function syncToolShell(component: any, shell: "default" | "self"): void {
1142
+ const target = shell === "self" ? component.selfRenderContainer : component.contentBox;
1143
+ if (!target || !Array.isArray(component.children)) return;
1144
+ const candidates = new Set([component.contentText, component.contentBox, component.selfRenderContainer].filter(Boolean));
1145
+ const indexes = component.children
1146
+ .map((child: any, index: number) => candidates.has(child) ? index : -1)
1147
+ .filter((index: number) => index >= 0);
1148
+ const targetIndex = indexes[0];
1149
+ // During construction getRenderShell() runs immediately before Pi mounts the
1150
+ // selected shell. Do not mount it here or the constructor will add it twice.
1151
+ if (targetIndex === undefined) return;
1152
+ component.children[targetIndex] = target;
1153
+ for (const index of indexes.sort((left: number, right: number) => right - left)) {
1154
+ if (index !== targetIndex) component.children.splice(index, 1);
1155
+ }
1156
+ }
1124
1157
 
1158
+ function shouldGloballyStyleTool(component: any, patch: GlobalToolRenderPatch): boolean {
1125
1159
  const extensionDefinition = component.toolDefinition;
1126
1160
  const builtInDefinition = component.builtInToolDefinition;
1127
1161
  const definition = extensionDefinition ?? builtInDefinition;
1128
1162
  const toolName = String(component.toolName || definition?.name || "");
1129
1163
  const useCcstyle =
1130
- patch.enabled() &&
1164
+ patch.mode() === "on" &&
1131
1165
  !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
1166
  component[COMPONENT_TOOL_RENDER_MODE] = useCcstyle;
1135
1167
  return useCcstyle;
1136
1168
  }
1137
1169
 
1138
1170
  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
1171
  const definition = component.toolDefinition ?? component.builtInToolDefinition;
1143
1172
  const toolName = String(component.toolName || definition?.name || "");
1144
1173
  const useSelfShell =
1145
1174
  patch.enabled() &&
1146
1175
  SUBAGENT_TOOL_NAMES.has(toolName) &&
1147
- definition?.renderShell !== "self";
1176
+ definition != null &&
1177
+ definition.renderShell === undefined;
1148
1178
  component[COMPONENT_TOOL_SELF_SHELL_MODE] = useSelfShell;
1149
1179
  return useSelfShell;
1150
1180
  }
@@ -1169,61 +1199,171 @@ function getGloballyStyledTool(component: any, patch: GlobalToolRenderPatch): an
1169
1199
  return wrapped;
1170
1200
  }
1171
1201
 
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
- };
1202
+ function prototypeToolRenderMethods(prototype: any): ToolRenderMethods {
1203
+ return {
1204
+ hasRendererDefinition: prototype.hasRendererDefinition,
1205
+ getRenderShell: prototype.getRenderShell,
1206
+ getCallRenderer: prototype.getCallRenderer,
1207
+ getResultRenderer: prototype.getResultRenderer,
1208
+ };
1209
+ }
1190
1210
 
1191
- host[GLOBAL_TOOL_RENDER_PATCH] = patch;
1211
+ function isOwnershipAwarePatch(value: any): value is GlobalToolRenderPatch {
1212
+ if (!value || value.version !== 2 || !value.installed || !value.downstream) return false;
1213
+ return ["hasRendererDefinition", "getRenderShell", "getCallRenderer", "getResultRenderer"]
1214
+ .every((name) => typeof value.installed[name] === "function" && typeof value.downstream[name] === "function");
1215
+ }
1216
+
1217
+ function isLegacyInstalledWrapper(method: unknown, downstreamField: string): boolean {
1218
+ if (typeof method !== "function") return false;
1219
+ try {
1220
+ const source = Function.prototype.toString.call(method);
1221
+ return source.includes(downstreamField)
1222
+ && (
1223
+ source.includes("shouldGloballyStyleTool")
1224
+ || source.includes("shouldUseSelfShell")
1225
+ || source.includes("getGloballyStyledTool")
1226
+ );
1227
+ } catch {
1228
+ return false;
1192
1229
  }
1230
+ }
1193
1231
 
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);
1232
+ function downstreamForGlobalToolInstall(prototype: any, previous: any): ToolRenderMethods {
1233
+ const current = prototypeToolRenderMethods(prototype);
1234
+ if (!previous || previous.prototype !== prototype) return current;
1235
+ if (isOwnershipAwarePatch(previous)) {
1236
+ return {
1237
+ hasRendererDefinition: current.hasRendererDefinition === previous.installed.hasRendererDefinition
1238
+ ? previous.downstream.hasRendererDefinition
1239
+ : current.hasRendererDefinition,
1240
+ getRenderShell: current.getRenderShell === previous.installed.getRenderShell
1241
+ ? previous.downstream.getRenderShell
1242
+ : current.getRenderShell,
1243
+ getCallRenderer: current.getCallRenderer === previous.installed.getCallRenderer
1244
+ ? previous.downstream.getCallRenderer
1245
+ : current.getCallRenderer,
1246
+ getResultRenderer: current.getResultRenderer === previous.installed.getResultRenderer
1247
+ ? previous.downstream.getResultRenderer
1248
+ : current.getResultRenderer,
1249
+ };
1250
+ }
1251
+
1252
+ // Pre-v2 Symbol state did not retain wrapper references. Recognize its known
1253
+ // wrappers when possible; otherwise preserve the current method as external.
1254
+ const legacyDownstream = (method: Function, field: string): Function => {
1255
+ const saved = previous[field];
1256
+ return typeof saved === "function" && isLegacyInstalledWrapper(method, field) ? saved : method;
1214
1257
  };
1215
- prototype.getResultRenderer = function (...args: any[]) {
1216
- if (shouldGloballyStyleTool(this, patch!)) return getGloballyStyledTool(this, patch!).renderResult;
1217
- return patch!.originalGetResultRenderer.apply(this, args);
1258
+ return {
1259
+ hasRendererDefinition: legacyDownstream(
1260
+ current.hasRendererDefinition,
1261
+ "originalHasRendererDefinition",
1262
+ ) as ToolRenderMethods["hasRendererDefinition"],
1263
+ getRenderShell: legacyDownstream(
1264
+ current.getRenderShell,
1265
+ "originalGetRenderShell",
1266
+ ) as ToolRenderMethods["getRenderShell"],
1267
+ getCallRenderer: legacyDownstream(
1268
+ current.getCallRenderer,
1269
+ "originalGetCallRenderer",
1270
+ ) as ToolRenderMethods["getCallRenderer"],
1271
+ getResultRenderer: legacyDownstream(
1272
+ current.getResultRenderer,
1273
+ "originalGetResultRenderer",
1274
+ ) as ToolRenderMethods["getResultRenderer"],
1218
1275
  };
1219
1276
  }
1220
1277
 
1221
- function deactivateGlobalToolRendering() {
1222
- const patch = (globalThis as any)[GLOBAL_TOOL_RENDER_PATCH] as GlobalToolRenderPatch | undefined;
1223
- if (patch?.owner !== globalToolRenderOwner) return;
1278
+ function disconnectGlobalToolRenderPatch(patch: any): void {
1279
+ if (!patch || typeof patch !== "object") return;
1280
+ patch.active = false;
1224
1281
  patch.enabled = () => false;
1282
+ patch.mode = () => "off";
1283
+ patch.wrap = (tool: any) => tool;
1225
1284
  patch.byDefinition = new WeakMap();
1226
- patch.byName.clear();
1285
+ if (patch.byName && typeof patch.byName.clear === "function") patch.byName.clear();
1286
+ else patch.byName = new Map();
1287
+ }
1288
+
1289
+ function installGlobalToolRendering(): GlobalToolRenderPatch {
1290
+ const prototype = (ToolExecutionComponent as any).prototype;
1291
+ const host = globalThis as any;
1292
+ const previous = host[GLOBAL_TOOL_RENDER_PATCH];
1293
+ const downstream = downstreamForGlobalToolInstall(prototype, previous);
1294
+ // Any wrapper retained by an external owner must become a callback-free
1295
+ // pass-through before the new installation is linked above it.
1296
+ disconnectGlobalToolRenderPatch(previous);
1297
+
1298
+ const patch: GlobalToolRenderPatch = {
1299
+ version: 2,
1300
+ prototype,
1301
+ owner: {},
1302
+ active: true,
1303
+ enabled: () => config.mode === "on",
1304
+ mode: () => config.mode,
1305
+ wrap: createCcstyleTool,
1306
+ byDefinition: new WeakMap(),
1307
+ byName: new Map(),
1308
+ downstream,
1309
+ installed: undefined as any,
1310
+ originalHasRendererDefinition: downstream.hasRendererDefinition,
1311
+ originalGetRenderShell: downstream.getRenderShell,
1312
+ originalGetCallRenderer: downstream.getCallRenderer,
1313
+ originalGetResultRenderer: downstream.getResultRenderer,
1314
+ };
1315
+
1316
+ patch.installed = {
1317
+ hasRendererDefinition: function (this: any, ...args: any[]) {
1318
+ if (patch.active && shouldGloballyStyleTool(this, patch)) return true;
1319
+ return patch.downstream.hasRendererDefinition.apply(this, args);
1320
+ },
1321
+ getRenderShell: function (this: any, ...args: any[]) {
1322
+ if (!patch.active) return patch.downstream.getRenderShell.apply(this, args);
1323
+ const shell = shouldUseSelfShell(this, patch) || shouldGloballyStyleTool(this, patch)
1324
+ ? "self"
1325
+ : patch.downstream.getRenderShell.apply(this, args);
1326
+ syncToolShell(this, shell);
1327
+ return shell;
1328
+ },
1329
+ getCallRenderer: function (this: any, ...args: any[]) {
1330
+ if (patch.active && shouldGloballyStyleTool(this, patch)) {
1331
+ return getGloballyStyledTool(this, patch).renderCall;
1332
+ }
1333
+ return patch.downstream.getCallRenderer.apply(this, args);
1334
+ },
1335
+ getResultRenderer: function (this: any, ...args: any[]) {
1336
+ if (patch.active && shouldGloballyStyleTool(this, patch)) {
1337
+ return getGloballyStyledTool(this, patch).renderResult;
1338
+ }
1339
+ return patch.downstream.getResultRenderer.apply(this, args);
1340
+ },
1341
+ };
1342
+
1343
+ prototype.hasRendererDefinition = patch.installed.hasRendererDefinition;
1344
+ prototype.getRenderShell = patch.installed.getRenderShell;
1345
+ prototype.getCallRenderer = patch.installed.getCallRenderer;
1346
+ prototype.getResultRenderer = patch.installed.getResultRenderer;
1347
+ host[GLOBAL_TOOL_RENDER_PATCH] = patch;
1348
+ return patch;
1349
+ }
1350
+
1351
+ function deactivateGlobalToolRendering(patch: GlobalToolRenderPatch): void {
1352
+ if (!patch.active) return;
1353
+ disconnectGlobalToolRenderPatch(patch);
1354
+ const prototype = patch.prototype;
1355
+ if (prototype.hasRendererDefinition === patch.installed.hasRendererDefinition) {
1356
+ prototype.hasRendererDefinition = patch.downstream.hasRendererDefinition;
1357
+ }
1358
+ if (prototype.getRenderShell === patch.installed.getRenderShell) {
1359
+ prototype.getRenderShell = patch.downstream.getRenderShell;
1360
+ }
1361
+ if (prototype.getCallRenderer === patch.installed.getCallRenderer) {
1362
+ prototype.getCallRenderer = patch.downstream.getCallRenderer;
1363
+ }
1364
+ if (prototype.getResultRenderer === patch.installed.getResultRenderer) {
1365
+ prototype.getResultRenderer = patch.downstream.getResultRenderer;
1366
+ }
1227
1367
  }
1228
1368
 
1229
1369
  const GLOBAL_COMPACTION_RENDER_PATCH = Symbol.for("pi.ccstyle.compaction-render-patch");
@@ -1251,15 +1391,20 @@ function notePendingScrollMessage(role: unknown): void {
1251
1391
  }
1252
1392
 
1253
1393
  export default function (pi: ExtensionAPI) {
1254
- installGlobalToolRendering();
1394
+ const globalToolRendering = installGlobalToolRendering();
1255
1395
  deactivateLegacyCompactionRendering();
1396
+ const compactStyle: CompactStyleHooks = installCompactStyle(pi, {
1397
+ getMode: () => config.mode,
1398
+ getExcludeRenderers: () => config.excludeRenderers,
1399
+ });
1256
1400
 
1257
1401
  pi.registerCommand("ccstyle", {
1258
1402
  description: "Configure Claude Code style",
1259
1403
  getArgumentCompletions: (prefix) => {
1260
1404
  const topLevel = [
1261
1405
  { value: "on", label: "on", description: "Enable Claude Code style" },
1262
- { value: "off", label: "off", description: "Disable Claude Code style" },
1406
+ { value: "off", label: "off", description: "Use Pi's native renderer" },
1407
+ { value: "compact", label: "compact", description: "Use compact transcript rendering" },
1263
1408
  { value: "status", label: "status", description: "Show current state" },
1264
1409
  ];
1265
1410
  return topLevel.filter((item) => item.value.startsWith(prefix));
@@ -1267,31 +1412,23 @@ export default function (pi: ExtensionAPI) {
1267
1412
  handler: async (args, ctx) => {
1268
1413
  const arg = args.trim().toLowerCase();
1269
1414
  if (!arg) {
1270
- await showCcstylePanel(ctx);
1415
+ await showCcstylePanel(ctx, compactStyle);
1271
1416
  return;
1272
1417
  }
1273
- if (arg === "on" || arg === "off") {
1274
- applyEnabledState(arg === "on", ctx);
1418
+ if (arg === "on" || arg === "off" || arg === "compact") {
1419
+ applyStyleMode(arg, ctx, compactStyle);
1275
1420
  return;
1276
1421
  }
1277
1422
  if (arg === "status") {
1278
- ctx.ui.notify(`Claude Code style: ${config.enabled ? "on" : "off"}`, "info");
1423
+ ctx.ui.notify(`Claude Code style: ${config.mode}`, "info");
1279
1424
  return;
1280
1425
  }
1281
- ctx.ui.notify("Usage: /ccstyle [on|off|status]", "warning");
1282
- },
1283
- });
1284
-
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");
1426
+ ctx.ui.notify("Usage: /ccstyle [on|off|compact|status]", "warning");
1291
1427
  },
1292
1428
  });
1293
1429
 
1294
- pi.on("session_start", async (_event, ctx) => {
1430
+ pi.on("session_start", async (event, ctx) => {
1431
+ compactStyle?.onSessionStart(event, ctx);
1295
1432
  pendingScrollMessages = 0;
1296
1433
  assistantMessageActive = false;
1297
1434
  ctx.ui.setStatus("ccstyle", undefined);
@@ -1303,7 +1440,8 @@ export default function (pi: ExtensionAPI) {
1303
1440
  notePendingScrollMessage(event?.message?.role);
1304
1441
  });
1305
1442
 
1306
- pi.on("message_update", async (event) => {
1443
+ pi.on("message_update", async (event, ctx) => {
1444
+ compactStyle?.onMessageUpdate(event, ctx);
1307
1445
  if (event?.message?.role === "assistant") notePendingScrollMessage("assistant");
1308
1446
  });
1309
1447
 
@@ -1311,9 +1449,34 @@ export default function (pi: ExtensionAPI) {
1311
1449
  if (event?.message?.role === "assistant") assistantMessageActive = false;
1312
1450
  });
1313
1451
 
1314
- pi.on("session_shutdown", async () => {
1452
+ pi.on("agent_start", async (event, ctx) => {
1453
+ compactStyle?.onAgentStart(event, ctx);
1454
+ });
1455
+
1456
+ pi.on("agent_end", async (event, ctx) => {
1457
+ compactStyle?.onAgentEnd(event, ctx);
1458
+ });
1459
+
1460
+ pi.on("turn_start", async (event, ctx) => {
1461
+ compactStyle?.onTurnStart(event, ctx);
1462
+ });
1463
+
1464
+ pi.on("tool_execution_start", async (event, ctx) => {
1465
+ compactStyle?.onToolExecutionStart(event, ctx);
1466
+ });
1467
+
1468
+ pi.on("tool_execution_update", async (event, ctx) => {
1469
+ compactStyle?.onToolExecutionUpdate(event, ctx);
1470
+ });
1471
+
1472
+ pi.on("tool_execution_end", async (event, ctx) => {
1473
+ compactStyle?.onToolExecutionEnd(event, ctx);
1474
+ });
1475
+
1476
+ pi.on("session_shutdown", async (event, ctx) => {
1477
+ compactStyle?.onSessionShutdown(event, ctx);
1315
1478
  teardownToolMouseInteraction();
1316
- deactivateGlobalToolRendering();
1479
+ deactivateGlobalToolRendering(globalToolRendering);
1317
1480
  deactivateLegacyCompactionRendering();
1318
1481
  clearAllAnimations();
1319
1482
  });