pi-cc-extensions 0.8.13 → 0.8.15

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.
@@ -192,7 +192,8 @@ function shortenPath(path: unknown): string {
192
192
  }
193
193
 
194
194
  function oneLine(value: unknown): string {
195
- return sanitizeToolResultText(String(value ?? ""))
195
+ // Preview paths only need a short prefix; skip scanning multi-MB tool outputs.
196
+ return sanitizeToolResultText(String(value ?? ""), 4_096)
196
197
  .replace(/\s+/g, " ")
197
198
  .trim();
198
199
  }
@@ -298,7 +299,8 @@ export function resultPreview(result: any, isPartial = false): string {
298
299
  ? result.content.find((c: any) => c?.type === "text" && typeof c.text === "string")?.text
299
300
  : undefined;
300
301
  if (!text) return isPartial ? "running" : "";
301
- const lines = sanitizeToolResultText(text)
302
+ // Only the first non-empty line / line count is used; bound the scan window.
303
+ const lines = sanitizeToolResultText(text, 8_192)
302
304
  .trim()
303
305
  .split("\n")
304
306
  .filter((line) => line.trim().length > 0);
@@ -213,19 +213,53 @@ export function formatTokens(tokens: number): string {
213
213
  return `${Math.round(tokens / 1_000)}k`;
214
214
  }
215
215
 
216
- function collectParts(ctx: ExtensionCommandContext): ContextPart[] {
217
- const options = ctx.getSystemPromptOptions();
216
+ type SystemPromptOptions = {
217
+ contextFiles?: Array<{ path: string; content: string }>;
218
+ skills?: Array<{
219
+ name: string;
220
+ description?: string;
221
+ filePath?: string;
222
+ disableModelInvocation?: boolean;
223
+ }>;
224
+ selectedTools?: string[];
225
+ toolSnippets?: Record<string, string>;
226
+ promptGuidelines?: string[];
227
+ };
228
+
229
+ type ContextBreakdown = {
230
+ parts: ContextPart[];
231
+ options: SystemPromptOptions;
232
+ systemPrompt: string;
233
+ };
234
+
235
+ /**
236
+ * Single pass over prompt options + session entries. Returns options/systemPrompt
237
+ * so the /context UI does not re-fetch or re-stringify the same sources.
238
+ */
239
+ function collectContextBreakdown(ctx: ExtensionCommandContext): ContextBreakdown {
240
+ const options = (ctx.getSystemPromptOptions?.() ?? {}) as SystemPromptOptions;
241
+ const systemPrompt = typeof ctx.getSystemPrompt === "function" ? ctx.getSystemPrompt() : "";
242
+
218
243
  const contextFileTokens = (options.contextFiles ?? []).reduce(
219
244
  (sum, file) => sum + tokenEstimate(file.content),
220
245
  0,
221
246
  );
222
- const skillTokens = (options.skills ?? []).reduce((sum, skill) => sum + tokenEstimate(skill), 0);
247
+ // Prefer field-level estimates over JSON.stringify(whole skill).
248
+ const skillTokens = (options.skills ?? []).reduce((sum, skill) => {
249
+ if (!skill || typeof skill !== "object") return sum + tokenEstimate(skill);
250
+ return (
251
+ sum +
252
+ tokenEstimate(skill.name) +
253
+ tokenEstimate(skill.description) +
254
+ tokenEstimate(skill.filePath)
255
+ );
256
+ }, 0);
223
257
  const tools = options.selectedTools ?? [];
224
- const toolTokens =
225
- tools.reduce(
226
- (sum, name) => sum + tokenEstimate(name) + tokenEstimate(options.toolSnippets?.[name]),
227
- 0,
228
- ) + tokenEstimate(options.promptGuidelines);
258
+ const snippets = options.toolSnippets;
259
+ let toolTokens = tokenEstimate(options.promptGuidelines);
260
+ for (const name of tools) {
261
+ toolTokens += tokenEstimate(name) + tokenEstimate(snippets?.[name]);
262
+ }
229
263
 
230
264
  let user = 0;
231
265
  let assistant = 0;
@@ -242,7 +276,7 @@ function collectParts(ctx: ExtensionCommandContext): ContextPart[] {
242
276
  }
243
277
  }
244
278
 
245
- const systemTotal = tokenEstimate(ctx.getSystemPrompt());
279
+ const systemTotal = tokenEstimate(systemPrompt);
246
280
  const baseSystem = Math.max(0, systemTotal - contextFileTokens - skillTokens - toolTokens);
247
281
  const parts: ContextPart[] = [
248
282
  { label: "System prompt", tokens: baseSystem, color: "accent" },
@@ -254,7 +288,11 @@ function collectParts(ctx: ExtensionCommandContext): ContextPart[] {
254
288
  { label: "Tool results", tokens: toolResults, color: "dim" },
255
289
  { label: "Compaction summaries", tokens: summaries, color: "success" },
256
290
  ];
257
- return parts.filter((part) => part.tokens > 0);
291
+ return {
292
+ parts: parts.filter((part) => part.tokens > 0),
293
+ options,
294
+ systemPrompt,
295
+ };
258
296
  }
259
297
 
260
298
  export default function contextUsageExtension(pi: ExtensionAPI) {
@@ -263,8 +301,9 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
263
301
  handler: async (_args, ctx) => {
264
302
  const usage = ctx.getContextUsage();
265
303
  const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
266
- const used = usage?.tokens ?? collectParts(ctx).reduce((sum, part) => sum + part.tokens, 0);
267
- const parts = scaleParts(collectParts(ctx), used);
304
+ const breakdown = collectContextBreakdown(ctx);
305
+ const used = usage?.tokens ?? breakdown.parts.reduce((sum, part) => sum + part.tokens, 0);
306
+ const parts = scaleParts(breakdown.parts, used);
268
307
  const free = Math.max(0, contextWindow - used);
269
308
  const allParts = [...parts, { label: "Free space", tokens: free, color: "dim" as const }];
270
309
 
@@ -274,7 +313,7 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
274
313
  return;
275
314
  }
276
315
 
277
- const options = ctx.getSystemPromptOptions();
316
+ const options = breakdown.options;
278
317
  const toolByName = new Map<string, ToolPreviewInfo>(
279
318
  (pi.getAllTools() as ToolPreviewInfo[]).map((tool) => [tool.name, tool] as const),
280
319
  );
@@ -313,7 +352,7 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
313
352
  key: "systemPrompt",
314
353
  label: "System prompt",
315
354
  title: "System Prompt",
316
- content: ctx.getSystemPrompt(),
355
+ content: breakdown.systemPrompt,
317
356
  },
318
357
  {
319
358
  key: "tools",
@@ -66,6 +66,20 @@ type SubagentManager = {
66
66
 
67
67
  // Local subagent record tracking (pi-subagents does not expose a global manager).
68
68
  const liveSubagentRecords = new Map<string, SubagentLiveRecord>();
69
+ /** Cap retained completed/live records so long-lived pi processes cannot grow unbounded. */
70
+ const MAX_LIVE_SUBAGENT_RECORDS = 200;
71
+
72
+ function pruneLiveSubagentRecords(): void {
73
+ while (liveSubagentRecords.size > MAX_LIVE_SUBAGENT_RECORDS) {
74
+ const oldest = liveSubagentRecords.keys().next().value;
75
+ if (oldest === undefined) break;
76
+ liveSubagentRecords.delete(oldest);
77
+ }
78
+ }
79
+
80
+ function clearLiveSubagentRecords(): void {
81
+ liveSubagentRecords.clear();
82
+ }
69
83
 
70
84
  function trackSubagentFromEvent(data: unknown): void {
71
85
  if (!data || typeof data !== "object") return;
@@ -87,6 +101,9 @@ function trackSubagentFromEvent(data: unknown): void {
87
101
  typeof event.endedAt === "number" ? event.endedAt : event.lastUpdate
88
102
  ) as number;
89
103
  }
104
+ // Refresh insertion order so completed-but-still-referenced runs stay recent.
105
+ liveSubagentRecords.delete(runId);
106
+ liveSubagentRecords.set(runId, existing);
90
107
  return;
91
108
  }
92
109
  // Started event — create new record
@@ -100,6 +117,7 @@ function trackSubagentFromEvent(data: unknown): void {
100
117
  cwd,
101
118
  startedAt,
102
119
  });
120
+ pruneLiveSubagentRecords();
103
121
  }
104
122
 
105
123
  function getSubagentManager(): SubagentManager | undefined {
@@ -134,8 +152,14 @@ function formatDate(date: Date): string {
134
152
  }).format(date);
135
153
  }
