pi-goosedump 0.8.0 → 0.9.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.
- package/README.md +69 -71
- package/index.ts +750 -917
- package/package.json +5 -3
package/index.ts
CHANGED
|
@@ -13,15 +13,27 @@ import type {
|
|
|
13
13
|
import { defineTool, getSettingsListTheme } from '@earendil-works/pi-coding-agent';
|
|
14
14
|
|
|
15
15
|
import { execFileSync } from 'node:child_process';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
closeSync,
|
|
18
|
+
existsSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
openSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
readSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from 'node:fs';
|
|
17
25
|
import { createRequire } from 'node:module';
|
|
18
26
|
import { homedir } from 'node:os';
|
|
19
|
-
import {
|
|
27
|
+
import { dirname, join } from 'node:path';
|
|
20
28
|
|
|
21
29
|
import {
|
|
30
|
+
type Component,
|
|
31
|
+
Input,
|
|
22
32
|
Key,
|
|
23
33
|
SettingsList,
|
|
24
34
|
type SettingItem,
|
|
35
|
+
decodeKittyPrintable,
|
|
36
|
+
getKeybindings,
|
|
25
37
|
matchesKey,
|
|
26
38
|
truncateToWidth,
|
|
27
39
|
visibleWidth,
|
|
@@ -31,95 +43,33 @@ import { Type } from '@sinclair/typebox';
|
|
|
31
43
|
|
|
32
44
|
const require = createRequire(import.meta.url);
|
|
33
45
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// One element of goosedump 0.8's `list` JSON output.
|
|
37
|
-
interface ListEntryJson {
|
|
38
|
-
id: string;
|
|
39
|
-
parent: string | null;
|
|
40
|
-
provider: string;
|
|
41
|
-
cwd: string;
|
|
42
|
-
count: number;
|
|
43
|
-
label: string | null;
|
|
44
|
-
from: string | null;
|
|
45
|
-
until: string | null;
|
|
46
|
-
native_id?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface GoosedumpListing {
|
|
50
|
-
id: string;
|
|
51
|
-
provider: string;
|
|
52
|
-
cwd: string;
|
|
53
|
-
parent: string | null;
|
|
54
|
-
label: string | null;
|
|
55
|
-
path: string | null;
|
|
56
|
-
}
|
|
57
|
-
|
|
46
|
+
// One rendered message after normalizing a context into pi's native JSONL.
|
|
58
47
|
interface GoosedumpMessage {
|
|
59
48
|
id: string;
|
|
60
49
|
role: string;
|
|
61
50
|
content: string;
|
|
62
|
-
score?: number;
|
|
63
51
|
}
|
|
64
52
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
totalPages: number;
|
|
69
|
-
}
|
|
70
|
-
interface ShowPartJson {
|
|
71
|
-
type: 'text' | 'reasoning' | 'toolCall' | 'toolResult' | 'image' | 'bash' | 'passthrough';
|
|
53
|
+
// A `content` part of a pi-native message line.
|
|
54
|
+
interface PiPartJson {
|
|
55
|
+
type: string;
|
|
72
56
|
text?: string;
|
|
57
|
+
thinking?: string;
|
|
73
58
|
name?: string;
|
|
74
59
|
arguments?: unknown;
|
|
75
|
-
toolName?: string;
|
|
76
|
-
content?: string;
|
|
77
|
-
isError?: boolean;
|
|
78
60
|
mimeType?: string;
|
|
79
|
-
|
|
80
|
-
command?: string;
|
|
81
|
-
output?: string;
|
|
82
|
-
kind?: string;
|
|
83
|
-
raw?: unknown;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
interface ShowMessageJson {
|
|
87
|
-
entryId: string;
|
|
88
|
-
role: string;
|
|
89
|
-
parts: ShowPartJson[];
|
|
61
|
+
data?: string;
|
|
90
62
|
}
|
|
91
63
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
role: string;
|
|
99
|
-
score: number;
|
|
100
|
-
text: string;
|
|
101
|
-
files: string[];
|
|
102
|
-
terms: string[];
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
interface SearchJson {
|
|
106
|
-
page: number;
|
|
107
|
-
totalPages: number;
|
|
108
|
-
total: number;
|
|
109
|
-
hits: HitJson[];
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
interface CompactJson {
|
|
113
|
-
goals: string[];
|
|
114
|
-
files: { modified: string[]; created: string[]; read: string[] };
|
|
115
|
-
commits: string[];
|
|
116
|
-
outstanding: string[];
|
|
117
|
-
preferences: string[];
|
|
118
|
-
brief: string;
|
|
64
|
+
// One JSONL line from `show`/`grep`/`search`/`compact` rendered as pi native.
|
|
65
|
+
interface PiEntryJson {
|
|
66
|
+
type?: string;
|
|
67
|
+
id?: string;
|
|
68
|
+
message?: { role?: string; content?: PiPartJson[] | string };
|
|
69
|
+
summary?: string;
|
|
119
70
|
}
|
|
120
71
|
|
|
121
72
|
interface GoosedumpSettings {
|
|
122
|
-
stepsBeforeCompact: number;
|
|
123
73
|
turnsBeforeCompact: number;
|
|
124
74
|
}
|
|
125
75
|
|
|
@@ -127,12 +77,9 @@ type SettingsScope = 'global' | 'project';
|
|
|
127
77
|
type GoosedumpSettingKey = keyof GoosedumpSettings;
|
|
128
78
|
|
|
129
79
|
const DEFAULT_GOOSEDUMP_SETTINGS: GoosedumpSettings = {
|
|
130
|
-
stepsBeforeCompact: 0,
|
|
131
80
|
turnsBeforeCompact: 0,
|
|
132
81
|
};
|
|
133
82
|
|
|
134
|
-
const COMPACT_SETTING_VALUES = ['0', '5', '10', '20', '50', '100'];
|
|
135
|
-
|
|
136
83
|
function piAgentDir(): string {
|
|
137
84
|
return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
|
|
138
85
|
}
|
|
@@ -165,11 +112,9 @@ function readGoosedumpSettingsFrom(path: string): Partial<GoosedumpSettings> {
|
|
|
165
112
|
if (!isRecord(raw)) return {};
|
|
166
113
|
|
|
167
114
|
const settings: Partial<GoosedumpSettings> = {};
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
settings[key] = value;
|
|
172
|
-
}
|
|
115
|
+
const value = raw.turnsBeforeCompact;
|
|
116
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 && value <= 255) {
|
|
117
|
+
settings.turnsBeforeCompact = value;
|
|
173
118
|
}
|
|
174
119
|
return settings;
|
|
175
120
|
}
|
|
@@ -206,48 +151,7 @@ function formatCompactSetting(value: number): string {
|
|
|
206
151
|
}
|
|
207
152
|
|
|
208
153
|
function resolveGoosedumpBinary(): string {
|
|
209
|
-
|
|
210
|
-
return require.resolve('@jarkkojs/goosedump/bin/goosedump.js');
|
|
211
|
-
} catch {
|
|
212
|
-
const dir = join(
|
|
213
|
-
process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent'),
|
|
214
|
-
'package-cache',
|
|
215
|
-
'node_modules',
|
|
216
|
-
'@jarkkojs',
|
|
217
|
-
'goosedump',
|
|
218
|
-
);
|
|
219
|
-
const bin = join(dir, 'bin', 'goosedump.js');
|
|
220
|
-
if (existsSync(bin)) return bin;
|
|
221
|
-
|
|
222
|
-
throw new Error('goosedump binary not found. Install with: npm install @jarkkojs/goosedump');
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function goosedumpVersion(): string | null {
|
|
227
|
-
try {
|
|
228
|
-
const pkg = require('@jarkkojs/goosedump/package.json') as { version: string };
|
|
229
|
-
return pkg.version;
|
|
230
|
-
} catch {
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function parseVersion(version: string): [number, number, number] | null {
|
|
236
|
-
const match = version.match(/\b(\d+)\.(\d+)\.(\d+)\b/);
|
|
237
|
-
if (!match) return null;
|
|
238
|
-
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function hasMinimumVersion(version: string, minimum: readonly [number, number, number]): boolean {
|
|
242
|
-
const parsed = parseVersion(version);
|
|
243
|
-
if (!parsed) return false;
|
|
244
|
-
|
|
245
|
-
for (let i = 0; i < minimum.length; i++) {
|
|
246
|
-
if (parsed[i] > minimum[i]) return true;
|
|
247
|
-
if (parsed[i] < minimum[i]) return false;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return true;
|
|
154
|
+
return require.resolve('@jarkkojs/goosedump/bin/goosedump.js');
|
|
251
155
|
}
|
|
252
156
|
|
|
253
157
|
function clip(value: string, max: number): string {
|
|
@@ -271,47 +175,50 @@ function summarizeToolArgs(args: unknown): string {
|
|
|
271
175
|
return clip(JSON.stringify(args), 160);
|
|
272
176
|
}
|
|
273
177
|
|
|
274
|
-
function
|
|
178
|
+
function flattenPiPart(part: PiPartJson): string {
|
|
275
179
|
switch (part.type) {
|
|
276
180
|
case 'text':
|
|
277
|
-
case 'reasoning':
|
|
278
181
|
return part.text ?? '';
|
|
182
|
+
case 'thinking':
|
|
183
|
+
return part.thinking ?? '';
|
|
279
184
|
case 'toolCall': {
|
|
280
185
|
const summary = summarizeToolArgs(part.arguments);
|
|
281
186
|
return summary ? `${part.name ?? 'tool'}(${summary})` : (part.name ?? 'tool');
|
|
282
187
|
}
|
|
283
|
-
case '
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return
|
|
287
|
-
}
|
|
288
|
-
case 'image': {
|
|
289
|
-
const header = `[image ${part.mimeType ?? 'unknown'}]`;
|
|
290
|
-
return part.ocrText ? `${header}\n${part.ocrText}` : header;
|
|
291
|
-
}
|
|
292
|
-
case 'bash': {
|
|
293
|
-
const cmd = part.command ? `$ ${part.command}` : '';
|
|
294
|
-
if (cmd && part.output) return `${cmd}\n${part.output}`;
|
|
295
|
-
return cmd || (part.output ?? '');
|
|
296
|
-
}
|
|
297
|
-
case 'passthrough': {
|
|
298
|
-
const kind = part.kind ? `passthrough:${part.kind}` : 'passthrough';
|
|
299
|
-
const raw = part.raw === undefined ? '' : JSON.stringify(part.raw);
|
|
300
|
-
return raw ? `[${kind}] ${raw}` : `[${kind}]`;
|
|
301
|
-
}
|
|
188
|
+
case 'image':
|
|
189
|
+
return part.mimeType ? `[image ${part.mimeType}]` : '[image]';
|
|
190
|
+
default:
|
|
191
|
+
return clip(JSON.stringify(part), 160);
|
|
302
192
|
}
|
|
303
193
|
}
|
|
304
194
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
return
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
195
|
+
// goosedump's pi render emits a tool result as a message whose `content` is a
|
|
196
|
+
// plain string rather than a part array, so accept both shapes.
|
|
197
|
+
function flattenPiContent(content: PiPartJson[] | string | undefined): string {
|
|
198
|
+
if (typeof content === 'string') return content;
|
|
199
|
+
if (!Array.isArray(content)) return '';
|
|
200
|
+
return content.map(flattenPiPart).filter(Boolean).join('\n');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// goosedump 0.9 renders `show`/`grep`/`search` in a provider's native format.
|
|
204
|
+
// Passing `--as-provider pi` normalizes every provider's output to pi's JSONL,
|
|
205
|
+
// so one parser covers all sources: each `{"type":"message"}` line becomes a
|
|
206
|
+
// `GoosedumpMessage` with its `content[]` parts flattened to readable text.
|
|
207
|
+
function parsePiMessages(output: string): GoosedumpMessage[] {
|
|
208
|
+
const messages: GoosedumpMessage[] = [];
|
|
209
|
+
for (const line of output.split('\n')) {
|
|
210
|
+
if (!line) continue;
|
|
211
|
+
let entry: PiEntryJson;
|
|
212
|
+
try {
|
|
213
|
+
entry = JSON.parse(line) as PiEntryJson;
|
|
214
|
+
} catch {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (entry.type !== 'message' || !entry.message) continue;
|
|
218
|
+
const content = flattenPiContent(entry.message.content);
|
|
219
|
+
messages.push({ id: entry.id ?? '', role: entry.message.role ?? 'unknown', content });
|
|
220
|
+
}
|
|
221
|
+
return messages;
|
|
315
222
|
}
|
|
316
223
|
|
|
317
224
|
const ANSI_SGR = new RegExp(String.fromCharCode(27) + '\\[[0-9;]*m', 'g');
|
|
@@ -334,64 +241,58 @@ function textResult(text: string): AgentToolResult<void> {
|
|
|
334
241
|
return { content: [{ type: 'text', text }], details: undefined };
|
|
335
242
|
}
|
|
336
243
|
|
|
337
|
-
function
|
|
244
|
+
function missingSessionId(tool: string, provider = 'pi'): AgentToolResult<void> {
|
|
338
245
|
if (provider !== 'pi') {
|
|
339
|
-
return textResult(`
|
|
246
|
+
return textResult(`session is required for ${tool} when provider is "${provider}"`);
|
|
340
247
|
}
|
|
341
|
-
return textResult(`
|
|
248
|
+
return textResult(`session is required for ${tool} when no current session is available`);
|
|
342
249
|
}
|
|
343
250
|
|
|
344
251
|
function runGoosedump(args: string[]): string {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
function goosedumpList(provider = 'pi'): GoosedumpListing[] {
|
|
351
|
-
const entries = JSON.parse(runGoosedump(['list'])) as ListEntryJson[];
|
|
352
|
-
return entries
|
|
353
|
-
.filter((entry) => entry.provider === provider)
|
|
354
|
-
.map((entry) => {
|
|
355
|
-
const nativeId = entry.native_id ?? entry.id;
|
|
356
|
-
return {
|
|
357
|
-
id: entry.id,
|
|
358
|
-
provider: entry.provider,
|
|
359
|
-
cwd: entry.cwd,
|
|
360
|
-
parent: entry.parent,
|
|
361
|
-
label: entry.label ?? null,
|
|
362
|
-
// Only pi sessions map back to a local file we can resume.
|
|
363
|
-
path: provider === 'pi' ? piSessionFilePath(nativeId) : null,
|
|
364
|
-
};
|
|
252
|
+
try {
|
|
253
|
+
return execFileSync('node', [resolveGoosedumpBinary(), ...args], {
|
|
254
|
+
encoding: 'utf-8',
|
|
255
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
365
256
|
});
|
|
257
|
+
} catch (err) {
|
|
258
|
+
// Surface goosedump's own stderr instead of Node's truncated generic message.
|
|
259
|
+
const stderr = (err as { stderr?: string }).stderr?.trim();
|
|
260
|
+
const message = stderr ?? (err instanceof Error ? err.message : String(err));
|
|
261
|
+
throw new Error(message);
|
|
262
|
+
}
|
|
366
263
|
}
|
|
367
264
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
265
|
+
// Read only the first line of a file without loading the whole transcript.
|
|
266
|
+
function readFirstLine(path: string): string | null {
|
|
267
|
+
const fd = openSync(path, 'r');
|
|
268
|
+
try {
|
|
269
|
+
const buf = Buffer.alloc(8192);
|
|
270
|
+
const bytesRead = readSync(fd, buf, 0, buf.length, 0);
|
|
271
|
+
if (bytesRead < 0) return null;
|
|
272
|
+
const slice = buf.subarray(0, bytesRead);
|
|
273
|
+
const newline = slice.indexOf(0x0a);
|
|
274
|
+
return (newline < 0 ? slice : slice.subarray(0, newline)).toString('utf8');
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
} finally {
|
|
278
|
+
closeSync(fd);
|
|
279
|
+
}
|
|
373
280
|
}
|
|
374
281
|
|
|
375
|
-
// goosedump
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const path = join(piSessionsDir(), `${nativeId}.jsonl`);
|
|
379
|
-
return existsSync(path) ? path : null;
|
|
282
|
+
// goosedump 0.9 addresses every context as an unambiguous `provider:id` target.
|
|
283
|
+
function formatTarget(provider: string, id: string): string {
|
|
284
|
+
return `${provider}:${id}`;
|
|
380
285
|
}
|
|
381
286
|
|
|
382
287
|
function goosedumpSearch(
|
|
383
288
|
target: string,
|
|
384
289
|
query: string,
|
|
385
290
|
options: { scope?: string; page?: number } = {},
|
|
386
|
-
):
|
|
291
|
+
): GoosedumpMessage[] {
|
|
387
292
|
const scope = options.scope ?? 'lineage';
|
|
388
|
-
const args = ['search', target, query];
|
|
389
|
-
if (options.page) args.push('
|
|
390
|
-
args
|
|
391
|
-
|
|
392
|
-
const json = JSON.parse(runGoosedump(args)) as SearchJson;
|
|
393
|
-
const messages = (json.hits ?? []).map(hitMessage);
|
|
394
|
-
return { messages, page: json.page ?? options.page ?? 1, totalPages: json.totalPages ?? 1 };
|
|
293
|
+
const args = ['search', target, query, '--as-provider', 'pi', '--scope', scope];
|
|
294
|
+
if (options.page) args.push('-p', String(options.page));
|
|
295
|
+
return parsePiMessages(runGoosedump(args));
|
|
395
296
|
}
|
|
396
297
|
|
|
397
298
|
function goosedumpGrep(
|
|
@@ -400,10 +301,9 @@ function goosedumpGrep(
|
|
|
400
301
|
options: { scope?: string } = {},
|
|
401
302
|
): GoosedumpMessage[] {
|
|
402
303
|
const scope = options.scope ?? 'lineage';
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
return (json.hits ?? []).map(hitMessage);
|
|
304
|
+
return parsePiMessages(
|
|
305
|
+
runGoosedump(['grep', target, pattern, '--as-provider', 'pi', '--scope', scope]),
|
|
306
|
+
);
|
|
407
307
|
}
|
|
408
308
|
|
|
409
309
|
function goosedumpShow(
|
|
@@ -411,100 +311,44 @@ function goosedumpShow(
|
|
|
411
311
|
options: { scope?: string; ids?: string[] } = {},
|
|
412
312
|
): GoosedumpMessage[] {
|
|
413
313
|
const scope = options.scope ?? 'lineage';
|
|
414
|
-
const args = ['show', target];
|
|
314
|
+
const args = ['show', target, '--as-provider', 'pi', '--scope', scope];
|
|
415
315
|
for (const id of options.ids ?? []) args.push('-m', id);
|
|
416
|
-
args
|
|
417
|
-
|
|
418
|
-
const json = JSON.parse(runGoosedump(args)) as ShowJson;
|
|
419
|
-
return (json.messages ?? []).map(showMessage);
|
|
316
|
+
return parsePiMessages(runGoosedump(args));
|
|
420
317
|
}
|
|
421
318
|
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
// returns it verbatim. Only used when the caller explicitly sets outputFormat.
|
|
426
|
-
function goosedumpConvert(
|
|
427
|
-
subcommand: 'show' | 'grep' | 'search',
|
|
428
|
-
target: string,
|
|
429
|
-
outputFormat: string,
|
|
430
|
-
options: { positionals?: string[]; scope?: string; ids?: string[]; page?: number } = {},
|
|
431
|
-
): string {
|
|
432
|
-
const args = [subcommand, target, ...(options.positionals ?? [])];
|
|
433
|
-
for (const id of options.ids ?? []) args.push('-m', id);
|
|
434
|
-
if (options.page) args.push('--page', String(options.page));
|
|
435
|
-
args.push('--scope', options.scope ?? 'lineage', '--output-format', outputFormat);
|
|
436
|
-
return runGoosedump(args);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Resolve a goosedump target id. In 0.7 every context is addressed by its 8-hex
|
|
440
|
-
// index id, so an explicit contextId is already the target. It falls back to the
|
|
441
|
-
// current session only for pi; other providers have no current session, so they
|
|
442
|
-
// require an explicit contextId.
|
|
319
|
+
// Resolve a goosedump 0.9 `provider:id` target. An explicit contextId is the
|
|
320
|
+
// target id already; for pi it falls back to the current session. Other
|
|
321
|
+
// providers have no current session and require an explicit id.
|
|
443
322
|
function resolveTarget(
|
|
444
323
|
ctx: ExtensionContext,
|
|
445
324
|
provider: string,
|
|
446
325
|
contextId: string | undefined,
|
|
447
326
|
): string | null {
|
|
448
|
-
if (contextId) return contextId;
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
function capList(items: string[], limit: number): string {
|
|
453
|
-
const shown = items.slice(0, limit).join(', ');
|
|
454
|
-
return items.length > limit ? `${shown} (+${items.length - limit} more)` : shown;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function bulletSection(title: string, items: string[]): string | null {
|
|
458
|
-
if (items.length === 0) return null;
|
|
459
|
-
return `[${title}]\n${items.map((item) => `- ${item}`).join('\n')}`;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function renderCompactSummary(c: CompactJson): string {
|
|
463
|
-
const fileLines: string[] = [];
|
|
464
|
-
if (c.files.modified.length > 0) fileLines.push(`Modified: ${capList(c.files.modified, 10)}`);
|
|
465
|
-
if (c.files.created.length > 0) fileLines.push(`Created: ${capList(c.files.created, 10)}`);
|
|
466
|
-
if (c.files.read.length > 0) fileLines.push(`Read: ${capList(c.files.read, 10)}`);
|
|
467
|
-
|
|
468
|
-
const parts = [
|
|
469
|
-
bulletSection('Session Goal', c.goals),
|
|
470
|
-
bulletSection('Files And Changes', fileLines),
|
|
471
|
-
bulletSection('Commits', c.commits),
|
|
472
|
-
bulletSection('Outstanding Context', c.outstanding),
|
|
473
|
-
bulletSection('User Preferences', c.preferences),
|
|
474
|
-
c.brief.trim() ? c.brief : null,
|
|
475
|
-
].filter((part): part is string => part !== null);
|
|
476
|
-
|
|
477
|
-
return parts.join('\n\n---\n\n');
|
|
327
|
+
if (contextId) return formatTarget(provider, contextId);
|
|
328
|
+
if (provider === 'pi') return currentSessionTarget(ctx);
|
|
329
|
+
return null;
|
|
478
330
|
}
|
|
479
331
|
|
|
480
332
|
function goosedumpCompact(
|
|
481
333
|
target: string,
|
|
482
|
-
options: {
|
|
483
|
-
scope?: string;
|
|
484
|
-
from?: string;
|
|
485
|
-
until?: string;
|
|
486
|
-
ids?: string[];
|
|
487
|
-
},
|
|
334
|
+
options: { scope?: string; from?: string; until: string },
|
|
488
335
|
): string {
|
|
489
336
|
const scope = options.scope ?? 'lineage';
|
|
490
337
|
const args = ['compact', target, '--scope', scope];
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (
|
|
338
|
+
if (options.from) args.push('--from', options.from);
|
|
339
|
+
args.push('--until', options.until);
|
|
340
|
+
|
|
341
|
+
const output = runGoosedump(args);
|
|
342
|
+
for (const line of output.split('\n')) {
|
|
343
|
+
if (!line) continue;
|
|
344
|
+
try {
|
|
345
|
+
const entry = JSON.parse(line) as PiEntryJson;
|
|
346
|
+
if (entry.type === 'compaction' && typeof entry.summary === 'string') return entry.summary;
|
|
347
|
+
} catch {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
497
350
|
}
|
|
498
|
-
|
|
499
|
-
const json = JSON.parse(runGoosedump(args)) as CompactJson;
|
|
500
|
-
return renderCompactSummary(json);
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function goosedumpDelete(id: string, options: { dryRun?: boolean } = {}): string {
|
|
504
|
-
const args = ['delete'];
|
|
505
|
-
if (options.dryRun) args.push('--dry-run');
|
|
506
|
-
args.push(id);
|
|
507
|
-
return runGoosedump(args);
|
|
351
|
+
return output.trim();
|
|
508
352
|
}
|
|
509
353
|
|
|
510
354
|
export interface GoosedumpIntegrationOptions {
|
|
@@ -516,18 +360,14 @@ interface GoosedumpCompactRange {
|
|
|
516
360
|
scope: string;
|
|
517
361
|
from?: string;
|
|
518
362
|
until: string;
|
|
519
|
-
ids?: string[];
|
|
520
363
|
}
|
|
521
364
|
|
|
522
365
|
interface GoosedumpCompactionDetails {
|
|
523
366
|
source: 'goosedump';
|
|
524
|
-
|
|
525
|
-
contextId: string;
|
|
367
|
+
target: string;
|
|
526
368
|
scope: string;
|
|
527
369
|
from?: string;
|
|
528
|
-
until
|
|
529
|
-
ids?: string[];
|
|
530
|
-
previousSummaryIncluded: boolean;
|
|
370
|
+
until: string;
|
|
531
371
|
fileOps: unknown;
|
|
532
372
|
}
|
|
533
373
|
|
|
@@ -539,34 +379,56 @@ export default function (pi: ExtensionAPI) {
|
|
|
539
379
|
createGoosedumpIntegration().register(pi);
|
|
540
380
|
}
|
|
541
381
|
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
// file
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {
|
|
548
|
-
const name = basename(sessionFile);
|
|
549
|
-
const ext = extname(name);
|
|
550
|
-
return ext ? name.slice(0, -ext.length) : name;
|
|
551
|
-
}
|
|
552
|
-
const ext = extname(rel);
|
|
553
|
-
return ext ? rel.slice(0, -ext.length) : rel;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Resolve the current pi session to its goosedump 8-hex context id by matching the
|
|
557
|
-
// session file's native id against goosedump's index (via `list`).
|
|
558
|
-
function currentSessionContextId(ctx: ExtensionContext): string | null {
|
|
382
|
+
// Resolve the current pi session to its goosedump `pi:<id>` target. goosedump
|
|
383
|
+
// 0.9 uses the session header's own `id` field as the context id, so read it
|
|
384
|
+
// from the session file's first line and provider-qualify it; every target
|
|
385
|
+
// passed to the binary is `provider:id`, never bare.
|
|
386
|
+
function currentSessionTarget(ctx: ExtensionContext): string | null {
|
|
559
387
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
560
388
|
if (!sessionFile) return null;
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
389
|
+
const line = readFirstLine(sessionFile);
|
|
390
|
+
if (!line) return null;
|
|
391
|
+
try {
|
|
392
|
+
const entry = JSON.parse(line) as PiEntryJson;
|
|
393
|
+
if (entry.type === 'session' && typeof entry.id === 'string') {
|
|
394
|
+
return formatTarget('pi', entry.id);
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
564
400
|
}
|
|
565
401
|
|
|
566
402
|
function isCompactionEntry(entry: SessionEntry): entry is CompactionEntry {
|
|
567
403
|
return entry.type === 'compaction';
|
|
568
404
|
}
|
|
569
405
|
|
|
406
|
+
function isMessageEntry(entry: SessionEntry): entry is SessionEntry & {
|
|
407
|
+
type: 'message';
|
|
408
|
+
message: { role?: string };
|
|
409
|
+
} {
|
|
410
|
+
return entry.type === 'message';
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Resolution of the `keep:N` boundary: scan the active lineage from the newest
|
|
414
|
+
// entry backward, count user turns (those whose message role is `user`), and
|
|
415
|
+
// return the id of the Nth-from-last user entry — the earliest entry to keep.
|
|
416
|
+
// Returns undefined when there are fewer than `keep` user turns (nothing to
|
|
417
|
+
// summarize without orphaning the kept tail) or when every entry would be kept.
|
|
418
|
+
function keepBoundaryId(branchEntries: SessionEntry[], keep: number): string | undefined {
|
|
419
|
+
let seen = 0;
|
|
420
|
+
for (let i = branchEntries.length - 1; i >= 0; i -= 1) {
|
|
421
|
+
const entry = branchEntries[i];
|
|
422
|
+
if (isMessageEntry(entry) && entry.message.role === 'user') {
|
|
423
|
+
seen += 1;
|
|
424
|
+
if (seen === keep) {
|
|
425
|
+
return i === 0 ? undefined : entry.id;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
|
|
570
432
|
function latestCompactionBefore(
|
|
571
433
|
branchEntries: SessionEntry[],
|
|
572
434
|
untilIndex: number,
|
|
@@ -613,107 +475,8 @@ function buildCompactRange(
|
|
|
613
475
|
: { scope: 'lineage', until: firstKeptEntryId };
|
|
614
476
|
}
|
|
615
477
|
|
|
616
|
-
const LEGACY_GOOSEDUMP_NOTE =
|
|
617
|
-
'Use `goosedump` to search prior active-lineage work, decisions, and context from before this summary. Do not redo work already completed.';
|
|
618
|
-
const GOOSEDUMP_SEPARATOR = '\n\n---\n\n';
|
|
619
|
-
const GOOSEDUMP_HEADERS = [
|
|
620
|
-
'Session Goal',
|
|
621
|
-
'Files And Changes',
|
|
622
|
-
'Commits',
|
|
623
|
-
'Outstanding Context',
|
|
624
|
-
'User Preferences',
|
|
625
|
-
] as const;
|
|
626
|
-
|
|
627
|
-
function isGoosedumpSection(value: string): boolean {
|
|
628
|
-
return GOOSEDUMP_HEADERS.some((header) => value.startsWith(`[${header}]`));
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
function stripGoosedumpRecallNote(summary: string): string {
|
|
632
|
-
const idx = summary.lastIndexOf(LEGACY_GOOSEDUMP_NOTE);
|
|
633
|
-
if (idx < 0) return summary.trim();
|
|
634
|
-
return summary
|
|
635
|
-
.slice(0, idx)
|
|
636
|
-
.replace(/\s*(?:\n\n---\n\n)?\s*$/, '')
|
|
637
|
-
.trim();
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
function sectionOf(summary: string, header: string): string {
|
|
641
|
-
const tag = `[${header}]`;
|
|
642
|
-
const start = summary.indexOf(tag);
|
|
643
|
-
if (start < 0) return '';
|
|
644
|
-
|
|
645
|
-
const rest = summary.slice(start);
|
|
646
|
-
const nextHeader = GOOSEDUMP_HEADERS.filter((h) => h !== header)
|
|
647
|
-
.map((h) => rest.indexOf(`[${h}]`))
|
|
648
|
-
.filter((idx) => idx > 0);
|
|
649
|
-
const nextSeparator = rest.indexOf(GOOSEDUMP_SEPARATOR);
|
|
650
|
-
const candidates = [...nextHeader, ...(nextSeparator > 0 ? [nextSeparator] : [])].sort(
|
|
651
|
-
(a, b) => a - b,
|
|
652
|
-
);
|
|
653
|
-
const end = candidates[0];
|
|
654
|
-
return (end ? rest.slice(0, end) : rest).trim();
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function briefOf(summary: string): string {
|
|
658
|
-
return stripGoosedumpRecallNote(summary)
|
|
659
|
-
.split(GOOSEDUMP_SEPARATOR)
|
|
660
|
-
.map((part) => part.trim())
|
|
661
|
-
.filter((part) => part && !isGoosedumpSection(part))
|
|
662
|
-
.join(GOOSEDUMP_SEPARATOR);
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
function mergeSection(header: string, previous: string, next: string): string {
|
|
666
|
-
if (header === 'Outstanding Context') return next;
|
|
667
|
-
if (!previous) return next;
|
|
668
|
-
if (!next) return previous;
|
|
669
|
-
|
|
670
|
-
const lines = [
|
|
671
|
-
...new Set(
|
|
672
|
-
[...previous.split('\n'), ...next.split('\n')].filter((line) => line.startsWith('- ')),
|
|
673
|
-
),
|
|
674
|
-
];
|
|
675
|
-
const cap = header === 'Session Goal' ? 8 : header === 'User Preferences' ? 15 : 10;
|
|
676
|
-
const capped = lines.length > cap ? lines.slice(-cap) : lines;
|
|
677
|
-
return capped.length > 0 ? `[${header}]\n${capped.join('\n')}` : '';
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function capBriefTranscript(brief: string): string {
|
|
681
|
-
const lines = brief.split('\n');
|
|
682
|
-
if (lines.length <= 120) return brief;
|
|
683
|
-
const kept = lines.slice(-120);
|
|
684
|
-
const firstHeader = kept.findIndex((line) => /^\[(user|assistant)\]$/.test(line));
|
|
685
|
-
const clean = firstHeader > 0 ? kept.slice(firstHeader) : kept;
|
|
686
|
-
return `...(${lines.length - clean.length} earlier lines omitted)\n\n${clean.join('\n')}`;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
function mergeWithPreviousSummary(
|
|
690
|
-
summary: string,
|
|
691
|
-
previousSummary: string | undefined,
|
|
692
|
-
): { summary: string; previousSummaryIncluded: boolean } {
|
|
693
|
-
const nextSummary = stripGoosedumpRecallNote(summary);
|
|
694
|
-
const priorSummary = previousSummary ? stripGoosedumpRecallNote(previousSummary) : '';
|
|
695
|
-
|
|
696
|
-
if (!priorSummary) {
|
|
697
|
-
return { summary: summary.trim(), previousSummaryIncluded: false };
|
|
698
|
-
}
|
|
699
|
-
if (!nextSummary) {
|
|
700
|
-
return { summary: previousSummary?.trim() ?? '', previousSummaryIncluded: true };
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const sections = GOOSEDUMP_HEADERS.map((header) =>
|
|
704
|
-
mergeSection(header, sectionOf(priorSummary, header), sectionOf(nextSummary, header)),
|
|
705
|
-
).filter(Boolean);
|
|
706
|
-
const brief = capBriefTranscript(
|
|
707
|
-
[briefOf(priorSummary), briefOf(nextSummary)].filter(Boolean).join('\n\n'),
|
|
708
|
-
);
|
|
709
|
-
const parts = [...sections, ...(brief ? [brief] : [])];
|
|
710
|
-
const merged = parts.join(GOOSEDUMP_SEPARATOR);
|
|
711
|
-
|
|
712
|
-
return { summary: merged.trim(), previousSummaryIncluded: true };
|
|
713
|
-
}
|
|
714
|
-
|
|
715
478
|
function buildGoosedumpCompaction(
|
|
716
|
-
|
|
479
|
+
target: string,
|
|
717
480
|
preparation: {
|
|
718
481
|
firstKeptEntryId: string;
|
|
719
482
|
tokensBefore: number;
|
|
@@ -721,103 +484,138 @@ function buildGoosedumpCompaction(
|
|
|
721
484
|
fileOps: unknown;
|
|
722
485
|
},
|
|
723
486
|
branchEntries: SessionEntry[],
|
|
724
|
-
|
|
725
|
-
|
|
487
|
+
keep?: number,
|
|
488
|
+
): CompactionResult<GoosedumpCompactionDetails> | { cancel: true; reason: string } | undefined {
|
|
489
|
+
// A `keep:N` override replaces Pi's token-based cut with a turn-based one:
|
|
490
|
+
// summarize everything before the Nth-from-last user entry and keep the tail.
|
|
491
|
+
let firstKeptEntryId = preparation.firstKeptEntryId;
|
|
492
|
+
if (keep !== undefined && keep > 0) {
|
|
493
|
+
const id = keepBoundaryId(branchEntries, keep);
|
|
494
|
+
if (!id) {
|
|
495
|
+
return { cancel: true, reason: `fewer than ${keep} user turns; nothing to compact` };
|
|
496
|
+
}
|
|
497
|
+
firstKeptEntryId = id;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const range = buildCompactRange(branchEntries, firstKeptEntryId);
|
|
726
501
|
if (!range) return undefined;
|
|
727
502
|
|
|
728
|
-
const summary = goosedumpCompact(
|
|
729
|
-
|
|
730
|
-
if (!merged.summary) return undefined;
|
|
503
|
+
const summary = goosedumpCompact(target, range);
|
|
504
|
+
if (!summary) return undefined;
|
|
731
505
|
|
|
732
506
|
return {
|
|
733
|
-
summary
|
|
734
|
-
firstKeptEntryId
|
|
507
|
+
summary,
|
|
508
|
+
firstKeptEntryId,
|
|
735
509
|
tokensBefore: preparation.tokensBefore,
|
|
736
510
|
details: {
|
|
737
511
|
source: 'goosedump',
|
|
738
|
-
|
|
739
|
-
contextId,
|
|
512
|
+
target,
|
|
740
513
|
scope: range.scope,
|
|
741
514
|
from: range.from,
|
|
742
515
|
until: range.until,
|
|
743
|
-
ids: range.ids,
|
|
744
|
-
previousSummaryIncluded: merged.previousSummaryIncluded,
|
|
745
516
|
fileOps: preparation.fileOps,
|
|
746
517
|
},
|
|
747
518
|
};
|
|
748
519
|
}
|
|
749
520
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
521
|
+
function isControlChar(char: string): boolean {
|
|
522
|
+
const code = char.charCodeAt(0);
|
|
523
|
+
return code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
class NumberInputSubmenu implements Component {
|
|
527
|
+
private readonly input = new Input();
|
|
528
|
+
private readonly hint: (text: string) => string;
|
|
529
|
+
private readonly onSubmitValue: (value: string) => void;
|
|
530
|
+
private readonly onCancelEdit: () => void;
|
|
531
|
+
private readonly title: string;
|
|
532
|
+
|
|
533
|
+
constructor(
|
|
534
|
+
title: string,
|
|
535
|
+
initialValue: string,
|
|
536
|
+
onSubmit: (value: string) => void,
|
|
537
|
+
onCancel: () => void,
|
|
538
|
+
) {
|
|
539
|
+
this.title = title;
|
|
540
|
+
this.onSubmitValue = onSubmit;
|
|
541
|
+
this.onCancelEdit = onCancel;
|
|
542
|
+
this.input.setValue(initialValue);
|
|
543
|
+
this.hint = getSettingsListTheme().hint;
|
|
764
544
|
}
|
|
765
545
|
|
|
766
|
-
|
|
767
|
-
|
|
546
|
+
handleInput(data: string): void {
|
|
547
|
+
const kb = getKeybindings();
|
|
548
|
+
if (kb.matches(data, 'tui.select.confirm') || data === '\n') {
|
|
549
|
+
const digits = this.input.getValue().replace(/\D/g, '');
|
|
550
|
+
const parsed = Number(digits);
|
|
551
|
+
this.onSubmitValue(digits !== '' && Number.isSafeInteger(parsed) ? String(parsed) : '0');
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (kb.matches(data, 'tui.select.cancel')) {
|
|
555
|
+
this.onCancelEdit();
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const kittyChar = decodeKittyPrintable(data);
|
|
559
|
+
const printable = kittyChar ?? (data.length === 1 && !isControlChar(data) ? data : undefined);
|
|
560
|
+
if (printable !== undefined && !/^[0-9]$/.test(printable)) return;
|
|
561
|
+
this.input.handleInput(data);
|
|
768
562
|
}
|
|
769
563
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
const head = Math.floor(maxLen / 2) + 2;
|
|
773
|
-
const tail = maxLen - head - 3;
|
|
774
|
-
if (tail < 4) return `${path.slice(0, maxLen - 3)}...`;
|
|
775
|
-
return `${path.slice(0, head)}...${path.slice(path.length - tail)}`;
|
|
564
|
+
invalidate(): void {
|
|
565
|
+
this.input.invalidate();
|
|
776
566
|
}
|
|
777
567
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
568
|
+
render(width: number): string[] {
|
|
569
|
+
return [
|
|
570
|
+
this.hint(` ${this.title}`),
|
|
571
|
+
'',
|
|
572
|
+
...this.input.render(width),
|
|
573
|
+
'',
|
|
574
|
+
this.hint(' enter submit · esc cancel'),
|
|
575
|
+
];
|
|
782
576
|
}
|
|
577
|
+
}
|
|
783
578
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
description:
|
|
794
|
-
key === 'stepsBeforeCompact'
|
|
795
|
-
? 'Tool steps before goosedump starts compaction; 0 disables this trigger.'
|
|
796
|
-
: 'LLM turns before goosedump starts compaction; 0 disables this trigger.',
|
|
797
|
-
currentValue: String(settingsByScope[scope][key]),
|
|
798
|
-
values: settingValues,
|
|
799
|
-
})),
|
|
800
|
-
);
|
|
801
|
-
}
|
|
579
|
+
function makeRow(content: string, innerW: number, border: string): string {
|
|
580
|
+
const line = truncateToWidth(content, innerW);
|
|
581
|
+
const pad = Math.max(0, innerW - visibleWidth(line));
|
|
582
|
+
return `${border} ${line}${' '.repeat(pad)} ${border}`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function runSettingsEditor(ctx: ExtensionContext): Promise<void> {
|
|
586
|
+
const OVERLAY_WIDTH = 84;
|
|
587
|
+
const location = ctx.cwd;
|
|
802
588
|
|
|
803
589
|
await ctx.ui.custom<void>(
|
|
804
590
|
(tui, theme, _kb, done) => {
|
|
805
|
-
let tab: 'sessions' | 'settings' = 'sessions';
|
|
806
|
-
let selectedIndex = 0;
|
|
807
|
-
let listScroll = 0;
|
|
808
|
-
let mode: 'list' | 'compact' = 'list';
|
|
809
|
-
let compactLines: string[] = [];
|
|
810
|
-
let compactScroll = 0;
|
|
811
|
-
let compactTitle = '';
|
|
812
|
-
let pendingDeletionId: string | null = null;
|
|
813
591
|
let settingsList: SettingsList;
|
|
814
592
|
|
|
815
|
-
|
|
593
|
+
function settingLabel(scope: SettingsScope, key: GoosedumpSettingKey): string {
|
|
594
|
+
const scopeLabel = scope === 'global' ? 'Global' : 'Project';
|
|
595
|
+
return `${scopeLabel} ${key}`;
|
|
596
|
+
}
|
|
816
597
|
|
|
817
|
-
function
|
|
818
|
-
const
|
|
819
|
-
|
|
820
|
-
|
|
598
|
+
function buildSettingItems(): SettingItem[] {
|
|
599
|
+
const global = readScopedGoosedumpSettings(location, 'global');
|
|
600
|
+
const project = readScopedGoosedumpSettings(location, 'project');
|
|
601
|
+
const settingsByScope = { global, project } satisfies Record<
|
|
602
|
+
SettingsScope,
|
|
603
|
+
GoosedumpSettings
|
|
604
|
+
>;
|
|
605
|
+
|
|
606
|
+
return (['global', 'project'] as const).map((scope) => ({
|
|
607
|
+
id: `${scope}:turnsBeforeCompact`,
|
|
608
|
+
label: settingLabel(scope, 'turnsBeforeCompact'),
|
|
609
|
+
description: 'LLM turns before compaction; 0 disables.',
|
|
610
|
+
currentValue: String(settingsByScope[scope].turnsBeforeCompact),
|
|
611
|
+
submenu: (currentValue, subDone) =>
|
|
612
|
+
new NumberInputSubmenu(
|
|
613
|
+
settingLabel(scope, 'turnsBeforeCompact'),
|
|
614
|
+
currentValue,
|
|
615
|
+
(value) => subDone(value),
|
|
616
|
+
() => subDone(),
|
|
617
|
+
),
|
|
618
|
+
}));
|
|
821
619
|
}
|
|
822
620
|
|
|
823
621
|
function rebuildSettingsList(): void {
|
|
@@ -828,305 +626,266 @@ async function runSessionBrowser(
|
|
|
828
626
|
(id, newValue) => {
|
|
829
627
|
const [scope, key] = id.split(':') as [SettingsScope, GoosedumpSettingKey];
|
|
830
628
|
const parsed = Number(newValue);
|
|
831
|
-
if (!Number.isSafeInteger(parsed) || parsed < 0) return;
|
|
629
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > 255) return;
|
|
832
630
|
writeScopedGoosedumpSetting(location, scope, key, parsed);
|
|
833
631
|
settingsList.updateValue(id, newValue);
|
|
834
632
|
ctx.ui.notify(`${settingLabel(scope, key)} = ${formatCompactSetting(parsed)}`, 'info');
|
|
835
633
|
tui.requestRender();
|
|
836
634
|
},
|
|
837
635
|
() => {
|
|
838
|
-
|
|
839
|
-
tui.requestRender();
|
|
636
|
+
done(undefined);
|
|
840
637
|
},
|
|
841
638
|
);
|
|
842
639
|
}
|
|
843
640
|
|
|
844
641
|
rebuildSettingsList();
|
|
845
642
|
|
|
846
|
-
|
|
643
|
+
return {
|
|
644
|
+
render(width: number): string[] {
|
|
645
|
+
const innerW = width - 4;
|
|
646
|
+
const border = theme.fg('border', '│');
|
|
647
|
+
const borderFg = (s: string) => theme.fg('border', s);
|
|
648
|
+
const accent = (s: string) => theme.fg('accent', s);
|
|
649
|
+
const dim = (s: string) => theme.fg('dim', s);
|
|
650
|
+
const lines: string[] = [];
|
|
651
|
+
const title = accent(' Goosedump Settings ');
|
|
652
|
+
const topFill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(title))));
|
|
653
|
+
lines.push(`${borderFg('╭─')}${title}${topFill}${borderFg('─╮')}`);
|
|
654
|
+
for (const line of settingsList.render(innerW)) lines.push(makeRow(line, innerW, border));
|
|
655
|
+
lines.push(makeRow('', innerW, border));
|
|
656
|
+
const effective = resolveGoosedumpSettings(location);
|
|
657
|
+
lines.push(
|
|
658
|
+
makeRow(
|
|
659
|
+
dim(`Effective: turns=${formatCompactSetting(effective.turnsBeforeCompact)}`),
|
|
660
|
+
innerW,
|
|
661
|
+
border,
|
|
662
|
+
),
|
|
663
|
+
);
|
|
664
|
+
lines.push(makeRow(dim('↑↓ select enter/space edit esc close'), innerW, border));
|
|
665
|
+
lines.push(`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`);
|
|
666
|
+
return lines;
|
|
667
|
+
},
|
|
668
|
+
handleInput(data: string): void {
|
|
669
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
|
|
670
|
+
done(undefined);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
settingsList.handleInput(data);
|
|
674
|
+
tui.requestRender();
|
|
675
|
+
},
|
|
676
|
+
invalidate(): void {},
|
|
677
|
+
};
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
overlay: true,
|
|
681
|
+
overlayOptions: {
|
|
682
|
+
anchor: 'center',
|
|
683
|
+
width: OVERLAY_WIDTH,
|
|
684
|
+
margin: 2,
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function runSearchPanel(ctx: ExtensionContext, query: string): Promise<void> {
|
|
691
|
+
const OVERLAY_WIDTH = 84;
|
|
692
|
+
const VIEWPORT = 18;
|
|
693
|
+
const PAGE_SIZE = 5;
|
|
694
|
+
const wrapWidth = OVERLAY_WIDTH - 4;
|
|
695
|
+
|
|
696
|
+
await ctx.ui.custom<void>(
|
|
697
|
+
(tui, theme, _kb, done) => {
|
|
698
|
+
const target = currentSessionTarget(ctx);
|
|
699
|
+
const borderFg = (s: string) => theme.fg('border', s);
|
|
700
|
+
const border = theme.fg('border', '│');
|
|
701
|
+
const accent = (s: string) => theme.fg('accent', s);
|
|
702
|
+
const text = (s: string) => theme.fg('text', s);
|
|
703
|
+
const dim = (s: string) => theme.fg('dim', s);
|
|
704
|
+
const muted = (s: string) => theme.fg('muted', s);
|
|
705
|
+
|
|
706
|
+
let entries: GoosedumpMessage[] = [];
|
|
707
|
+
let error: string | null = null;
|
|
708
|
+
let mode: 'results' | 'detail' = 'results';
|
|
709
|
+
let selected = 0;
|
|
710
|
+
let scroll = 0;
|
|
711
|
+
let detailLines: string[] = [];
|
|
712
|
+
let detailScroll = 0;
|
|
713
|
+
|
|
714
|
+
function runSearch(): void {
|
|
847
715
|
try {
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
716
|
+
const first = goosedumpSearch(target!, query, { scope: 'lineage', page: 1 });
|
|
717
|
+
if (first.length === 0) {
|
|
718
|
+
entries = [];
|
|
719
|
+
error = `No results for "${query}"`;
|
|
720
|
+
selected = 0;
|
|
721
|
+
scroll = 0;
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const all: GoosedumpMessage[] = [...first];
|
|
726
|
+
for (let page = 2; page <= 10; page += 1) {
|
|
727
|
+
const result = goosedumpSearch(target!, query, { scope: 'lineage', page });
|
|
728
|
+
if (result.length === 0) break;
|
|
729
|
+
all.push(...result);
|
|
730
|
+
if (result.length < PAGE_SIZE) break;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
entries = all;
|
|
734
|
+
error = null;
|
|
735
|
+
selected = 0;
|
|
736
|
+
scroll = 0;
|
|
853
737
|
} catch (err) {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
pendingDeletionId = null;
|
|
738
|
+
entries = [];
|
|
739
|
+
error = err instanceof Error ? err.message : String(err);
|
|
857
740
|
}
|
|
858
|
-
tui.requestRender();
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
function makeRow(content: string, innerW: number, border: string): string {
|
|
862
|
-
const line = truncateToWidth(content, innerW);
|
|
863
|
-
const pad = Math.max(0, innerW - visibleWidth(line));
|
|
864
|
-
return `${border} ${line}${' '.repeat(pad)} ${border}`;
|
|
865
741
|
}
|
|
866
742
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
743
|
+
if (!target) {
|
|
744
|
+
error = 'No current Pi session available.';
|
|
745
|
+
} else {
|
|
746
|
+
runSearch();
|
|
871
747
|
}
|
|
872
748
|
|
|
873
|
-
function
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
dim('Settings are written to ~/.pi/agent/settings.json and .pi/settings.json.'),
|
|
885
|
-
innerW,
|
|
886
|
-
border,
|
|
887
|
-
),
|
|
888
|
-
);
|
|
889
|
-
lines.push(makeRow('', innerW, border));
|
|
890
|
-
for (const line of settingsList.render(innerW)) lines.push(makeRow(line, innerW, border));
|
|
891
|
-
lines.push(makeRow('', innerW, border));
|
|
892
|
-
const effective = resolveGoosedumpSettings(location);
|
|
893
|
-
lines.push(
|
|
894
|
-
makeRow(
|
|
895
|
-
dim(
|
|
896
|
-
`Effective: steps=${formatCompactSetting(effective.stepsBeforeCompact)} turns=${formatCompactSetting(effective.turnsBeforeCompact)}`,
|
|
897
|
-
),
|
|
898
|
-
innerW,
|
|
899
|
-
border,
|
|
900
|
-
),
|
|
901
|
-
);
|
|
902
|
-
lines.push(
|
|
903
|
-
makeRow(dim('tab switch ↑↓ select enter/space cycle esc close'), innerW, border),
|
|
904
|
-
);
|
|
905
|
-
lines.push(`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`);
|
|
906
|
-
return lines;
|
|
749
|
+
function loadDetail(id: string): void {
|
|
750
|
+
try {
|
|
751
|
+
const messages = goosedumpShow(target!, { ids: [id] });
|
|
752
|
+
const content = messages.length > 0 ? messages[0].content : 'No content for this entry.';
|
|
753
|
+
detailLines = wrapTextWithAnsi(content, wrapWidth);
|
|
754
|
+
detailScroll = 0;
|
|
755
|
+
mode = 'detail';
|
|
756
|
+
} catch (err) {
|
|
757
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
758
|
+
ctx.ui.notify(`goosedump error: ${message}`, 'error');
|
|
759
|
+
}
|
|
907
760
|
}
|
|
908
761
|
|
|
909
762
|
return {
|
|
910
763
|
render(width: number): string[] {
|
|
911
764
|
const innerW = width - 4;
|
|
912
|
-
const
|
|
913
|
-
const
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
const text = (s: string) => theme.fg('text', s);
|
|
917
|
-
const muted = (s: string) => theme.fg('muted', s);
|
|
765
|
+
const lines: string[] = [];
|
|
766
|
+
const title = accent(` Goosedump search: ${truncateToWidth(query, 44)} `);
|
|
767
|
+
const topFill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(title))));
|
|
768
|
+
lines.push(`${borderFg('╭─')}${title}${topFill}${borderFg('─╮')}`);
|
|
918
769
|
|
|
919
|
-
if (
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
lines
|
|
770
|
+
if (error) {
|
|
771
|
+
lines.push(makeRow(muted(error), innerW, border));
|
|
772
|
+
lines.push(makeRow(dim('esc close'), innerW, border));
|
|
773
|
+
lines.push(`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`);
|
|
774
|
+
return lines;
|
|
775
|
+
}
|
|
924
776
|
|
|
925
|
-
|
|
926
|
-
const
|
|
777
|
+
if (mode === 'detail') {
|
|
778
|
+
const total = detailLines.length;
|
|
779
|
+
const window = detailLines.slice(detailScroll, detailScroll + VIEWPORT);
|
|
927
780
|
for (const row of window) lines.push(makeRow(text(row), innerW, border));
|
|
928
|
-
for (let i = window.length; i <
|
|
929
|
-
lines.push(makeRow('', innerW, border));
|
|
930
|
-
|
|
781
|
+
for (let i = window.length; i < VIEWPORT; i++) lines.push(makeRow('', innerW, border));
|
|
931
782
|
const position =
|
|
932
|
-
total >
|
|
933
|
-
? `${
|
|
783
|
+
total > VIEWPORT
|
|
784
|
+
? `${detailScroll + 1}-${Math.min(detailScroll + VIEWPORT, total)} of ${total}`
|
|
934
785
|
: `${total} line${total === 1 ? '' : 's'}`;
|
|
935
786
|
lines.push(makeRow('', innerW, border));
|
|
936
|
-
lines.push(makeRow(dim(
|
|
787
|
+
lines.push(makeRow(dim(`esc back ↑↓ scroll (${position})`), innerW, border));
|
|
937
788
|
lines.push(`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`);
|
|
938
789
|
return lines;
|
|
939
790
|
}
|
|
940
791
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
const
|
|
944
|
-
const lines: string[] = [];
|
|
945
|
-
const title = accent(' Goosedump Dashboard ');
|
|
946
|
-
const topFill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(title))));
|
|
947
|
-
lines.push(`${borderFg('╭─')}${title}${topFill}${borderFg('─╮')}`);
|
|
948
|
-
lines.push(renderTabs(innerW, border));
|
|
949
|
-
lines.push(
|
|
950
|
-
makeRow(muted(`Location: ${shortPath(location, innerW - 10)}`), innerW, border),
|
|
951
|
-
);
|
|
952
|
-
lines.push(
|
|
953
|
-
makeRow(
|
|
954
|
-
muted(
|
|
955
|
-
`${currentListings.length} session${currentListings.length === 1 ? '' : 's'} here`,
|
|
956
|
-
),
|
|
957
|
-
innerW,
|
|
958
|
-
border,
|
|
959
|
-
),
|
|
960
|
-
);
|
|
961
|
-
lines.push(makeRow('', innerW, border));
|
|
962
|
-
|
|
963
|
-
const window = currentListings.slice(listScroll, listScroll + LIST_VIEWPORT);
|
|
792
|
+
const maxScroll = Math.max(0, entries.length - VIEWPORT);
|
|
793
|
+
const clampScroll = Math.min(scroll, maxScroll);
|
|
794
|
+
const window = entries.slice(clampScroll, clampScroll + VIEWPORT);
|
|
964
795
|
for (let i = 0; i < window.length; i++) {
|
|
965
|
-
const absolute =
|
|
966
|
-
const
|
|
967
|
-
const isSelected = absolute ===
|
|
968
|
-
const
|
|
969
|
-
const
|
|
970
|
-
const
|
|
971
|
-
? dim(listing.id)
|
|
972
|
-
: isSelected
|
|
973
|
-
? accent(listing.id)
|
|
974
|
-
: text(listing.id);
|
|
975
|
-
const label = listing.label ? ` ${listing.label}` : '';
|
|
976
|
-
const count = muted(
|
|
977
|
-
` (${listing.provider}, ${listing.path ? 'resumable' : 'no file'})`,
|
|
978
|
-
);
|
|
979
|
-
lines.push(makeRow(` ${cursor} ${idText}${muted(label)}${count}`, innerW, border));
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
for (let i = window.length; i < LIST_VIEWPORT; i++)
|
|
983
|
-
lines.push(makeRow('', innerW, border));
|
|
984
|
-
|
|
985
|
-
if (currentListings.length > LIST_VIEWPORT) {
|
|
986
|
-
const position = `${listScroll + 1}-${Math.min(listScroll + LIST_VIEWPORT, currentListings.length)} of ${currentListings.length}`;
|
|
987
|
-
lines.push(makeRow(dim(`(${position})`), innerW, border));
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
if (pendingDeletionId) {
|
|
991
|
-
lines.push(makeRow('', innerW, border));
|
|
796
|
+
const absolute = clampScroll + i;
|
|
797
|
+
const entry = window[i];
|
|
798
|
+
const isSelected = absolute === selected;
|
|
799
|
+
const cursor = isSelected ? accent('▶') : ' ';
|
|
800
|
+
const idText = isSelected ? accent(entry.id) : text(entry.id);
|
|
801
|
+
const snippet = compactLine(entry.content);
|
|
992
802
|
lines.push(
|
|
993
|
-
makeRow(
|
|
994
|
-
dim(`Delete session ${pendingDeletionId}? enter=confirm esc=cancel`),
|
|
995
|
-
innerW,
|
|
996
|
-
border,
|
|
997
|
-
),
|
|
803
|
+
makeRow(` ${cursor} ${idText} ${muted(entry.role)} ${dim(snippet)}`, innerW, border),
|
|
998
804
|
);
|
|
999
805
|
}
|
|
1000
|
-
|
|
806
|
+
for (let i = window.length; i < VIEWPORT; i++) lines.push(makeRow('', innerW, border));
|
|
807
|
+
const count = entries.length;
|
|
808
|
+
const position =
|
|
809
|
+
count > VIEWPORT
|
|
810
|
+
? `${clampScroll + 1}-${Math.min(clampScroll + VIEWPORT, count)} of ${count}`
|
|
811
|
+
: `${count} entr${count === 1 ? 'y' : 'ies'}`;
|
|
1001
812
|
lines.push(makeRow('', innerW, border));
|
|
1002
813
|
lines.push(
|
|
1003
|
-
makeRow(
|
|
1004
|
-
dim(
|
|
1005
|
-
'tab settings ↑↓ navigate enter compact shift+enter resume ctrl+d delete esc close',
|
|
1006
|
-
),
|
|
1007
|
-
innerW,
|
|
1008
|
-
border,
|
|
1009
|
-
),
|
|
814
|
+
makeRow(dim(`↑↓ navigate enter expand esc close (${position})`), innerW, border),
|
|
1010
815
|
);
|
|
1011
816
|
lines.push(`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`);
|
|
1012
817
|
return lines;
|
|
1013
818
|
},
|
|
1014
|
-
|
|
1015
819
|
handleInput(data: string): void {
|
|
1016
|
-
if (
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
mode = 'list';
|
|
820
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
|
|
821
|
+
if (mode === 'detail') {
|
|
822
|
+
mode = 'results';
|
|
1020
823
|
tui.requestRender();
|
|
1021
824
|
return;
|
|
1022
825
|
}
|
|
826
|
+
done(undefined);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (error) return;
|
|
830
|
+
|
|
831
|
+
if (mode === 'detail') {
|
|
832
|
+
const maxScroll = Math.max(0, detailLines.length - VIEWPORT);
|
|
1023
833
|
if (matchesKey(data, Key.up)) {
|
|
1024
|
-
|
|
834
|
+
detailScroll = Math.max(0, detailScroll - 1);
|
|
1025
835
|
tui.requestRender();
|
|
1026
836
|
return;
|
|
1027
837
|
}
|
|
1028
838
|
if (matchesKey(data, Key.down)) {
|
|
1029
|
-
|
|
839
|
+
detailScroll = Math.min(maxScroll, detailScroll + 1);
|
|
1030
840
|
tui.requestRender();
|
|
1031
841
|
return;
|
|
1032
842
|
}
|
|
1033
843
|
if (matchesKey(data, Key.pageUp)) {
|
|
1034
|
-
|
|
844
|
+
detailScroll = Math.max(0, detailScroll - VIEWPORT);
|
|
1035
845
|
tui.requestRender();
|
|
1036
846
|
return;
|
|
1037
847
|
}
|
|
1038
848
|
if (matchesKey(data, Key.pageDown)) {
|
|
1039
|
-
|
|
849
|
+
detailScroll = Math.min(maxScroll, detailScroll + VIEWPORT);
|
|
1040
850
|
tui.requestRender();
|
|
1041
851
|
return;
|
|
1042
852
|
}
|
|
1043
853
|
return;
|
|
1044
854
|
}
|
|
1045
855
|
|
|
1046
|
-
|
|
1047
|
-
tab = tab === 'sessions' ? 'settings' : 'sessions';
|
|
1048
|
-
if (tab === 'settings') rebuildSettingsList();
|
|
1049
|
-
tui.requestRender();
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
if (tab === 'settings') {
|
|
1054
|
-
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
|
|
1055
|
-
done(undefined);
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
settingsList.handleInput(data);
|
|
1059
|
-
tui.requestRender();
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
|
|
1064
|
-
done(undefined);
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
const currentListings = locationListings();
|
|
856
|
+
const maxScroll = Math.max(0, entries.length - VIEWPORT);
|
|
1069
857
|
if (matchesKey(data, Key.up)) {
|
|
1070
|
-
|
|
1071
|
-
if (
|
|
858
|
+
selected = Math.max(0, selected - 1);
|
|
859
|
+
if (selected < scroll) scroll = selected;
|
|
1072
860
|
tui.requestRender();
|
|
1073
861
|
return;
|
|
1074
862
|
}
|
|
1075
|
-
|
|
1076
863
|
if (matchesKey(data, Key.down)) {
|
|
1077
|
-
|
|
1078
|
-
if (
|
|
1079
|
-
listScroll = selectedIndex - LIST_VIEWPORT + 1;
|
|
1080
|
-
}
|
|
864
|
+
selected = Math.min(Math.max(0, entries.length - 1), selected + 1);
|
|
865
|
+
if (selected >= scroll + VIEWPORT) scroll = selected - VIEWPORT + 1;
|
|
1081
866
|
tui.requestRender();
|
|
1082
867
|
return;
|
|
1083
868
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
pendingDeletionId = null;
|
|
1088
|
-
tui.requestRender();
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
|
|
1092
|
-
executeDeletion();
|
|
1093
|
-
return;
|
|
1094
|
-
}
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
if (matchesKey(data, Key.ctrl('d'))) {
|
|
1099
|
-
const listing = currentListings[selectedIndex];
|
|
1100
|
-
if (listing) pendingDeletionId = listing.id;
|
|
869
|
+
if (matchesKey(data, Key.pageUp)) {
|
|
870
|
+
selected = Math.max(0, selected - VIEWPORT);
|
|
871
|
+
scroll = Math.max(0, Math.min(scroll, selected));
|
|
1101
872
|
tui.requestRender();
|
|
1102
873
|
return;
|
|
1103
874
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
875
|
+
if (matchesKey(data, Key.pageDown)) {
|
|
876
|
+
selected = Math.min(entries.length - 1, selected + VIEWPORT);
|
|
877
|
+
scroll = Math.min(maxScroll, Math.max(scroll, selected - VIEWPORT + 1));
|
|
878
|
+
tui.requestRender();
|
|
1108
879
|
return;
|
|
1109
880
|
}
|
|
1110
|
-
|
|
1111
881
|
if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
|
|
1112
|
-
const
|
|
1113
|
-
if (!
|
|
1114
|
-
|
|
1115
|
-
compactScroll = 0;
|
|
1116
|
-
try {
|
|
1117
|
-
const summary = goosedumpCompact(listing.id, { scope: 'lineage' }).trim();
|
|
1118
|
-
compactLines = summary
|
|
1119
|
-
? wrapTextWithAnsi(summary, wrapWidth)
|
|
1120
|
-
: ['No compact summary available for this session.'];
|
|
1121
|
-
} catch (err) {
|
|
1122
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1123
|
-
compactLines = wrapTextWithAnsi(`Failed to compact session: ${msg}`, wrapWidth);
|
|
1124
|
-
}
|
|
1125
|
-
mode = 'compact';
|
|
882
|
+
const entry = entries[selected];
|
|
883
|
+
if (!entry) return;
|
|
884
|
+
loadDetail(entry.id);
|
|
1126
885
|
tui.requestRender();
|
|
886
|
+
return;
|
|
1127
887
|
}
|
|
1128
888
|
},
|
|
1129
|
-
|
|
1130
889
|
invalidate(): void {},
|
|
1131
890
|
};
|
|
1132
891
|
},
|
|
@@ -1139,8 +898,6 @@ async function runSessionBrowser(
|
|
|
1139
898
|
},
|
|
1140
899
|
},
|
|
1141
900
|
);
|
|
1142
|
-
|
|
1143
|
-
return resumePath;
|
|
1144
901
|
}
|
|
1145
902
|
|
|
1146
903
|
export function createGoosedumpIntegration(
|
|
@@ -1152,21 +909,21 @@ export function createGoosedumpIntegration(
|
|
|
1152
909
|
let goosedumpAvailable = false;
|
|
1153
910
|
let compactSettings = DEFAULT_GOOSEDUMP_SETTINGS;
|
|
1154
911
|
let settingsSignature = '';
|
|
1155
|
-
let stepsRemaining = 0;
|
|
1156
912
|
let turnsRemaining = 0;
|
|
1157
913
|
let counterTriggered = false;
|
|
1158
914
|
let compactionQueued = false;
|
|
915
|
+
let pendingKeep: number | null = null;
|
|
1159
916
|
|
|
1160
917
|
function compactSettingsSignature(settings: GoosedumpSettings): string {
|
|
1161
|
-
return
|
|
918
|
+
return String(settings.turnsBeforeCompact);
|
|
1162
919
|
}
|
|
1163
920
|
|
|
1164
921
|
function resetCompactCounters(cwd: string): void {
|
|
1165
922
|
compactSettings = resolveGoosedumpSettings(cwd);
|
|
1166
923
|
settingsSignature = compactSettingsSignature(compactSettings);
|
|
1167
|
-
stepsRemaining = compactSettings.stepsBeforeCompact;
|
|
1168
924
|
turnsRemaining = compactSettings.turnsBeforeCompact;
|
|
1169
925
|
counterTriggered = false;
|
|
926
|
+
pendingKeep = null;
|
|
1170
927
|
}
|
|
1171
928
|
|
|
1172
929
|
function reloadCompactSettings(cwd: string): void {
|
|
@@ -1175,13 +932,13 @@ export function createGoosedumpIntegration(
|
|
|
1175
932
|
if (nextSignature !== settingsSignature) resetCompactCounters(cwd);
|
|
1176
933
|
}
|
|
1177
934
|
|
|
1178
|
-
function
|
|
1179
|
-
if (!goosedumpAvailable
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
935
|
+
function updateStatus(ctx: ExtensionContext): void {
|
|
936
|
+
if (!goosedumpAvailable) return;
|
|
937
|
+
const showCount = shouldOverrideDefaultCompaction && compactSettings.turnsBeforeCompact > 0;
|
|
938
|
+
const suffix = showCount
|
|
939
|
+
? `: ${Math.max(0, turnsRemaining).toString(16).toUpperCase().padStart(2, '0')}`
|
|
940
|
+
: '';
|
|
941
|
+
ctx.ui.setStatus('goosedump', ctx.ui.theme.fg('text', `GD${suffix}`));
|
|
1185
942
|
}
|
|
1186
943
|
|
|
1187
944
|
function noteCompletedTurn(ctx: ExtensionContext): void {
|
|
@@ -1191,18 +948,26 @@ export function createGoosedumpIntegration(
|
|
|
1191
948
|
turnsRemaining -= 1;
|
|
1192
949
|
if (turnsRemaining <= 0) counterTriggered = true;
|
|
1193
950
|
}
|
|
951
|
+
updateStatus(ctx);
|
|
1194
952
|
|
|
1195
953
|
if (!counterTriggered || compactionQueued) return;
|
|
954
|
+
// ctx.compact() uses manual compaction semantics and aborts the current run.
|
|
955
|
+
// Wait until no queued work remains before triggering plugin-driven compaction.
|
|
956
|
+
if (ctx.hasPendingMessages()) return;
|
|
1196
957
|
|
|
1197
958
|
resetCompactCounters(ctx.cwd);
|
|
959
|
+
updateStatus(ctx);
|
|
1198
960
|
compactionQueued = true;
|
|
1199
961
|
ctx.compact({
|
|
1200
962
|
onComplete: () => {
|
|
1201
963
|
compactionQueued = false;
|
|
1202
964
|
resetCompactCounters(ctx.cwd);
|
|
965
|
+
updateStatus(ctx);
|
|
1203
966
|
},
|
|
1204
967
|
onError: (error) => {
|
|
1205
968
|
compactionQueued = false;
|
|
969
|
+
resetCompactCounters(ctx.cwd);
|
|
970
|
+
updateStatus(ctx);
|
|
1206
971
|
const message = error instanceof Error ? error.message : String(error);
|
|
1207
972
|
if (ctx.hasUI) ctx.ui.notify(`goosedump compact failed: ${message}`, 'error');
|
|
1208
973
|
},
|
|
@@ -1210,8 +975,10 @@ export function createGoosedumpIntegration(
|
|
|
1210
975
|
}
|
|
1211
976
|
|
|
1212
977
|
function checkGoosedump(ctx?: ExtensionContext): boolean {
|
|
1213
|
-
|
|
1214
|
-
|
|
978
|
+
try {
|
|
979
|
+
resolveGoosedumpBinary();
|
|
980
|
+
return true;
|
|
981
|
+
} catch {
|
|
1215
982
|
if (ctx) {
|
|
1216
983
|
ctx.ui.notify(
|
|
1217
984
|
'goosedump was not found. Install with: npm install @jarkkojs/goosedump',
|
|
@@ -1220,84 +987,74 @@ export function createGoosedumpIntegration(
|
|
|
1220
987
|
}
|
|
1221
988
|
return false;
|
|
1222
989
|
}
|
|
990
|
+
}
|
|
1223
991
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
992
|
+
// Shared trigger for on-demand goosedump compaction (from the /goose-compact
|
|
993
|
+
// command and the goose_compact tool). Stashes a turn-based keep override and
|
|
994
|
+
// fires Pi's compaction flow; the session_before_compact hook reads pendingKeep
|
|
995
|
+
// and resets it on use, completion, or error.
|
|
996
|
+
function requestCompaction(ctx: ExtensionContext, keep: number | null): void {
|
|
997
|
+
pendingKeep = keep;
|
|
998
|
+
ctx.compact({
|
|
999
|
+
onComplete: () => {
|
|
1000
|
+
pendingKeep = null;
|
|
1001
|
+
},
|
|
1002
|
+
onError: (error) => {
|
|
1003
|
+
pendingKeep = null;
|
|
1004
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1005
|
+
if (ctx.hasUI) ctx.ui.notify(`goosedump compact failed: ${message}`, 'error');
|
|
1006
|
+
},
|
|
1007
|
+
});
|
|
1235
1008
|
}
|
|
1236
1009
|
|
|
1237
1010
|
function register(pi: ExtensionAPI): void {
|
|
1238
1011
|
if (shouldRegisterTool) {
|
|
1239
1012
|
pi.registerTool(
|
|
1240
1013
|
defineTool({
|
|
1241
|
-
name: '
|
|
1242
|
-
label: '
|
|
1014
|
+
name: 'goose_search',
|
|
1015
|
+
label: 'goose_search',
|
|
1243
1016
|
description:
|
|
1244
|
-
|
|
1017
|
+
'Search coding agent session history by ranked relevance. Returns compact entry overviews with entry IDs; use goose_get to expand specific IDs to full content. Defaults to the current Pi session.',
|
|
1245
1018
|
promptSnippet:
|
|
1246
|
-
'
|
|
1019
|
+
'goose_search({ query, provider?, session?, scope?, page? }) - rank messages by query relevance; defaults to current Pi session',
|
|
1247
1020
|
promptGuidelines: [
|
|
1248
|
-
|
|
1249
|
-
'Start with compact ranked
|
|
1021
|
+
"Use goose_search to find relevant context in this or another coding agent's session history.",
|
|
1022
|
+
'Start with compact ranked results, then use goose_get to expand interesting entry IDs to full content.',
|
|
1250
1023
|
'Default scope is "lineage" (current branch); use scope: "all" to include all entries in the session.',
|
|
1251
|
-
'Omit
|
|
1252
|
-
'Set provider (e.g. "claude", "gemini") with a contextId to browse another tool\'s sessions; results render the same way as Pi sessions.',
|
|
1253
|
-
'Set outputFormat (e.g. "claude", "json") only when you explicitly need the raw native session instead of the rendered view.',
|
|
1024
|
+
'Omit session to search the current Pi session; set provider (e.g. "claude", "gemini") with a session id to browse another tool\'s sessions.',
|
|
1254
1025
|
],
|
|
1255
1026
|
parameters: Type.Object({
|
|
1256
|
-
|
|
1027
|
+
query: Type.String({ description: 'Search query to rank messages by relevance' }),
|
|
1028
|
+
provider: Type.Optional(
|
|
1257
1029
|
Type.Union(
|
|
1258
1030
|
[
|
|
1259
|
-
Type.Literal('
|
|
1260
|
-
Type.Literal('
|
|
1261
|
-
Type.Literal('
|
|
1262
|
-
Type.Literal('
|
|
1263
|
-
Type.Literal('
|
|
1031
|
+
Type.Literal('pi'),
|
|
1032
|
+
Type.Literal('claude'),
|
|
1033
|
+
Type.Literal('codex'),
|
|
1034
|
+
Type.Literal('crush'),
|
|
1035
|
+
Type.Literal('gemini'),
|
|
1036
|
+
Type.Literal('goose'),
|
|
1037
|
+
Type.Literal('opencode'),
|
|
1264
1038
|
],
|
|
1265
1039
|
{
|
|
1266
|
-
default: '
|
|
1040
|
+
default: 'pi',
|
|
1267
1041
|
description:
|
|
1268
|
-
'
|
|
1042
|
+
'Source provider whose sessions to search. Defaults to pi. Non-pi providers have no current session, so they require an explicit session id.',
|
|
1269
1043
|
},
|
|
1270
1044
|
),
|
|
1271
1045
|
),
|
|
1272
|
-
|
|
1046
|
+
session: Type.Optional(
|
|
1273
1047
|
Type.String({
|
|
1274
1048
|
description:
|
|
1275
|
-
'Context/session ID. Defaults to the current Pi session for
|
|
1049
|
+
'Context/session ID. Defaults to the current Pi session for pi; required when provider is not pi.',
|
|
1276
1050
|
}),
|
|
1277
1051
|
),
|
|
1278
|
-
query: Type.Optional(
|
|
1279
|
-
Type.String({ description: 'Search query to rank messages by relevance' }),
|
|
1280
|
-
),
|
|
1281
|
-
pattern: Type.Optional(
|
|
1282
|
-
Type.String({ description: 'Glob pattern to filter messages (e.g. *rand*)' }),
|
|
1283
|
-
),
|
|
1284
1052
|
scope: Type.Optional(
|
|
1285
1053
|
Type.Union([Type.Literal('lineage'), Type.Literal('all')], {
|
|
1286
1054
|
default: 'lineage',
|
|
1287
1055
|
description: 'Scope: lineage (current branch) or all entries in the session',
|
|
1288
1056
|
}),
|
|
1289
1057
|
),
|
|
1290
|
-
compact: Type.Optional(
|
|
1291
|
-
Type.Boolean({
|
|
1292
|
-
default: true,
|
|
1293
|
-
description: 'Display results in compact entry-overview format',
|
|
1294
|
-
}),
|
|
1295
|
-
),
|
|
1296
|
-
ids: Type.Optional(
|
|
1297
|
-
Type.Array(Type.String(), {
|
|
1298
|
-
description: 'Entry IDs to expand (show full content)',
|
|
1299
|
-
}),
|
|
1300
|
-
),
|
|
1301
1058
|
page: Type.Optional(
|
|
1302
1059
|
Type.Integer({
|
|
1303
1060
|
minimum: 1,
|
|
@@ -1305,6 +1062,57 @@ export function createGoosedumpIntegration(
|
|
|
1305
1062
|
description: 'Page number for ranked results',
|
|
1306
1063
|
}),
|
|
1307
1064
|
),
|
|
1065
|
+
}),
|
|
1066
|
+
async execute(_id, params, _signal, _onUpdate, ctx): Promise<AgentToolResult<void>> {
|
|
1067
|
+
if (!goosedumpAvailable) {
|
|
1068
|
+
return textResult(
|
|
1069
|
+
'goosedump is not available. Install with: npm install @jarkkojs/goosedump',
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
try {
|
|
1074
|
+
const provider = params.provider ?? 'pi';
|
|
1075
|
+
const target = resolveTarget(ctx, provider, params.session);
|
|
1076
|
+
if (!target) return missingSessionId('goose_search', provider);
|
|
1077
|
+
|
|
1078
|
+
const page = params.page ?? 1;
|
|
1079
|
+
const messages = goosedumpSearch(target, params.query, {
|
|
1080
|
+
scope: params.scope ?? 'lineage',
|
|
1081
|
+
page,
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
if (messages.length === 0) {
|
|
1085
|
+
return textResult(`No results for "${params.query}"`);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const text = formatMessagesCompact(messages);
|
|
1089
|
+
const header = `Results for "${params.query}" (page ${page}):\n\n`;
|
|
1090
|
+
return textResult(header + text);
|
|
1091
|
+
} catch (err) {
|
|
1092
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1093
|
+
return textResult(`goosedump error: ${message}`);
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
}),
|
|
1097
|
+
);
|
|
1098
|
+
|
|
1099
|
+
pi.registerTool(
|
|
1100
|
+
defineTool({
|
|
1101
|
+
name: 'goose_get',
|
|
1102
|
+
label: 'goose_get',
|
|
1103
|
+
description:
|
|
1104
|
+
'Expand specific entry IDs (from goose_search or goose_grep) to their full content. Defaults to the current Pi session.',
|
|
1105
|
+
promptSnippet:
|
|
1106
|
+
'goose_get({ ids, provider?, session? }) - expand entry IDs to full content; defaults to current Pi session',
|
|
1107
|
+
promptGuidelines: [
|
|
1108
|
+
'Use goose_get to expand entry IDs returned by goose_search or goose_grep into their full content.',
|
|
1109
|
+
'Pass only the specific IDs you need; full transcripts are expensive.',
|
|
1110
|
+
],
|
|
1111
|
+
parameters: Type.Object({
|
|
1112
|
+
ids: Type.Array(Type.String(), {
|
|
1113
|
+
minItems: 1,
|
|
1114
|
+
description: 'Entry IDs to expand (show full content)',
|
|
1115
|
+
}),
|
|
1308
1116
|
provider: Type.Optional(
|
|
1309
1117
|
Type.Union(
|
|
1310
1118
|
[
|
|
@@ -1319,28 +1127,97 @@ export function createGoosedumpIntegration(
|
|
|
1319
1127
|
{
|
|
1320
1128
|
default: 'pi',
|
|
1321
1129
|
description:
|
|
1322
|
-
'Source provider whose sessions to
|
|
1130
|
+
'Source provider whose sessions to read. Defaults to pi. Non-pi providers have no current session, so they require an explicit session id.',
|
|
1323
1131
|
},
|
|
1324
1132
|
),
|
|
1325
1133
|
),
|
|
1326
|
-
|
|
1134
|
+
session: Type.Optional(
|
|
1135
|
+
Type.String({
|
|
1136
|
+
description:
|
|
1137
|
+
'Context/session ID. Defaults to the current Pi session for pi; required when provider is not pi.',
|
|
1138
|
+
}),
|
|
1139
|
+
),
|
|
1140
|
+
scope: Type.Optional(
|
|
1141
|
+
Type.Union([Type.Literal('lineage'), Type.Literal('all')], {
|
|
1142
|
+
default: 'lineage',
|
|
1143
|
+
description: 'Scope: lineage (current branch) or all entries in the session',
|
|
1144
|
+
}),
|
|
1145
|
+
),
|
|
1146
|
+
}),
|
|
1147
|
+
async execute(_id, params, _signal, _onUpdate, ctx): Promise<AgentToolResult<void>> {
|
|
1148
|
+
if (!goosedumpAvailable) {
|
|
1149
|
+
return textResult(
|
|
1150
|
+
'goosedump is not available. Install with: npm install @jarkkojs/goosedump',
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
try {
|
|
1155
|
+
const provider = params.provider ?? 'pi';
|
|
1156
|
+
const target = resolveTarget(ctx, provider, params.session);
|
|
1157
|
+
if (!target) return missingSessionId('goose_get', provider);
|
|
1158
|
+
const messages = goosedumpShow(target, {
|
|
1159
|
+
scope: params.scope ?? 'lineage',
|
|
1160
|
+
ids: params.ids,
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
if (messages.length === 0) {
|
|
1164
|
+
return textResult('No entries found for the given IDs.');
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return textResult(formatMessagesFull(messages));
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1170
|
+
return textResult(`goosedump error: ${message}`);
|
|
1171
|
+
}
|
|
1172
|
+
},
|
|
1173
|
+
}),
|
|
1174
|
+
);
|
|
1175
|
+
|
|
1176
|
+
pi.registerTool(
|
|
1177
|
+
defineTool({
|
|
1178
|
+
name: 'goose_grep',
|
|
1179
|
+
label: 'goose_grep',
|
|
1180
|
+
description:
|
|
1181
|
+
'Filter session messages by glob pattern (e.g. *rand*). Returns compact entry overviews with entry IDs; use goose_get to expand. Defaults to the current Pi session.',
|
|
1182
|
+
promptSnippet:
|
|
1183
|
+
'goose_grep({ pattern, provider?, session?, scope? }) - filter messages by glob; defaults to current Pi session',
|
|
1184
|
+
promptGuidelines: [
|
|
1185
|
+
'Use goose_grep to filter session messages by glob pattern when you need exact substring/glob matching instead of ranked relevance.',
|
|
1186
|
+
'Expand interesting entry IDs with goose_get.',
|
|
1187
|
+
'Default scope is "lineage" (current branch); use scope: "all" to include all entries in the session.',
|
|
1188
|
+
],
|
|
1189
|
+
parameters: Type.Object({
|
|
1190
|
+
pattern: Type.String({ description: 'Glob pattern to filter messages (e.g. *rand*)' }),
|
|
1191
|
+
provider: Type.Optional(
|
|
1327
1192
|
Type.Union(
|
|
1328
1193
|
[
|
|
1329
|
-
Type.Literal('
|
|
1194
|
+
Type.Literal('pi'),
|
|
1330
1195
|
Type.Literal('claude'),
|
|
1331
1196
|
Type.Literal('codex'),
|
|
1332
1197
|
Type.Literal('crush'),
|
|
1333
1198
|
Type.Literal('gemini'),
|
|
1334
1199
|
Type.Literal('goose'),
|
|
1335
1200
|
Type.Literal('opencode'),
|
|
1336
|
-
Type.Literal('pi'),
|
|
1337
1201
|
],
|
|
1338
1202
|
{
|
|
1203
|
+
default: 'pi',
|
|
1339
1204
|
description:
|
|
1340
|
-
|
|
1205
|
+
'Source provider whose sessions to grep. Defaults to pi. Non-pi providers have no current session, so they require an explicit session id.',
|
|
1341
1206
|
},
|
|
1342
1207
|
),
|
|
1343
1208
|
),
|
|
1209
|
+
session: Type.Optional(
|
|
1210
|
+
Type.String({
|
|
1211
|
+
description:
|
|
1212
|
+
'Context/session ID. Defaults to the current Pi session for pi; required when provider is not pi.',
|
|
1213
|
+
}),
|
|
1214
|
+
),
|
|
1215
|
+
scope: Type.Optional(
|
|
1216
|
+
Type.Union([Type.Literal('lineage'), Type.Literal('all')], {
|
|
1217
|
+
default: 'lineage',
|
|
1218
|
+
description: 'Scope: lineage (current branch) or all entries in the session',
|
|
1219
|
+
}),
|
|
1220
|
+
),
|
|
1344
1221
|
}),
|
|
1345
1222
|
async execute(_id, params, _signal, _onUpdate, ctx): Promise<AgentToolResult<void>> {
|
|
1346
1223
|
if (!goosedumpAvailable) {
|
|
@@ -1350,144 +1227,19 @@ export function createGoosedumpIntegration(
|
|
|
1350
1227
|
}
|
|
1351
1228
|
|
|
1352
1229
|
try {
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
lines.push(` ${listing.id} ${suffix}`);
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
return textResult(lines.join('\n'));
|
|
1367
|
-
}
|
|
1368
|
-
|
|
1369
|
-
case 'search': {
|
|
1370
|
-
const provider = params.provider ?? 'pi';
|
|
1371
|
-
const target = resolveTarget(ctx, provider, params.contextId);
|
|
1372
|
-
if (!target) return missingContextId('search', provider);
|
|
1373
|
-
|
|
1374
|
-
if (!params.query) return textResult('query is required for search');
|
|
1375
|
-
|
|
1376
|
-
if (params.outputFormat) {
|
|
1377
|
-
return textResult(
|
|
1378
|
-
goosedumpConvert('search', target, params.outputFormat, {
|
|
1379
|
-
positionals: [params.query],
|
|
1380
|
-
scope: params.scope,
|
|
1381
|
-
page: params.page ?? 1,
|
|
1382
|
-
}),
|
|
1383
|
-
);
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
const result = goosedumpSearch(target, params.query, {
|
|
1387
|
-
scope: params.scope ?? 'lineage',
|
|
1388
|
-
page: params.page ?? 1,
|
|
1389
|
-
});
|
|
1390
|
-
|
|
1391
|
-
if (result.messages.length === 0) {
|
|
1392
|
-
return textResult(`No results for "${params.query}"`);
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
const useCompact = params.compact ?? true;
|
|
1396
|
-
const text = useCompact
|
|
1397
|
-
? formatMessagesCompact(result.messages)
|
|
1398
|
-
: formatMessagesFull(result.messages);
|
|
1399
|
-
|
|
1400
|
-
const header = `Results for "${params.query}" (page ${result.page} of ${result.totalPages}):\n\n`;
|
|
1401
|
-
return textResult(header + text);
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
case 'grep': {
|
|
1405
|
-
const provider = params.provider ?? 'pi';
|
|
1406
|
-
const target = resolveTarget(ctx, provider, params.contextId);
|
|
1407
|
-
if (!target) return missingContextId('grep', provider);
|
|
1408
|
-
|
|
1409
|
-
if (!params.pattern) return textResult('pattern is required for grep');
|
|
1410
|
-
|
|
1411
|
-
if (params.outputFormat) {
|
|
1412
|
-
return textResult(
|
|
1413
|
-
goosedumpConvert('grep', target, params.outputFormat, {
|
|
1414
|
-
positionals: [params.pattern],
|
|
1415
|
-
scope: params.scope,
|
|
1416
|
-
}),
|
|
1417
|
-
);
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
const messages = goosedumpGrep(target, params.pattern, {
|
|
1421
|
-
scope: params.scope ?? 'lineage',
|
|
1422
|
-
});
|
|
1423
|
-
|
|
1424
|
-
if (messages.length === 0) {
|
|
1425
|
-
return textResult(`No matches for "${params.pattern}"`);
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
const useCompact = params.compact ?? true;
|
|
1429
|
-
const text = useCompact
|
|
1430
|
-
? formatMessagesCompact(messages)
|
|
1431
|
-
: formatMessagesFull(messages);
|
|
1432
|
-
|
|
1433
|
-
return textResult(text);
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
case 'expand': {
|
|
1437
|
-
const provider = params.provider ?? 'pi';
|
|
1438
|
-
const target = resolveTarget(ctx, provider, params.contextId);
|
|
1439
|
-
if (!target) return missingContextId('expand', provider);
|
|
1440
|
-
|
|
1441
|
-
if (!params.ids || params.ids.length === 0) {
|
|
1442
|
-
return textResult('ids is required for expand (array of entry IDs)');
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
if (params.outputFormat) {
|
|
1446
|
-
return textResult(
|
|
1447
|
-
goosedumpConvert('show', target, params.outputFormat, {
|
|
1448
|
-
ids: params.ids,
|
|
1449
|
-
scope: params.scope,
|
|
1450
|
-
}),
|
|
1451
|
-
);
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
const messages = goosedumpShow(target, {
|
|
1455
|
-
scope: params.scope ?? 'lineage',
|
|
1456
|
-
ids: params.ids,
|
|
1457
|
-
});
|
|
1458
|
-
|
|
1459
|
-
if (messages.length === 0) {
|
|
1460
|
-
return textResult('No entries found for the given IDs.');
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
return textResult(formatMessagesFull(messages));
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
case 'view': {
|
|
1467
|
-
const provider = params.provider ?? 'pi';
|
|
1468
|
-
const target = resolveTarget(ctx, provider, params.contextId);
|
|
1469
|
-
if (!target) return missingContextId('view', provider);
|
|
1470
|
-
|
|
1471
|
-
if (params.outputFormat) {
|
|
1472
|
-
return textResult(
|
|
1473
|
-
goosedumpConvert('show', target, params.outputFormat, {
|
|
1474
|
-
scope: params.scope,
|
|
1475
|
-
}),
|
|
1476
|
-
);
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
const messages = goosedumpShow(target, { scope: params.scope ?? 'lineage' });
|
|
1480
|
-
|
|
1481
|
-
if (messages.length === 0) {
|
|
1482
|
-
return textResult(`Session "${target}" has no messages.`);
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
return textResult(formatMessagesFull(messages));
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
|
-
default:
|
|
1489
|
-
return textResult(`Unknown action: ${params.action ?? 'list'}`);
|
|
1230
|
+
const provider = params.provider ?? 'pi';
|
|
1231
|
+
const target = resolveTarget(ctx, provider, params.session);
|
|
1232
|
+
if (!target) return missingSessionId('goose_grep', provider);
|
|
1233
|
+
|
|
1234
|
+
const messages = goosedumpGrep(target, params.pattern, {
|
|
1235
|
+
scope: params.scope ?? 'lineage',
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
if (messages.length === 0) {
|
|
1239
|
+
return textResult(`No matches for "${params.pattern}"`);
|
|
1490
1240
|
}
|
|
1241
|
+
|
|
1242
|
+
return textResult(formatMessagesCompact(messages));
|
|
1491
1243
|
} catch (err) {
|
|
1492
1244
|
const message = err instanceof Error ? err.message : String(err);
|
|
1493
1245
|
return textResult(`goosedump error: ${message}`);
|
|
@@ -1495,22 +1247,52 @@ export function createGoosedumpIntegration(
|
|
|
1495
1247
|
},
|
|
1496
1248
|
}),
|
|
1497
1249
|
);
|
|
1250
|
+
|
|
1251
|
+
pi.registerTool(
|
|
1252
|
+
defineTool({
|
|
1253
|
+
name: 'goose_compact',
|
|
1254
|
+
label: 'goose_compact',
|
|
1255
|
+
description:
|
|
1256
|
+
"Trigger a goosedump compaction of the current session now, optionally keeping the last N user turns verbatim while summarizing everything before. Compaction runs asynchronously via Pi's compaction flow.",
|
|
1257
|
+
promptSnippet:
|
|
1258
|
+
"goose_compact({ keep? }) - compact now; keep last N user turns (turn-based), omit to compact up to Pi's cut",
|
|
1259
|
+
promptGuidelines: [
|
|
1260
|
+
'Use goose_compact to compact the session proactively with goosedump when context is large or nearing limits.',
|
|
1261
|
+
"Pass keep:N (N >= 1) to keep the last N user turns verbatim and summarize everything before them; omit to compact up to Pi's token-based cut.",
|
|
1262
|
+
'Compaction is asynchronous; the session is notified when it completes.',
|
|
1263
|
+
],
|
|
1264
|
+
parameters: Type.Object({
|
|
1265
|
+
keep: Type.Optional(
|
|
1266
|
+
Type.Integer({
|
|
1267
|
+
minimum: 1,
|
|
1268
|
+
description:
|
|
1269
|
+
"User turns to keep verbatim; summarize everything before them. Omit to compact up to Pi's token-based cut.",
|
|
1270
|
+
}),
|
|
1271
|
+
),
|
|
1272
|
+
}),
|
|
1273
|
+
async execute(_id, params, _signal, _onUpdate, ctx): Promise<AgentToolResult<void>> {
|
|
1274
|
+
if (!goosedumpAvailable) {
|
|
1275
|
+
return textResult(
|
|
1276
|
+
'goosedump is not available. Install with: npm install @jarkkojs/goosedump',
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const keep = params.keep ?? null;
|
|
1281
|
+
requestCompaction(ctx, keep);
|
|
1282
|
+
return textResult(
|
|
1283
|
+
keep !== null
|
|
1284
|
+
? `Compacting with goosedump; keeping the last ${keep} user turn${keep === 1 ? '' : 's'}.`
|
|
1285
|
+
: "Compacting with goosedump up to Pi's cut.",
|
|
1286
|
+
);
|
|
1287
|
+
},
|
|
1288
|
+
}),
|
|
1289
|
+
);
|
|
1498
1290
|
}
|
|
1499
1291
|
|
|
1500
1292
|
pi.on('session_start', async (_event, ctx) => {
|
|
1501
1293
|
goosedumpAvailable = checkGoosedump(ctx);
|
|
1502
1294
|
resetCompactCounters(ctx.cwd);
|
|
1503
|
-
|
|
1504
|
-
if (goosedumpAvailable) {
|
|
1505
|
-
const theme = ctx.ui.theme;
|
|
1506
|
-
const dot = theme.fg('success', '●');
|
|
1507
|
-
const label = theme.fg('text', 'Goosedump');
|
|
1508
|
-
ctx.ui.setStatus('goosedump', `${dot} ${label}`);
|
|
1509
|
-
}
|
|
1510
|
-
});
|
|
1511
|
-
|
|
1512
|
-
pi.on('tool_execution_end', async (_event, ctx) => {
|
|
1513
|
-
noteCompletedStep(ctx);
|
|
1295
|
+
updateStatus(ctx);
|
|
1514
1296
|
});
|
|
1515
1297
|
|
|
1516
1298
|
pi.on('turn_end', async (_event, ctx) => {
|
|
@@ -1525,15 +1307,23 @@ export function createGoosedumpIntegration(
|
|
|
1525
1307
|
const { messagesToSummarize, turnPrefixMessages } = event.preparation;
|
|
1526
1308
|
if (messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) return;
|
|
1527
1309
|
|
|
1528
|
-
const
|
|
1529
|
-
if (!
|
|
1310
|
+
const target = currentSessionTarget(ctx);
|
|
1311
|
+
if (!target) return;
|
|
1530
1312
|
|
|
1531
1313
|
try {
|
|
1314
|
+
const keep = pendingKeep ?? undefined;
|
|
1532
1315
|
const compaction = buildGoosedumpCompaction(
|
|
1533
|
-
|
|
1316
|
+
target,
|
|
1534
1317
|
event.preparation,
|
|
1535
1318
|
event.branchEntries,
|
|
1319
|
+
keep,
|
|
1536
1320
|
);
|
|
1321
|
+
if (compaction && 'cancel' in compaction) {
|
|
1322
|
+
pendingKeep = null;
|
|
1323
|
+
if (ctx.hasUI) ctx.ui.notify(compaction.reason, 'info');
|
|
1324
|
+
return { cancel: true };
|
|
1325
|
+
}
|
|
1326
|
+
pendingKeep = null;
|
|
1537
1327
|
return compaction ? { compaction } : undefined;
|
|
1538
1328
|
} catch (err) {
|
|
1539
1329
|
if (ctx.hasUI) {
|
|
@@ -1569,8 +1359,8 @@ export function createGoosedumpIntegration(
|
|
|
1569
1359
|
registerCommand?: ExtensionAPI['registerCommand'];
|
|
1570
1360
|
};
|
|
1571
1361
|
|
|
1572
|
-
maybePi.registerCommand?.('
|
|
1573
|
-
description: '
|
|
1362
|
+
maybePi.registerCommand?.('goose-settings', {
|
|
1363
|
+
description: 'Edit goosedump compaction settings',
|
|
1574
1364
|
handler: async (_args, ctx) => {
|
|
1575
1365
|
if (!goosedumpAvailable) {
|
|
1576
1366
|
ctx.ui.notify(
|
|
@@ -1581,20 +1371,63 @@ export function createGoosedumpIntegration(
|
|
|
1581
1371
|
}
|
|
1582
1372
|
|
|
1583
1373
|
try {
|
|
1584
|
-
|
|
1374
|
+
await runSettingsEditor(ctx);
|
|
1375
|
+
updateStatus(ctx);
|
|
1376
|
+
} catch (err) {
|
|
1377
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1378
|
+
ctx.ui.notify(`goosedump error: ${message}`, 'error');
|
|
1379
|
+
}
|
|
1380
|
+
},
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
maybePi.registerCommand?.('goose-compact', {
|
|
1384
|
+
description: 'Compact now with goosedump; optionally keep the last N user turns',
|
|
1385
|
+
handler: async (args, ctx) => {
|
|
1386
|
+
if (!goosedumpAvailable) {
|
|
1387
|
+
ctx.ui.notify(
|
|
1388
|
+
'goosedump is not available. Install with: npm install @jarkkojs/goosedump',
|
|
1389
|
+
'error',
|
|
1390
|
+
);
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1585
1393
|
|
|
1586
|
-
|
|
1587
|
-
|
|
1394
|
+
// `/goose-compact [keep:N]` — an optional leading `keep:N` (N >= 1) keeps
|
|
1395
|
+
// the last N user turns verbatim and summarizes everything before; with
|
|
1396
|
+
// no argument goosedump summarizes up to Pi's token-based cut.
|
|
1397
|
+
let keep: number | null = null;
|
|
1398
|
+
if (typeof args === 'string') {
|
|
1399
|
+
const match = args.trim().match(/^keep:(\d+)$/);
|
|
1400
|
+
if (match) keep = Number(match[1]);
|
|
1401
|
+
if (match && (keep === null || keep < 1)) {
|
|
1402
|
+
ctx.ui.notify('keep:N requires N >= 1', 'error');
|
|
1588
1403
|
return;
|
|
1589
1404
|
}
|
|
1405
|
+
}
|
|
1590
1406
|
|
|
1591
|
-
|
|
1407
|
+
requestCompaction(ctx, keep);
|
|
1408
|
+
},
|
|
1409
|
+
});
|
|
1592
1410
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1411
|
+
maybePi.registerCommand?.('goose-search', {
|
|
1412
|
+
description: 'Search the current session history',
|
|
1413
|
+
handler: async (args, ctx) => {
|
|
1414
|
+
if (!goosedumpAvailable) {
|
|
1415
|
+
ctx.ui.notify(
|
|
1416
|
+
'goosedump is not available. Install with: npm install @jarkkojs/goosedump',
|
|
1417
|
+
'error',
|
|
1418
|
+
);
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
let query = typeof args === 'string' ? args.trim() : '';
|
|
1423
|
+
if (query.length === 0) {
|
|
1424
|
+
const input = await ctx.ui.input('Search query:', '');
|
|
1425
|
+
query = (input ?? '').trim();
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
if (query.length === 0) return;
|
|
1429
|
+
try {
|
|
1430
|
+
await runSearchPanel(ctx, query);
|
|
1598
1431
|
} catch (err) {
|
|
1599
1432
|
const message = err instanceof Error ? err.message : String(err);
|
|
1600
1433
|
ctx.ui.notify(`goosedump error: ${message}`, 'error');
|