pi-cc-extensions 0.8.35 → 0.8.36

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.
@@ -23,37 +23,24 @@ type CompactThinkingOwner = {
23
23
  stop(event?: any, ctx?: any): void;
24
24
  };
25
25
 
26
- const COMPACT_THINKING_OWNER = Symbol.for("pi.ccstyle.compact-thinking-owner");
26
+ type UpstreamHandler = (event: any, ctx: any) => void;
27
27
 
28
- export function installCompactThinking(
29
- pi: ExtensionAPI,
30
- initialConfig: CompactThinkingConfig,
31
- ): CompactThinkingController {
32
- const owner = {};
33
- const host = globalThis as typeof globalThis & {
34
- [COMPACT_THINKING_OWNER]?: CompactThinkingOwner;
35
- };
36
- host[COMPACT_THINKING_OWNER]?.stop();
37
-
38
- const agentDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
39
- // Upstream requires this file while loading; mirror ccstyle into it and never read it back.
40
- mkdirSync(agentDir, { recursive: true });
41
- writeFileSync(
42
- join(agentDir, "compact-thinking.json"),
43
- `${JSON.stringify(initialConfig, null, 2)}\n`,
44
- "utf8",
45
- );
28
+ const COMPACT_THINKING_OWNER = Symbol.for("pi.ccstyle.compact-thinking-owner");
46
29
 
30
+ function loadPatchedUpstream(): {
31
+ config: CompactThinkingConfig;
32
+ compactThinking: (api: ExtensionAPI) => void;
33
+ } {
47
34
  // Fork the upstream entry so a running subagent tool keeps the thinking
48
35
  // loading animation until the model emits the next text/thinking boundary.
49
- // Upstream finalizes on tool_execution_start, freezing the summary into a
50
- // static "Thought for Xs" for the whole subagent run.
36
+ // Upstream finalizes on tool_execution_start / message_end, freezing the
37
+ // summary into a static "Thought for Xs" for the whole subagent run.
51
38
  const upstreamRequire = createRequire(import.meta.url);
52
39
  const upstreamIndexPath = upstreamRequire.resolve("pi-compact-thinking/index.ts");
53
40
  const toolStartOriginal = ` pi.on("tool_execution_start", finishThinking);`;
54
41
  const toolStartPatched = ` // Subagent tools can run for minutes: keep the thinking loading animation
55
- // alive for the whole execution and only finalize once the model emits the
56
- // next text/thinking boundary or the message ends.
42
+ // alive for the whole execution and only finalize once the tool ends or the
43
+ // model emits the next text/thinking boundary.
57
44
  function resumeAgentThinking(message: AssistantMessage | undefined) {
58
45
  if (activeThinking) return;
59
46
  const content = message?.content;
@@ -62,12 +49,27 @@ export function installCompactThinking(
62
49
  if (index < 0) return;
63
50
  startThinking(message, index);
64
51
  }
52
+ function messageHasAgentTool(message: AssistantMessage | undefined) {
53
+ return (
54
+ Array.isArray(message?.content) &&
55
+ message.content.some(
56
+ (content) =>
57
+ content?.type === "toolCall" &&
58
+ (content.name === "Agent" ||
59
+ content.name === "Agents" ||
60
+ content.args?.subagent_type != null),
61
+ )
62
+ );
63
+ }
65
64
  pi.on("tool_execution_start", (event: any) => {
66
65
  if (event.toolName === "Agent" || event.toolName === "Agents") {
67
66
  resumeAgentThinking(latestComponent?.lastMessage ?? undefined);
68
67
  } else {
69
68
  finishThinking();
70
69
  }
70
+ });
71
+ pi.on("tool_execution_end", (event: any) => {
72
+ if (event.toolName === "Agent" || event.toolName === "Agents") finishThinking();
71
73
  });`;
72
74
  const updateBoundaryOriginal = ` } else if (
73
75
  update.type === "text_start" ||
@@ -82,33 +84,33 @@ export function installCompactThinking(
82
84
  update.type === "toolcall_start" ||
83
85
  update.type === "toolcall_delta"
84
86
  ) {
85
- if (
86
- Array.isArray(event.message?.content) &&
87
- event.message.content.some(
88
- (content) =>
89
- content?.type === "toolCall" &&
90
- (content.name === "Agent" ||
91
- content.name === "Agents" ||
92
- content.args?.subagent_type != null),
93
- )
94
- ) {
87
+ if (messageHasAgentTool(event.message)) {
95
88
  resumeAgentThinking(event.message);
96
89
  } else {
97
90
  finishThinking();
98
91
  }
99
92
  }`;
93
+ const messageEndOriginal = ` pi.on("message_end", (event) => {
94
+ if (event.message.role === "assistant") finishThinking();
95
+ });`;
96
+ const messageEndPatched = ` pi.on("message_end", (event) => {
97
+ if (event.message.role !== "assistant") return;
98
+ // Agent tool runs after message_end; keep the ticker until tool_execution_end
99
+ // or the next text/thinking boundary.
100
+ if (messageHasAgentTool(event.message)) return;
101
+ finishThinking();
102
+ });`;
103
+
100
104
  let upstreamIndexSource = readFileSync(upstreamIndexPath, "utf8");
101
105
  if (
102
106
  upstreamIndexSource.includes(toolStartOriginal) &&
103
- upstreamIndexSource.includes(updateBoundaryOriginal)
107
+ upstreamIndexSource.includes(updateBoundaryOriginal) &&
108
+ upstreamIndexSource.includes(messageEndOriginal)
104
109
  ) {
105
110
  upstreamIndexSource = upstreamIndexSource
106
111
  .replace(toolStartOriginal, toolStartPatched)
107
- .replace(updateBoundaryOriginal, updateBoundaryPatched);
108
- } else {
109
- // Upstream changed shape: fall back to the untouched entry so the loader
110
- // keeps working; only the subagent animation nicety is lost.
111
- upstreamIndexSource = readFileSync(upstreamIndexPath, "utf8");
112
+ .replace(updateBoundaryOriginal, updateBoundaryPatched)
113
+ .replace(messageEndOriginal, messageEndPatched);
112
114
  }
113
115
 
114
116
  // Reuse Pi's live modules so the upstream prototype patch reaches runtime components.
@@ -127,12 +129,34 @@ export function installCompactThinking(
127
129
  default: (api: ExtensionAPI) => void;
128
130
  }
129
131
  ).default;
130
- Object.assign(upstreamConfig, initialConfig);
132
+ return { config: upstreamConfig, compactThinking };
133
+ }
134
+
135
+ export function installCompactThinking(
136
+ pi: ExtensionAPI,
137
+ initialConfig: CompactThinkingConfig,
138
+ ): CompactThinkingController {
139
+ const owner = {};
140
+ const host = globalThis as typeof globalThis & {
141
+ [COMPACT_THINKING_OWNER]?: CompactThinkingOwner;
142
+ };
143
+
144
+ const agentDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
145
+ // Upstream requires this file while loading; mirror ccstyle into it and never read it back.
146
+ mkdirSync(agentDir, { recursive: true });
147
+ writeFileSync(
148
+ join(agentDir, "compact-thinking.json"),
149
+ `${JSON.stringify(initialConfig, null, 2)}\n`,
150
+ "utf8",
151
+ );
152
+
153
+ let upstreamConfig: CompactThinkingConfig | undefined;
154
+ let session: { event: any; ctx: any } | undefined;
155
+ let active = false;
156
+ // Stable pi.on wrappers delegate here so activate/reload never double-binds.
157
+ const delegates = new Map<string, UpstreamHandler>();
158
+ const boundEvents = new Set<string>();
131
159
 
132
- // Upstream restores completedDurations from sessionManager.getBranch() (the
133
- // current leaf path). After compaction or a branch switch, duration entries
134
- // of older messages leave that path, so scrolling back renders a bare
135
- // "Thinking..." instead of "Thought for Xs". Restore from every entry.
136
160
  const restoreAllDurations = (ctx: any): any => {
137
161
  const sessionManager = ctx?.sessionManager;
138
162
  if (!sessionManager || typeof sessionManager.getEntries !== "function") return ctx;
@@ -145,40 +169,73 @@ export function installCompactThinking(
145
169
  };
146
170
  };
147
171
 
148
- let session: { event: any; ctx: any } | undefined;
149
- let shutdown: ((event: any, ctx: any) => void) | undefined;
150
- let stopped = false;
172
+ const bind = (eventName: string) => {
173
+ if (boundEvents.has(eventName)) return;
174
+ boundEvents.add(eventName);
175
+ pi.on(eventName as any, (e: any, ctx: any) => {
176
+ if (!active) return;
177
+ const handler = delegates.get(eventName);
178
+ if (!handler) return;
179
+ handler(e, eventName === "session_tree" ? restoreAllDurations(ctx) : ctx);
180
+ });
181
+ };
182
+
151
183
  const stop = (event?: any, ctx?: any) => {
152
- if (stopped) return;
153
- stopped = true;
184
+ if (!active) return;
185
+ active = false;
186
+ const shutdown = delegates.get("session_shutdown");
187
+ delegates.clear();
154
188
  shutdown?.(event ?? session?.event ?? {}, ctx ?? session?.ctx ?? { mode: "rpc", ui: {} });
155
189
  if (host[COMPACT_THINKING_OWNER]?.owner === owner) delete host[COMPACT_THINKING_OWNER];
156
190
  };
157
- compactThinking({
158
- on(event: string, handler: (event: any, ctx: any) => void) {
159
- if (event === "session_start") {
160
- pi.on(event, (startEvent, ctx) => {
161
- session = { event: startEvent, ctx };
162
- handler(startEvent, restoreAllDurations(ctx));
163
- });
164
- } else if (event === "session_shutdown") {
165
- shutdown = handler;
166
- pi.on(event, (shutdownEvent, ctx) => {
167
- if (host[COMPACT_THINKING_OWNER]?.owner === owner) stop(shutdownEvent, ctx);
168
- });
169
- } else {
170
- pi.on(event as any, (e: any, ctx: any) =>
171
- handler(e, event === "session_tree" ? restoreAllDurations(ctx) : ctx),
172
- );
173
- }
174
- },
175
- appendEntry: (...args: any[]) => (pi.appendEntry as any)(...args),
176
- } as unknown as ExtensionAPI);
177
- host[COMPACT_THINKING_OWNER] = { owner, stop };
191
+
192
+ const activate = (event: any, ctx: any) => {
193
+ // Headless subagent runtimes share this process. Never steal the parent
194
+ // TUI prototype patch or kill its thinking ticker.
195
+ if (ctx?.mode !== "tui") return;
196
+
197
+ host[COMPACT_THINKING_OWNER]?.stop(event, ctx);
198
+ session = { event, ctx };
199
+
200
+ const loaded = loadPatchedUpstream();
201
+ upstreamConfig = loaded.config;
202
+ Object.assign(upstreamConfig, initialConfig);
203
+ delegates.clear();
204
+
205
+ loaded.compactThinking({
206
+ on(eventName: string, handler: UpstreamHandler) {
207
+ if (eventName === "session_start") {
208
+ // Already inside session_start — run immediately.
209
+ handler(event, restoreAllDurations(ctx));
210
+ return;
211
+ }
212
+ if (eventName === "session_shutdown") {
213
+ delegates.set(eventName, handler);
214
+ return;
215
+ }
216
+ delegates.set(eventName, handler);
217
+ bind(eventName);
218
+ },
219
+ appendEntry: (...args: any[]) => (pi.appendEntry as any)(...args),
220
+ } as unknown as ExtensionAPI);
221
+
222
+ active = true;
223
+ host[COMPACT_THINKING_OWNER] = { owner, stop };
224
+ };
225
+
226
+ pi.on("session_start", (event, ctx) => {
227
+ session = { event, ctx };
228
+ activate(event, ctx);
229
+ });
230
+ pi.on("session_shutdown", (event, ctx) => {
231
+ if (host[COMPACT_THINKING_OWNER]?.owner === owner) stop(event, ctx);
232
+ session = undefined;
233
+ });
178
234
 
179
235
  return {
180
236
  updateConfig(next) {
181
- Object.assign(upstreamConfig, next);
237
+ Object.assign(initialConfig, next);
238
+ if (upstreamConfig) Object.assign(upstreamConfig, next);
182
239
  },
183
240
  };
184
241
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.8.35",
3
+ "version": "0.8.36",
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": [