136
154
 
155
+ /** Stable across keystrokes while getReferences returns the same reference objects. */
156
+ const sessionSearchTextCache = new WeakMap<SessionReference, string>();
157
+ const sessionItemCache = new WeakMap<SessionReference, { cwd: string; item: AutocompleteItem }>();
158
+
137
159
  function sessionSearchText(reference: SessionReference): string {
138
- return [
160
+ const cached = sessionSearchTextCache.get(reference);
161
+ if (cached !== undefined) return cached;
162
+ const text = [
139
163
  reference.info.name,
140
164
  reference.info.firstMessage,
141
165
  reference.info.cwd,
@@ -144,34 +168,65 @@ function sessionSearchText(reference: SessionReference): string {
144
168
  ]
145
169
  .filter(Boolean)
146
170
  .join(" ");
171
+ sessionSearchTextCache.set(reference, text);
172
+ return text;
147
173
  }
148
174
 
149
175
  function sessionItem(reference: SessionReference, currentCwd: string): AutocompleteItem {
176
+ const cached = sessionItemCache.get(reference);
177
+ if (cached && cached.cwd === currentCwd) return cached.item;
178
+
150
179
  const session = reference.info;
151
180
  const workspace = samePath(session.cwd, currentCwd)
152
181
  ? "current workspace"
153
182
  : session.cwd || "unknown workspace";
154
183
  const label = reference.kind === "subagent" ? "[SubAgent]" : "[Session]";
155
- return {
184
+ const item: AutocompleteItem = {
156
185
  value: `${SESSION_REFERENCE_PREFIX}${reference.referenceIds[0]}`,
157
186
  label: `${label} ${sessionTitle(session)}`,
158
187
  description: `${workspace} · ${session.messageCount} messages · ${formatDate(session.modified)}`,
159
188
  };
189
+ sessionItemCache.set(reference, { cwd: currentCwd, item });
190
+ return item;
160
191
  }
161
192
 
162
- function filterSessions(
193
+ /** Sort cache: same array + cwd reuses order (getReferences returns a stable ordered array). */
194
+ const orderedReferencesCache = new WeakMap<
195
+ SessionReference[],
196
+ { cwd: string; ordered: SessionReference[] }
197
+ >();
198
+
199
+ function orderSessionReferences(
163
200
  references: SessionReference[],
164
- query: string,
165
201
  currentCwd: string,
166
- ): AutocompleteItem[] {
167
- if (query.startsWith("session:")) return [];
202
+ ): SessionReference[] {
203
+ const cached = orderedReferencesCache.get(references);
204
+ if (cached && cached.cwd === currentCwd) return cached.ordered;
168
205
 
169
206
  const ordered = [...references].sort((left, right) => {
170
207
  const leftLocal = samePath(left.info.cwd, currentCwd) ? 1 : 0;
171
208
  const rightLocal = samePath(right.info.cwd, currentCwd) ? 1 : 0;
172
209
  return rightLocal - leftLocal || right.info.modified.getTime() - left.info.modified.getTime();
173
210
  });
174
- const matches = query.trim() ? fuzzyFilter(ordered, query, sessionSearchText) : ordered;
211
+ orderedReferencesCache.set(references, { cwd: currentCwd, ordered });
212
+ // Point the ordered array at itself so subsequent keystrokes skip re-sorting.
213
+ orderedReferencesCache.set(ordered, { cwd: currentCwd, ordered });
214
+ return ordered;
215
+ }
216
+
217
+ function filterSessions(
218
+ references: SessionReference[],
219
+ query: string,
220
+ currentCwd: string,
221
+ ): AutocompleteItem[] {
222
+ if (query.startsWith("session:")) return [];
223
+
224
+ const ordered = orderSessionReferences(references, currentCwd);
225
+ const trimmed = query.trim();
226
+ // Empty query: top-N already sorted — no fuzzy scan over the full list.
227
+ const matches = trimmed
228
+ ? fuzzyFilter(ordered, trimmed, sessionSearchText)
229
+ : ordered.slice(0, MAX_SESSION_SUGGESTIONS);
175
230
  return matches
176
231
  .slice(0, MAX_SESSION_SUGGESTIONS)
177
232
  .map((reference) => sessionItem(reference, currentCwd));
@@ -341,6 +396,7 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
341
396
  pi.on("session_start", (_event, ctx) => {
342
397
  const generation = ++sessionGeneration;
343
398
  subagentIds.clear();
399
+ clearLiveSubagentRecords();
344
400
  let loadErrorShown = false;
345
401
  const currentSessionId = ctx.sessionManager.getSessionId();
346
402
  const currentSessionFile = ctx.sessionManager.getSessionFile();
@@ -365,8 +421,27 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
365
421
  return sessionsPromise;
366
422
  };
367
423
 
368
- const getReferences = async (): Promise<SessionReference[]> =>
369
- mergeReferences(await getSessions(), liveSubagentReferences(subagentIds, currentSessionId));
424
+ // Pre-sort once per (sessions list, subagent id set) so @ keystrokes only fuzzy-filter.
425
+ let referencesCache:
426
+ | { sessions: SessionInfo[]; subagentKey: string; ordered: SessionReference[] }
427
+ | undefined;
428
+ const getReferences = async (): Promise<SessionReference[]> => {
429
+ const sessions = await getSessions();
430
+ const subagentKey = [...subagentIds].join("\0");
431
+ if (
432
+ referencesCache &&
433
+ referencesCache.sessions === sessions &&
434
+ referencesCache.subagentKey === subagentKey
435
+ ) {
436
+ return referencesCache.ordered;
437
+ }
438
+ const ordered = orderSessionReferences(
439
+ mergeReferences(sessions, liveSubagentReferences(subagentIds, currentSessionId)),
440
+ ctx.cwd,
441
+ );
442
+ referencesCache = { sessions, subagentKey, ordered };
443
+ return ordered;
444
+ };
370
445
 
371
446
  getAvailableReferences = getReferences;
372
447
  if (ctx.mode === "tui") {
@@ -463,6 +538,7 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
463
538
 
464
539
  pi.on("session_before_switch", () => {
465
540
  subagentIds.clear();
541
+ clearLiveSubagentRecords();
466
542
  getAvailableReferences = undefined;
467
543
  });
468
544
 
@@ -470,6 +546,7 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
470
546
  sessionGeneration++;
471
547
  for (const unsubscribe of unsubscribeSubagentEvents) unsubscribe();
472
548
  subagentIds.clear();
549
+ clearLiveSubagentRecords();
473
550
  getAvailableReferences = undefined;
474
551
  });
475
552
  }