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.
- package/extensions/ask-user-question/state/build-questionnaire.ts +14 -2
- package/extensions/ask-user-question/view/components/preview/preview-layout-decider.ts +7 -0
- package/extensions/claude-code-style.ts +949 -72
- package/extensions/compact-style.ts +4 -2
- package/extensions/context.ts +53 -14
- package/extensions/session-reference.ts +86 -9
- package/extensions/tool-diff/diff-renderer.ts +220 -86
- package/extensions/tool-diff/index.ts +17 -4
- package/extensions/tool-diff/types.ts +7 -1
- package/extensions/tool-result-sanitize.ts +10 -3
- package/package.json +1 -1
|
@@ -192,7 +192,8 @@ function shortenPath(path: unknown): string {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
function oneLine(value: unknown): string {
|
|
195
|
-
|
|
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
|
-
|
|
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);
|
package/extensions/context.ts
CHANGED
|
@@ -213,19 +213,53 @@ export function formatTokens(tokens: number): string {
|
|
|
213
213
|
return `${Math.round(tokens / 1_000)}k`;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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(
|
|
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
|
|
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
|
|
267
|
-
const
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
):
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
369
|
-
|
|
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
|
}
|