pi-cc-extensions 0.8.36 → 0.8.38

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.
@@ -1126,6 +1126,13 @@ function useFixedEditorFeatures(tui: any): boolean {
1126
1126
  return toolMouseFixedFeaturesEnabled && isFixedEditorTui(tui);
1127
1127
  }
1128
1128
 
1129
+ /** Mouse hover/click affordances need ccstyle rendering AND the fixed editor. Without
1130
+ * the fixed editor, Pi's native TUI has no mouse handling, so any reporting mode leaves
1131
+ * the wheel in a dead zone — native mode must leave reporting off entirely. */
1132
+ function toolMouseInteractionActive(): boolean {
1133
+ return config.mode !== "off" && toolMouseFixedFeaturesEnabled;
1134
+ }
1135
+
1129
1136
  function formatShortcut(shortcut: string): string {
1130
1137
  return shortcut
1131
1138
  .split("+")
@@ -1249,7 +1256,12 @@ function hideScrollButton(tui: any): void {
1249
1256
  }
1250
1257
 
1251
1258
  function scheduleScrollButtonSync(tui: any, data: string): void {
1252
- if (!useFixedEditorFeatures(tui) || !isScrollNavigationInput(data) || scrollButtonSyncScheduled)
1259
+ if (
1260
+ !useFixedEditorFeatures(tui) ||
1261
+ !toolMouseInteractionActive() ||
1262
+ !isScrollNavigationInput(data) ||
1263
+ scrollButtonSyncScheduled
1264
+ )
1253
1265
  return;
1254
1266
  scrollButtonSyncScheduled = true;
1255
1267
  const previousLines = tui.previousLines;
@@ -1278,7 +1290,7 @@ function scheduleScrollButtonSync(tui: any, data: string): void {
1278
1290
  }
1279
1291
 
1280
1292
  function updateScrollButtonFromInput(tui: any, data: string): void {
1281
- if (!useFixedEditorFeatures(tui)) return;
1293
+ if (!useFixedEditorFeatures(tui) || !toolMouseInteractionActive()) return;
1282
1294
  if (matchesKey(data, "enter") || matchesKey(data, "return")) hideScrollButton(tui);
1283
1295
  }
1284
1296
 
@@ -1521,7 +1533,7 @@ function patchToolMouseInputCapture(tui: any): void {
1521
1533
  )
1522
1534
  return;
1523
1535
  const packets = parseSgrMousePackets(data);
1524
- if (packets) {
1536
+ if (packets && toolMouseInteractionActive()) {
1525
1537
  for (const packet of packets) {
1526
1538
  // Fixed-editor owners may consume motion before extension listeners run,
1527
1539
  // so hover must be handled at the root input boundary too.
@@ -1810,7 +1822,8 @@ function patchToolMouseMotionAfterRender(tui: any): void {
1810
1822
  this.previousLines = extracted.lines;
1811
1823
  placements = extracted.placements;
1812
1824
  }
1813
- if (!useFixedEditorFeatures(this)) toolMouseRawWrite?.(TOOL_MOUSE_MOTION_ENABLE);
1825
+ if (!useFixedEditorFeatures(this) && toolMouseInteractionActive())
1826
+ toolMouseRawWrite?.(TOOL_MOUSE_MOTION_ENABLE);
1814
1827
  return result;
1815
1828
  } finally {
1816
1829
  activeIoViewFrame = previousFrame;
@@ -1833,7 +1846,8 @@ function patchToolMouseMotionAfterRender(tui: any): void {
1833
1846
  toolMouseRenderPatchOriginal = original;
1834
1847
  toolMouseRenderPatchWrapper = wrapper;
1835
1848
  toolMouseRenderPatchState = patchState;
1836
- if (!useFixedEditorFeatures(tui)) toolMouseRawWrite?.(TOOL_MOUSE_MOTION_ENABLE);
1849
+ if (!useFixedEditorFeatures(tui) && toolMouseInteractionActive())
1850
+ toolMouseRawWrite?.(TOOL_MOUSE_MOTION_ENABLE);
1837
1851
  }
1838
1852
 
1839
1853
  function handleToolMouseInput(data: string): { consume: true } | undefined {
@@ -1857,6 +1871,9 @@ function handleToolMouseInput(data: string): { consume: true } | undefined {
1857
1871
  return { consume: true };
1858
1872
  }
1859
1873
  }
1874
+ // Off mode restores native input: wheel keeps scrolling through Pi's normal
1875
+ // dispatcher, while hover/click affordances are entirely inactive.
1876
+ if (!toolMouseInteractionActive()) return undefined;
1860
1877
  const packets = parseSgrMousePackets(data);
1861
1878
  if (!packets) {
1862
1879
  scheduleScrollButtonSync(toolMouseTui, data);
@@ -1892,7 +1909,7 @@ function teardownToolMouseInteraction(nextFixedEditorFeatures = false): void {
1892
1909
  setHoveredToolGroup(null);
1893
1910
  setHoveredToolIo(null, null);
1894
1911
  try {
1895
- if (!toolMouseFixedFeaturesEnabled && !nextFixedEditorFeatures) {
1912
+ if (!nextFixedEditorFeatures) {
1896
1913
  toolMouseTui?.terminal?.write?.(TOOL_MOUSE_DISABLE);
1897
1914
  }
1898
1915
  } catch {
@@ -1932,13 +1949,17 @@ export function installToolMouseInteraction(
1932
1949
  toolMouseFixedFeaturesEnabled = fixedEditorFeatures;
1933
1950
  ctx.ui.setWidget(TOOL_MOUSE_WIDGET_KEY, (tui: any, theme: any) => {
1934
1951
  toolMouseTui = tui;
1935
- if (fixedEditorFeatures) patchToolMouseInputCapture(tui);
1936
- // Fixed mode: wrap only after compositor install makes features live.
1937
- if (!fixedEditorFeatures || useFixedEditorFeatures(tui)) {
1938
- patchToolMouseMotionAfterRender(tui);
1939
- }
1940
- // The fixed-editor compositor owns its mouse mode; native Pi still needs motion enabled.
1941
- if (!fixedEditorFeatures) tui?.terminal?.write?.(TOOL_MOUSE_MOTION_ENABLE);
1952
+ // Root input capture follows the live compositor; onRebuild re-applies it
1953
+ // once the fixed-editor compositor actually owns the terminal.
1954
+ if (useFixedEditorFeatures(tui)) patchToolMouseInputCapture(tui);
1955
+ // Wrap doRender unconditionally: the fixed-editor rebuild replaces the
1956
+ // chain (setBeforeFixedEditorStart restores it first) and onRebuild
1957
+ // re-applies this wrapper once the compositor owns the terminal.
1958
+ patchToolMouseMotionAfterRender(tui);
1959
+ // The fixed-editor compositor owns its mouse mode; native Pi gets motion only
1960
+ // while mouse interaction is active (rendering mode on + fixed editor).
1961
+ if (!fixedEditorFeatures && toolMouseInteractionActive())
1962
+ tui?.terminal?.write?.(TOOL_MOUSE_MOTION_ENABLE);
1942
1963
  const widget = {
1943
1964
  render: (width: number) => renderScrollButton(width, theme),
1944
1965
  invalidate() {},
@@ -1998,6 +2019,21 @@ function applyStyleMode(
1998
2019
  ): void {
1999
2020
  config.mode = mode;
2000
2021
  saveConfig();
2022
+ if (mode === "off") {
2023
+ // Native rendering mode: drop hover/click state and fully disable mouse
2024
+ // reporting so the terminal restores its default scrollback wheel scrolling.
2025
+ // (The fixed-editor compositor owns its own reporting when enabled.)
2026
+ hoveredToolCallId = null;
2027
+ setHoveredToolGroup(null);
2028
+ setHoveredToolIo(null, null);
2029
+ scrollButtonVisible = false;
2030
+ scrollButtonHovered = false;
2031
+ pendingScrollMessages = 0;
2032
+ assistantMessageActive = false;
2033
+ if (toolMouseTui && !useFixedEditorFeatures(toolMouseTui)) {
2034
+ toolMouseTui.terminal?.write?.(TOOL_MOUSE_DISABLE);
2035
+ }
2036
+ }
2001
2037
  refreshCurrentTranscript(compactStyle, ctx, toolGrouping);
2002
2038
  ctx.ui.notify(`Claude Code style: ${mode}`, "info");
2003
2039
  }
@@ -2015,7 +2051,7 @@ function modeSettingDescription(mode: CompactStyleMode): string {
2015
2051
  function fixedEditorSettingDescription(enabled: boolean): string {
2016
2052
  return enabled
2017
2053
  ? "Pinned editor via @tifan/pi-fixed-editor. Mouse capture, 5-row wheel, tool clicks, back-to-bottom button, message count, and Ctrl+End."
2018
- : "Native scrolling editor; fixed-editor mouse features and button off. Ctrl+End remains enabled.";
2054
+ : "Native scrolling editor; mouse hover/click and wheel reporting off. Ctrl+End remains enabled.";
2019
2055
  }
2020
2056
 
2021
2057
  function excludeRenderersDescription(names: readonly string[]): string {
@@ -3135,7 +3171,12 @@ export default function (
3135
3171
  const tui = toolMouseTui;
3136
3172
  if (!tui) return;
3137
3173
  if (toolMouseFixedFeaturesEnabled) patchToolMouseInputCapture(tui);
3138
- patchToolMouseMotionAfterRender(tui);
3174
+ // compositor.install captured the current chain as originalDoRender, so an
3175
+ // already-installed wrapper keeps receiving calls through it — re-wrapping
3176
+ // only when no wrapper is active avoids stacking one per rebuild/reload.
3177
+ if (!(toolMouseRenderPatchTui === tui && toolMouseRenderPatchState?.active)) {
3178
+ patchToolMouseMotionAfterRender(tui);
3179
+ }
3139
3180
  tui.requestRender?.(true);
3140
3181
  });
3141
3182
  const writeExecutionMetadata = installWriteOverride(pi, new WriteExecutionMetadataStore());
@@ -170,7 +170,10 @@ export function installFixedEditorImePatch(compositorClass: {
170
170
  const wasInstalled = Boolean(this.installed);
171
171
  const result = Reflect.apply(originalInstall, this, args);
172
172
  // Dependents re-wrap doRender only after the compositor actually owns it.
173
- if (!wasInstalled && this.installed && !this.disposed) compositorInstallNotify?.();
173
+ // Read the global each time: the patch survives /reload on the shared
174
+ // prototype while the owner callback belongs to the live extension module.
175
+ if (!wasInstalled && this.installed && !this.disposed)
176
+ getCompositorInstallNotify()?.callback();
174
177
  return result;
175
178
  };
176
179
  prototype[MOUSE_WRITE_PATCH] = true;
@@ -211,9 +214,22 @@ type FixedEditorOwner = {
211
214
  const FIXED_EDITOR_OWNER = Symbol.for("pi.ccstyle.fixed-editor-owner");
212
215
  const FIXED_EDITOR_FOOTER_HOOK = Symbol.for("pi.ccstyle.fixed-editor-footer-hook");
213
216
 
214
- /** Active owner callback; fired from compositor.install after a fresh install succeeds. */
215
- let compositorInstallNotify: (() => void) | undefined;
216
- let compositorInstallNotifyOwner: object | undefined;
217
+ // /reload keeps the same TerminalSplitCompositor.prototype, so the install hook
218
+ // patched by the pre-reload module must read a cross-reload global instead of a
219
+ // module-local variable; otherwise notifyRebuild never fires after reload and
220
+ // dependents (ccstyle mouse input capture) stay unbound.
221
+ const COMPOSITOR_INSTALL_NOTIFY_KEY = Symbol.for("pi.ccstyle.compositor-install-notify");
222
+
223
+ type CompositorInstallNotify = { owner: object; callback: () => void };
224
+ function getCompositorInstallNotify(): CompositorInstallNotify | undefined {
225
+ return (globalThis as Record<PropertyKey, unknown>)[COMPOSITOR_INSTALL_NOTIFY_KEY] as
226
+ | CompositorInstallNotify
227
+ | undefined;
228
+ }
229
+ function setCompositorInstallNotify(value: CompositorInstallNotify | undefined): void {
230
+ if (value) (globalThis as Record<PropertyKey, unknown>)[COMPOSITOR_INSTALL_NOTIFY_KEY] = value;
231
+ else delete (globalThis as Record<PropertyKey, unknown>)[COMPOSITOR_INSTALL_NOTIFY_KEY];
232
+ }
217
233
  /** Runs before fixed-editor start/probe so dependents can unwrap doRender before capture. */
218
234
  let beforeFixedEditorStart: (() => void) | undefined;
219
235
 
@@ -289,10 +305,7 @@ export function installFixedEditor(
289
305
  });
290
306
  };
291
307
  function clearInstallNotify() {
292
- if (compositorInstallNotifyOwner === owner) {
293
- compositorInstallNotify = undefined;
294
- compositorInstallNotifyOwner = undefined;
295
- }
308
+ if (getCompositorInstallNotify()?.owner === owner) setCompositorInstallNotify(undefined);
296
309
  }
297
310
  function deactivate() {
298
311
  fixedEditorState.scrollButtonHitbox = null;
@@ -311,12 +324,14 @@ export function installFixedEditor(
311
324
  if (previous?.owner !== owner) previous?.stop();
312
325
  const currentSession = session;
313
326
  // Register before start/probe so the first real compositor.install notifies us.
314
- compositorInstallNotify = () => {
315
- if (!active) return;
316
- if ((globalThis as any)[FIXED_EDITOR_OWNER]?.owner !== owner) return;
317
- notifyRebuild();
318
- };
319
- compositorInstallNotifyOwner = owner;
327
+ setCompositorInstallNotify({
328
+ owner,
329
+ callback: () => {
330
+ if (!active) return;
331
+ if ((globalThis as any)[FIXED_EDITOR_OWNER]?.owner !== owner) return;
332
+ notifyRebuild();
333
+ },
334
+ });
320
335
  // Unwrap outer doRender patches before compositor construction captures it.
321
336
  beforeFixedEditorStart?.();
322
337
  start?.(currentSession.event, currentSession.ctx);
@@ -20,11 +20,12 @@ function formatCount(value: number): string {
20
20
 
21
21
  function estimateTextLength(message: any): number {
22
22
  if (!Array.isArray(message?.content)) return 0;
23
- return message.content.reduce(
24
- (sum: number, block: any) =>
25
- sum + (block?.type === "text" && typeof block.text === "string" ? block.text.length : 0),
26
- 0,
27
- );
23
+ return message.content.reduce((sum: number, block: any) => {
24
+ if (block?.type === "text" && typeof block.text === "string") return sum + block.text.length;
25
+ if (block?.type === "thinking" && block.thinkingSignature?.body)
26
+ return sum + (block.thinkingSignature.body as string).length;
27
+ return sum;
28
+ }, 0);
28
29
  }
29
30
 
30
31
  function textBlockLengths(message: any): number[] {
@@ -34,6 +35,8 @@ function textBlockLengths(message: any): number[] {
34
35
  const block = message.content[index];
35
36
  if (block?.type === "text" && typeof block.text === "string") {
36
37
  lengths[index] = block.text.length;
38
+ } else if (block?.type === "thinking" && block.thinkingSignature?.body) {
39
+ lengths[index] = (block.thinkingSignature.body as string).length;
37
40
  }
38
41
  }
39
42
  return lengths;
@@ -122,8 +125,11 @@ export default function (pi: ExtensionAPI): void {
122
125
  if (!turnActive || refreshTimer) return;
123
126
  refreshTimer = setTimeout(() => {
124
127
  refreshTimer = null;
125
- syncWorkingMessage();
126
- scheduleRefreshTick();
128
+ try {
129
+ syncWorkingMessage();
130
+ } finally {
131
+ scheduleRefreshTick();
132
+ }
127
133
  }, REFRESH_INTERVAL_MS);
128
134
  refreshTimer.unref?.();
129
135
  }
@@ -163,15 +169,12 @@ export default function (pi: ExtensionAPI): void {
163
169
 
164
170
  if (evt.type === "start") {
165
171
  resetResponseTracking(evt.partial);
166
- } else if (evt.type === "text_start") {
172
+ } else if (evt.type === "thinking_start" || evt.type === "text_start") {
167
173
  setTextBlockLength(evt.contentIndex, 0);
168
174
  updateProviderUsage(evt.partial);
169
- } else if (evt.type === "text_delta") {
170
- setTextBlockLength(
171
- evt.contentIndex,
172
- (responseTextBlockLengths[evt.contentIndex] ?? 0) +
173
- (typeof evt.delta === "string" ? evt.delta.length : 0),
174
- );
175
+ } else if (evt.type === "thinking_delta" || evt.type === "text_delta") {
176
+ const add = typeof evt.delta === "string" ? evt.delta.length : 0;
177
+ setTextBlockLength(evt.contentIndex, (responseTextBlockLengths[evt.contentIndex] ?? 0) + add);
175
178
  updateProviderUsage(evt.partial);
176
179
  } else if (evt.type === "text_end") {
177
180
  setTextBlockLength(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.8.36",
3
+ "version": "0.8.38",
4
4
  "description": "A Pi productivity suite with Claude Code-style UI, fixed editor, context inspection, and agent/session references",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -40,7 +40,7 @@
40
40
 
41
41
  "selectedBg": "#13233a",
42
42
  "userMessageBg": "surface",
43
- "userMessageText": "text",
43
+ "userMessageText": "",
44
44
  "customMessageBg": "surface",
45
45
  "customMessageText": "text",
46
46
  "customMessageLabel": "accent",