pi-goosedump 0.5.0 → 0.5.2
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/index.ts +292 -370
- package/package.json +2 -2
package/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
import { defineTool } from '@earendil-works/pi-coding-agent';
|
|
14
14
|
|
|
15
15
|
import { execFileSync } from 'node:child_process';
|
|
16
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
+
import { type Dirent, existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
17
17
|
import { createRequire } from 'node:module';
|
|
18
18
|
import { homedir } from 'node:os';
|
|
19
19
|
import { basename, join } from 'node:path';
|
|
@@ -23,7 +23,7 @@ import { Type } from '@sinclair/typebox';
|
|
|
23
23
|
|
|
24
24
|
const require = createRequire(import.meta.url);
|
|
25
25
|
|
|
26
|
-
const GOOSEDUMP_VERSION = [0, 5,
|
|
26
|
+
const GOOSEDUMP_VERSION = [0, 5, 1] as const;
|
|
27
27
|
|
|
28
28
|
interface GoosedumpListing {
|
|
29
29
|
id: string;
|
|
@@ -37,17 +37,12 @@ interface GoosedumpMessage {
|
|
|
37
37
|
score?: number;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
interface
|
|
40
|
+
interface GoosedumpSearchResult {
|
|
41
41
|
messages: GoosedumpMessage[];
|
|
42
|
-
totalCount: number;
|
|
43
42
|
page: number;
|
|
44
43
|
totalPages: number;
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
interface ListJson {
|
|
48
|
-
sessions: { id: string; path?: string | null }[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
46
|
interface ToolCallJson {
|
|
52
47
|
name: string;
|
|
53
48
|
arguments: unknown;
|
|
@@ -87,10 +82,6 @@ interface SearchJson {
|
|
|
87
82
|
hits: HitJson[];
|
|
88
83
|
}
|
|
89
84
|
|
|
90
|
-
interface GrepJson {
|
|
91
|
-
hits: HitJson[];
|
|
92
|
-
}
|
|
93
|
-
|
|
94
85
|
interface CompactJson {
|
|
95
86
|
goals: string[];
|
|
96
87
|
files: { modified: string[]; created: string[]; read: string[] };
|
|
@@ -233,6 +224,14 @@ function formatMessagesFull(messages: GoosedumpMessage[]): string {
|
|
|
233
224
|
return messages.map((m) => `[${m.id}] ${m.role}:\n${m.content}`).join('\n\n---\n\n');
|
|
234
225
|
}
|
|
235
226
|
|
|
227
|
+
function textResult(text: string): AgentToolResult<void> {
|
|
228
|
+
return { content: [{ type: 'text', text }], details: undefined };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function missingContextId(action: string): AgentToolResult<void> {
|
|
232
|
+
return textResult(`contextId is required for ${action} when no current session is available`);
|
|
233
|
+
}
|
|
234
|
+
|
|
236
235
|
function runGoosedump(args: string[]): string {
|
|
237
236
|
return execFileSync(process.execPath, [binaryPath(), ...args], {
|
|
238
237
|
encoding: 'utf-8',
|
|
@@ -240,66 +239,74 @@ function runGoosedump(args: string[]): string {
|
|
|
240
239
|
}
|
|
241
240
|
|
|
242
241
|
function goosedumpList(): GoosedumpListing[] {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
242
|
+
const ids = runGoosedump(['list', 'pi:*'])
|
|
243
|
+
.split('\n')
|
|
244
|
+
.map((line) => line.trim())
|
|
245
|
+
.filter((line) => line.startsWith('pi:'))
|
|
246
|
+
.map((line) => line.slice('pi:'.length));
|
|
247
|
+
|
|
248
|
+
const paths = indexPiSessionPaths();
|
|
249
|
+
return ids.map((id) => ({ id, path: paths.get(id) ?? null }));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function piSessionsDir(): string {
|
|
253
|
+
const explicit = process.env.PI_CODING_AGENT_SESSION_DIR;
|
|
254
|
+
if (explicit) return explicit;
|
|
255
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
|
|
256
|
+
return join(agentDir, 'sessions');
|
|
248
257
|
}
|
|
249
258
|
|
|
250
|
-
|
|
259
|
+
// goosedump's `list` no longer reports session file paths, so map each pi
|
|
260
|
+
// session id back to its file by scanning the sessions tree, matching the same
|
|
261
|
+
// id goosedump derives from the session header.
|
|
262
|
+
function indexPiSessionPaths(): Map<string, string> {
|
|
263
|
+
const paths = new Map<string, string>();
|
|
264
|
+
const walk = (dir: string): void => {
|
|
265
|
+
let entries: Dirent[];
|
|
266
|
+
try {
|
|
267
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
268
|
+
} catch {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
const full = join(dir, entry.name);
|
|
273
|
+
if (entry.isDirectory()) {
|
|
274
|
+
walk(full);
|
|
275
|
+
} else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
276
|
+
const id = sessionFileContextId(full);
|
|
277
|
+
if (id && !paths.has(id)) paths.set(id, full);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
walk(piSessionsDir());
|
|
282
|
+
return paths;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function goosedumpSearch(
|
|
251
286
|
contextId: string,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
ids?: string;
|
|
256
|
-
rank?: boolean;
|
|
257
|
-
page?: number;
|
|
258
|
-
} = {},
|
|
259
|
-
): GoosedumpContextResult {
|
|
287
|
+
query: string,
|
|
288
|
+
options: { scope?: string; page?: number } = {},
|
|
289
|
+
): GoosedumpSearchResult {
|
|
260
290
|
const scope = options.scope ?? 'lineage';
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
const target = `pi:${contextId}`;
|
|
264
|
-
const args = hasQuery
|
|
265
|
-
? isSearch
|
|
266
|
-
? ['search', target, options.grep ?? '']
|
|
267
|
-
: ['grep', target, options.grep ?? '']
|
|
268
|
-
: ['show', target];
|
|
269
|
-
|
|
270
|
-
if (options.ids) {
|
|
271
|
-
args.push('--ids', options.ids);
|
|
272
|
-
}
|
|
273
|
-
if (isSearch && options.page) {
|
|
274
|
-
args.push('--page', String(options.page));
|
|
275
|
-
}
|
|
291
|
+
const args = ['search', `pi:${contextId}`, query];
|
|
292
|
+
if (options.page) args.push('--page', String(options.page));
|
|
276
293
|
args.push('--scope', scope);
|
|
277
294
|
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const json = JSON.parse(output) as SearchJson | GrepJson;
|
|
282
|
-
const messages = (json.hits ?? []).map(hitMessage);
|
|
283
|
-
const page = isSearch ? ((json as SearchJson).page ?? options.page ?? 1) : 1;
|
|
284
|
-
const totalPages = isSearch ? ((json as SearchJson).totalPages ?? 1) : 1;
|
|
285
|
-
return { messages, totalCount: messages.length, page, totalPages };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const json = JSON.parse(output) as ShowJson;
|
|
289
|
-
const messages = (json.messages ?? []).map(showMessage);
|
|
290
|
-
return { messages, totalCount: messages.length, page: options.page ?? 1, totalPages: 1 };
|
|
295
|
+
const json = JSON.parse(runGoosedump(args)) as SearchJson;
|
|
296
|
+
const messages = (json.hits ?? []).map(hitMessage);
|
|
297
|
+
return { messages, page: json.page ?? options.page ?? 1, totalPages: json.totalPages ?? 1 };
|
|
291
298
|
}
|
|
292
299
|
|
|
293
|
-
function
|
|
300
|
+
function goosedumpShow(
|
|
294
301
|
contextId: string,
|
|
295
|
-
|
|
296
|
-
scope: string = 'lineage',
|
|
302
|
+
options: { scope?: string; ids?: string[] } = {},
|
|
297
303
|
): GoosedumpMessage[] {
|
|
298
|
-
|
|
304
|
+
const scope = options.scope ?? 'lineage';
|
|
305
|
+
const args = ['show', `pi:${contextId}`];
|
|
306
|
+
if (options.ids && options.ids.length > 0) args.push('--ids', options.ids.join(','));
|
|
307
|
+
args.push('--scope', scope);
|
|
299
308
|
|
|
300
|
-
const json = JSON.parse(
|
|
301
|
-
runGoosedump(['show', `pi:${contextId}`, '--ids', entryIds.join(','), '--scope', scope]),
|
|
302
|
-
) as ShowJson;
|
|
309
|
+
const json = JSON.parse(runGoosedump(args)) as ShowJson;
|
|
303
310
|
return (json.messages ?? []).map(showMessage);
|
|
304
311
|
}
|
|
305
312
|
|
|
@@ -592,14 +599,204 @@ function buildGoosedumpCompaction(
|
|
|
592
599
|
};
|
|
593
600
|
}
|
|
594
601
|
|
|
602
|
+
async function runSessionBrowser(
|
|
603
|
+
ctx: ExtensionContext,
|
|
604
|
+
listings: GoosedumpListing[],
|
|
605
|
+
): Promise<string | null | undefined> {
|
|
606
|
+
let resumePath: string | null | undefined;
|
|
607
|
+
|
|
608
|
+
await ctx.ui.custom<void>(
|
|
609
|
+
(tui, theme, _kb, done) => {
|
|
610
|
+
let selectedIndex = 0;
|
|
611
|
+
let mode: 'list' | 'compact' = 'list';
|
|
612
|
+
let compactLines: string[] = [];
|
|
613
|
+
let compactScroll = 0;
|
|
614
|
+
let compactTitle = '';
|
|
615
|
+
|
|
616
|
+
const COMPACT_VIEWPORT = 20;
|
|
617
|
+
|
|
618
|
+
return {
|
|
619
|
+
render(width: number): string[] {
|
|
620
|
+
const innerW = width - 4;
|
|
621
|
+
const border = theme.fg('border', '│');
|
|
622
|
+
const borderFg = (s: string) => theme.fg('border', s);
|
|
623
|
+
const dim = (s: string) => theme.fg('dim', s);
|
|
624
|
+
const accent = (s: string) => theme.fg('accent', s);
|
|
625
|
+
const text = (s: string) => theme.fg('text', s);
|
|
626
|
+
const muted = (s: string) => theme.fg('muted', s);
|
|
627
|
+
|
|
628
|
+
const lines: string[] = [];
|
|
629
|
+
|
|
630
|
+
if (mode === 'compact') {
|
|
631
|
+
const title = accent(` Compact: ${truncateToWidth(compactTitle, 40)} `);
|
|
632
|
+
const topFill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(title))));
|
|
633
|
+
lines.push(`${borderFg('╭─')}${title}${topFill}${borderFg('─╮')}`);
|
|
634
|
+
|
|
635
|
+
const total = compactLines.length;
|
|
636
|
+
const maxScroll = Math.max(0, total - COMPACT_VIEWPORT);
|
|
637
|
+
if (compactScroll > maxScroll) compactScroll = maxScroll;
|
|
638
|
+
const window = compactLines.slice(compactScroll, compactScroll + COMPACT_VIEWPORT);
|
|
639
|
+
for (const row of window) {
|
|
640
|
+
lines.push(makeRow(text(truncateToWidth(row, innerW)), innerW, border));
|
|
641
|
+
}
|
|
642
|
+
for (let i = window.length; i < COMPACT_VIEWPORT; i++) {
|
|
643
|
+
lines.push(makeRow('', innerW, border));
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const position =
|
|
647
|
+
total > COMPACT_VIEWPORT
|
|
648
|
+
? `${compactScroll + 1}-${Math.min(compactScroll + COMPACT_VIEWPORT, total)} of ${total}`
|
|
649
|
+
: `${total} line${total === 1 ? '' : 's'}`;
|
|
650
|
+
lines.push(makeRow('', innerW, border));
|
|
651
|
+
lines.push(makeRow(dim(`↑↓ scroll esc back (${position})`), innerW, border));
|
|
652
|
+
lines.push(`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`);
|
|
653
|
+
return lines;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Top border
|
|
657
|
+
const title = accent(' Goosedump Sessions ');
|
|
658
|
+
const topFill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(title))));
|
|
659
|
+
lines.push(`${borderFg('╭─')}${title}${topFill}${borderFg('─╮')}`);
|
|
660
|
+
|
|
661
|
+
// Session count
|
|
662
|
+
const countInfo = `${listings.length} session${listings.length === 1 ? '' : 's'}`;
|
|
663
|
+
lines.push(makeRow(`${muted(countInfo)}`, innerW, border));
|
|
664
|
+
|
|
665
|
+
lines.push(makeRow('', innerW, border));
|
|
666
|
+
|
|
667
|
+
// Session list
|
|
668
|
+
const maxItems = Math.min(listings.length, 20);
|
|
669
|
+
for (let i = 0; i < maxItems; i++) {
|
|
670
|
+
const listing = listings[i];
|
|
671
|
+
const isSelected = i === selectedIndex;
|
|
672
|
+
const cursor = isSelected ? accent('▶') : ' ';
|
|
673
|
+
const idText = isSelected ? accent(listing.id) : text(listing.id);
|
|
674
|
+
const line = ` ${cursor} ${idText}`;
|
|
675
|
+
lines.push(makeRow(truncateToWidth(line, innerW), innerW, border));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (listings.length > 20) {
|
|
679
|
+
lines.push(makeRow(dim(` ... and ${listings.length - 20} more`), innerW, border));
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Footer
|
|
683
|
+
lines.push(makeRow('', innerW, border));
|
|
684
|
+
lines.push(
|
|
685
|
+
makeRow(
|
|
686
|
+
dim('↑↓ navigate enter compact shift+enter resume esc close'),
|
|
687
|
+
innerW,
|
|
688
|
+
border,
|
|
689
|
+
),
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
// Bottom border
|
|
693
|
+
lines.push(`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`);
|
|
694
|
+
|
|
695
|
+
return lines;
|
|
696
|
+
},
|
|
697
|
+
|
|
698
|
+
handleInput(data: string): void {
|
|
699
|
+
if (mode === 'compact') {
|
|
700
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
|
|
701
|
+
mode = 'list';
|
|
702
|
+
tui.requestRender();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (matchesKey(data, Key.up)) {
|
|
706
|
+
compactScroll = Math.max(0, compactScroll - 1);
|
|
707
|
+
tui.requestRender();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (matchesKey(data, Key.down)) {
|
|
711
|
+
compactScroll += 1;
|
|
712
|
+
tui.requestRender();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (matchesKey(data, Key.pageUp)) {
|
|
716
|
+
compactScroll = Math.max(0, compactScroll - COMPACT_VIEWPORT);
|
|
717
|
+
tui.requestRender();
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (matchesKey(data, Key.pageDown)) {
|
|
721
|
+
compactScroll += COMPACT_VIEWPORT;
|
|
722
|
+
tui.requestRender();
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
|
|
729
|
+
done(undefined);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (matchesKey(data, Key.up)) {
|
|
734
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
735
|
+
tui.requestRender();
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (matchesKey(data, Key.down)) {
|
|
740
|
+
selectedIndex = Math.min(listings.length - 1, selectedIndex + 1);
|
|
741
|
+
tui.requestRender();
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (matchesKey(data, Key.shift(Key.enter))) {
|
|
746
|
+
resumePath = listings[selectedIndex]?.path;
|
|
747
|
+
done(undefined);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
|
|
752
|
+
const listing = listings[selectedIndex];
|
|
753
|
+
if (!listing) return;
|
|
754
|
+
compactTitle = listing.id;
|
|
755
|
+
compactScroll = 0;
|
|
756
|
+
try {
|
|
757
|
+
const summary = goosedumpCompact(listing.id, { scope: 'lineage' }).trim();
|
|
758
|
+
compactLines = summary
|
|
759
|
+
? summary.split('\n')
|
|
760
|
+
: ['No compact summary available for this session.'];
|
|
761
|
+
} catch (err) {
|
|
762
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
763
|
+
compactLines = [`Failed to compact session: ${msg}`];
|
|
764
|
+
}
|
|
765
|
+
mode = 'compact';
|
|
766
|
+
tui.requestRender();
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
},
|
|
770
|
+
|
|
771
|
+
invalidate(): void {},
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
function makeRow(content: string, innerW: number, border: string): string {
|
|
775
|
+
const line = truncateToWidth(content, innerW);
|
|
776
|
+
const pad = Math.max(0, innerW - visibleWidth(line));
|
|
777
|
+
return `${border} ${line}${' '.repeat(pad)} ${border}`;
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
overlay: true,
|
|
782
|
+
overlayOptions: {
|
|
783
|
+
anchor: 'center',
|
|
784
|
+
width: 72,
|
|
785
|
+
margin: 2,
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
return resumePath;
|
|
791
|
+
}
|
|
792
|
+
|
|
595
793
|
export function createGoosedumpIntegration(
|
|
596
794
|
options: GoosedumpIntegrationOptions = {},
|
|
597
795
|
): GoosedumpIntegration {
|
|
598
796
|
const shouldRegisterTool = options.registerTool ?? true;
|
|
599
797
|
const shouldOverrideDefaultCompaction = options.overrideDefaultCompaction ?? true;
|
|
600
798
|
|
|
601
|
-
let
|
|
602
|
-
let goosedumpEnabled = false;
|
|
799
|
+
let goosedumpAvailable = false;
|
|
603
800
|
|
|
604
801
|
function checkGoosedump(ctx?: ExtensionContext): boolean {
|
|
605
802
|
const version = goosedumpVersion();
|
|
@@ -690,16 +887,10 @@ export function createGoosedumpIntegration(
|
|
|
690
887
|
),
|
|
691
888
|
}),
|
|
692
889
|
async execute(_id, params, _signal, _onUpdate, ctx): Promise<AgentToolResult<void>> {
|
|
693
|
-
if (!
|
|
694
|
-
return
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
type: 'text',
|
|
698
|
-
text: 'goosedump is not available. Install with: npm install @jarkkojs/goosedump',
|
|
699
|
-
},
|
|
700
|
-
],
|
|
701
|
-
details: undefined,
|
|
702
|
-
};
|
|
890
|
+
if (!goosedumpAvailable) {
|
|
891
|
+
return textResult(
|
|
892
|
+
'goosedump is not available. Install with: npm install @jarkkojs/goosedump',
|
|
893
|
+
);
|
|
703
894
|
}
|
|
704
895
|
|
|
705
896
|
try {
|
|
@@ -707,10 +898,7 @@ export function createGoosedumpIntegration(
|
|
|
707
898
|
case 'list': {
|
|
708
899
|
const listings = goosedumpList();
|
|
709
900
|
if (listings.length === 0) {
|
|
710
|
-
return
|
|
711
|
-
content: [{ type: 'text', text: 'No sessions found.' }],
|
|
712
|
-
details: undefined,
|
|
713
|
-
};
|
|
901
|
+
return textResult('No sessions found.');
|
|
714
902
|
}
|
|
715
903
|
|
|
716
904
|
const lines = ['Available sessions:', ''];
|
|
@@ -718,45 +906,22 @@ export function createGoosedumpIntegration(
|
|
|
718
906
|
lines.push(` ${listing.id}`);
|
|
719
907
|
}
|
|
720
908
|
|
|
721
|
-
return
|
|
722
|
-
content: [{ type: 'text', text: lines.join('\n') }],
|
|
723
|
-
details: undefined,
|
|
724
|
-
};
|
|
909
|
+
return textResult(lines.join('\n'));
|
|
725
910
|
}
|
|
726
911
|
|
|
727
912
|
case 'search': {
|
|
728
913
|
const contextId = params.contextId ?? currentSessionContextId(ctx);
|
|
729
|
-
if (!contextId)
|
|
730
|
-
return {
|
|
731
|
-
content: [
|
|
732
|
-
{
|
|
733
|
-
type: 'text',
|
|
734
|
-
text: 'contextId is required for search when no current session is available',
|
|
735
|
-
},
|
|
736
|
-
],
|
|
737
|
-
details: undefined,
|
|
738
|
-
};
|
|
739
|
-
}
|
|
914
|
+
if (!contextId) return missingContextId('search');
|
|
740
915
|
|
|
741
|
-
if (!params.query)
|
|
742
|
-
return {
|
|
743
|
-
content: [{ type: 'text', text: 'query is required for search' }],
|
|
744
|
-
details: undefined,
|
|
745
|
-
};
|
|
746
|
-
}
|
|
916
|
+
if (!params.query) return textResult('query is required for search');
|
|
747
917
|
|
|
748
|
-
const result =
|
|
918
|
+
const result = goosedumpSearch(contextId, params.query, {
|
|
749
919
|
scope: params.scope ?? 'lineage',
|
|
750
|
-
grep: params.query,
|
|
751
|
-
rank: true,
|
|
752
920
|
page: params.page ?? 1,
|
|
753
921
|
});
|
|
754
922
|
|
|
755
923
|
if (result.messages.length === 0) {
|
|
756
|
-
return {
|
|
757
|
-
content: [{ type: 'text', text: `No results for "${params.query}"` }],
|
|
758
|
-
details: undefined,
|
|
759
|
-
};
|
|
924
|
+
return textResult(`No results for "${params.query}"`);
|
|
760
925
|
}
|
|
761
926
|
|
|
762
927
|
const useCompact = params.compact ?? true;
|
|
@@ -765,97 +930,48 @@ export function createGoosedumpIntegration(
|
|
|
765
930
|
: formatMessagesFull(result.messages);
|
|
766
931
|
|
|
767
932
|
const header = `Results for "${params.query}" (page ${result.page} of ${result.totalPages}):\n\n`;
|
|
768
|
-
return
|
|
769
|
-
content: [{ type: 'text', text: header + text }],
|
|
770
|
-
details: undefined,
|
|
771
|
-
};
|
|
933
|
+
return textResult(header + text);
|
|
772
934
|
}
|
|
773
935
|
|
|
774
936
|
case 'expand': {
|
|
775
937
|
const contextId = params.contextId ?? currentSessionContextId(ctx);
|
|
776
|
-
if (!contextId)
|
|
777
|
-
return {
|
|
778
|
-
content: [
|
|
779
|
-
{
|
|
780
|
-
type: 'text',
|
|
781
|
-
text: 'contextId is required for expand when no current session is available',
|
|
782
|
-
},
|
|
783
|
-
],
|
|
784
|
-
details: undefined,
|
|
785
|
-
};
|
|
786
|
-
}
|
|
938
|
+
if (!contextId) return missingContextId('expand');
|
|
787
939
|
|
|
788
940
|
if (!params.ids || params.ids.length === 0) {
|
|
789
|
-
return
|
|
790
|
-
content: [
|
|
791
|
-
{ type: 'text', text: 'ids is required for expand (array of entry IDs)' },
|
|
792
|
-
],
|
|
793
|
-
details: undefined,
|
|
794
|
-
};
|
|
941
|
+
return textResult('ids is required for expand (array of entry IDs)');
|
|
795
942
|
}
|
|
796
943
|
|
|
797
|
-
const messages =
|
|
798
|
-
|
|
799
|
-
params.ids,
|
|
800
|
-
|
|
801
|
-
);
|
|
944
|
+
const messages = goosedumpShow(contextId, {
|
|
945
|
+
scope: params.scope ?? 'lineage',
|
|
946
|
+
ids: params.ids,
|
|
947
|
+
});
|
|
802
948
|
|
|
803
949
|
if (messages.length === 0) {
|
|
804
|
-
return
|
|
805
|
-
content: [{ type: 'text', text: 'No entries found for the given IDs.' }],
|
|
806
|
-
details: undefined,
|
|
807
|
-
};
|
|
950
|
+
return textResult('No entries found for the given IDs.');
|
|
808
951
|
}
|
|
809
952
|
|
|
810
|
-
return
|
|
811
|
-
content: [{ type: 'text', text: formatMessagesFull(messages) }],
|
|
812
|
-
details: undefined,
|
|
813
|
-
};
|
|
953
|
+
return textResult(formatMessagesFull(messages));
|
|
814
954
|
}
|
|
815
955
|
|
|
816
956
|
case 'view': {
|
|
817
957
|
const contextId = params.contextId ?? currentSessionContextId(ctx);
|
|
818
|
-
if (!contextId)
|
|
819
|
-
return {
|
|
820
|
-
content: [
|
|
821
|
-
{
|
|
822
|
-
type: 'text',
|
|
823
|
-
text: 'contextId is required for view when no current session is available',
|
|
824
|
-
},
|
|
825
|
-
],
|
|
826
|
-
details: undefined,
|
|
827
|
-
};
|
|
828
|
-
}
|
|
958
|
+
if (!contextId) return missingContextId('view');
|
|
829
959
|
|
|
830
|
-
const
|
|
831
|
-
scope: params.scope ?? 'lineage',
|
|
832
|
-
});
|
|
960
|
+
const messages = goosedumpShow(contextId, { scope: params.scope ?? 'lineage' });
|
|
833
961
|
|
|
834
|
-
if (
|
|
835
|
-
return {
|
|
836
|
-
content: [{ type: 'text', text: `Session "${contextId}" has no messages.` }],
|
|
837
|
-
details: undefined,
|
|
838
|
-
};
|
|
962
|
+
if (messages.length === 0) {
|
|
963
|
+
return textResult(`Session "${contextId}" has no messages.`);
|
|
839
964
|
}
|
|
840
965
|
|
|
841
|
-
return
|
|
842
|
-
content: [{ type: 'text', text: formatMessagesFull(result.messages) }],
|
|
843
|
-
details: undefined,
|
|
844
|
-
};
|
|
966
|
+
return textResult(formatMessagesFull(messages));
|
|
845
967
|
}
|
|
846
968
|
|
|
847
969
|
default:
|
|
848
|
-
return {
|
|
849
|
-
content: [{ type: 'text', text: `Unknown action: ${params.action}` }],
|
|
850
|
-
details: undefined,
|
|
851
|
-
};
|
|
970
|
+
return textResult(`Unknown action: ${params.action}`);
|
|
852
971
|
}
|
|
853
972
|
} catch (err) {
|
|
854
973
|
const message = err instanceof Error ? err.message : String(err);
|
|
855
|
-
return {
|
|
856
|
-
content: [{ type: 'text', text: `goosedump error: ${message}` }],
|
|
857
|
-
details: undefined,
|
|
858
|
-
};
|
|
974
|
+
return textResult(`goosedump error: ${message}`);
|
|
859
975
|
}
|
|
860
976
|
},
|
|
861
977
|
}),
|
|
@@ -863,10 +979,9 @@ export function createGoosedumpIntegration(
|
|
|
863
979
|
}
|
|
864
980
|
|
|
865
981
|
pi.on('session_start', async (_event, ctx) => {
|
|
866
|
-
|
|
867
|
-
goosedumpEnabled = goosedumpReady;
|
|
982
|
+
goosedumpAvailable = checkGoosedump(ctx);
|
|
868
983
|
|
|
869
|
-
if (
|
|
984
|
+
if (goosedumpAvailable) {
|
|
870
985
|
const theme = ctx.ui.theme;
|
|
871
986
|
const dot = theme.fg('success', '●');
|
|
872
987
|
const label = theme.fg('text', 'Goosedump');
|
|
@@ -875,7 +990,7 @@ export function createGoosedumpIntegration(
|
|
|
875
990
|
});
|
|
876
991
|
|
|
877
992
|
pi.on('session_before_compact', async (event, ctx) => {
|
|
878
|
-
if (!shouldOverrideDefaultCompaction || !
|
|
993
|
+
if (!shouldOverrideDefaultCompaction || !goosedumpAvailable) return;
|
|
879
994
|
if (event.customInstructions?.trim()) return;
|
|
880
995
|
if (event.signal.aborted) return;
|
|
881
996
|
|
|
@@ -905,7 +1020,7 @@ export function createGoosedumpIntegration(
|
|
|
905
1020
|
});
|
|
906
1021
|
|
|
907
1022
|
pi.on('session_compact', async (event, ctx) => {
|
|
908
|
-
if (!
|
|
1023
|
+
if (!goosedumpAvailable || !event.fromExtension) return;
|
|
909
1024
|
|
|
910
1025
|
const compactionEntry = event.compactionEntry;
|
|
911
1026
|
if (!compactionEntry) return;
|
|
@@ -929,7 +1044,7 @@ export function createGoosedumpIntegration(
|
|
|
929
1044
|
maybePi.registerCommand?.('goosedump', {
|
|
930
1045
|
description: 'Browse coding agent session history',
|
|
931
1046
|
handler: async (_args, ctx) => {
|
|
932
|
-
if (!
|
|
1047
|
+
if (!goosedumpAvailable) {
|
|
933
1048
|
ctx.ui.notify(
|
|
934
1049
|
'goosedump is not available. Install with: npm install @jarkkojs/goosedump',
|
|
935
1050
|
'error',
|
|
@@ -945,200 +1060,7 @@ export function createGoosedumpIntegration(
|
|
|
945
1060
|
return;
|
|
946
1061
|
}
|
|
947
1062
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
await ctx.ui.custom<void>(
|
|
951
|
-
(tui, theme, _kb, done) => {
|
|
952
|
-
let selectedIndex = 0;
|
|
953
|
-
let mode: 'list' | 'compact' = 'list';
|
|
954
|
-
let compactLines: string[] = [];
|
|
955
|
-
let compactScroll = 0;
|
|
956
|
-
let compactTitle = '';
|
|
957
|
-
|
|
958
|
-
const COMPACT_VIEWPORT = 20;
|
|
959
|
-
|
|
960
|
-
return {
|
|
961
|
-
render(width: number): string[] {
|
|
962
|
-
const innerW = width - 4;
|
|
963
|
-
const border = theme.fg('border', '│');
|
|
964
|
-
const borderFg = (s: string) => theme.fg('border', s);
|
|
965
|
-
const dim = (s: string) => theme.fg('dim', s);
|
|
966
|
-
const accent = (s: string) => theme.fg('accent', s);
|
|
967
|
-
const text = (s: string) => theme.fg('text', s);
|
|
968
|
-
const muted = (s: string) => theme.fg('muted', s);
|
|
969
|
-
|
|
970
|
-
const lines: string[] = [];
|
|
971
|
-
|
|
972
|
-
if (mode === 'compact') {
|
|
973
|
-
const title = accent(` Compact: ${truncateToWidth(compactTitle, 40)} `);
|
|
974
|
-
const topFill = borderFg(
|
|
975
|
-
'─'.repeat(Math.max(0, width - 4 - visibleWidth(title))),
|
|
976
|
-
);
|
|
977
|
-
lines.push(`${borderFg('╭─')}${title}${topFill}${borderFg('─╮')}`);
|
|
978
|
-
|
|
979
|
-
const total = compactLines.length;
|
|
980
|
-
const maxScroll = Math.max(0, total - COMPACT_VIEWPORT);
|
|
981
|
-
if (compactScroll > maxScroll) compactScroll = maxScroll;
|
|
982
|
-
const window = compactLines.slice(
|
|
983
|
-
compactScroll,
|
|
984
|
-
compactScroll + COMPACT_VIEWPORT,
|
|
985
|
-
);
|
|
986
|
-
for (const row of window) {
|
|
987
|
-
lines.push(makeRow(text(truncateToWidth(row, innerW)), innerW, border));
|
|
988
|
-
}
|
|
989
|
-
for (let i = window.length; i < COMPACT_VIEWPORT; i++) {
|
|
990
|
-
lines.push(makeRow('', innerW, border));
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
const position =
|
|
994
|
-
total > COMPACT_VIEWPORT
|
|
995
|
-
? `${compactScroll + 1}-${Math.min(compactScroll + COMPACT_VIEWPORT, total)} of ${total}`
|
|
996
|
-
: `${total} line${total === 1 ? '' : 's'}`;
|
|
997
|
-
lines.push(makeRow('', innerW, border));
|
|
998
|
-
lines.push(makeRow(dim(`↑↓ scroll esc back (${position})`), innerW, border));
|
|
999
|
-
lines.push(
|
|
1000
|
-
`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`,
|
|
1001
|
-
);
|
|
1002
|
-
return lines;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
// Top border
|
|
1006
|
-
const title = accent(' Goosedump Sessions ');
|
|
1007
|
-
const topFill = borderFg(
|
|
1008
|
-
'─'.repeat(Math.max(0, width - 4 - visibleWidth(title))),
|
|
1009
|
-
);
|
|
1010
|
-
lines.push(`${borderFg('╭─')}${title}${topFill}${borderFg('─╮')}`);
|
|
1011
|
-
|
|
1012
|
-
// Session count
|
|
1013
|
-
const countInfo = `${listings.length} session${listings.length === 1 ? '' : 's'}`;
|
|
1014
|
-
lines.push(makeRow(`${muted(countInfo)}`, innerW, border));
|
|
1015
|
-
|
|
1016
|
-
lines.push(makeRow('', innerW, border));
|
|
1017
|
-
|
|
1018
|
-
// Session list
|
|
1019
|
-
const maxItems = Math.min(listings.length, 20);
|
|
1020
|
-
for (let i = 0; i < maxItems; i++) {
|
|
1021
|
-
const listing = listings[i];
|
|
1022
|
-
const isSelected = i === selectedIndex;
|
|
1023
|
-
const cursor = isSelected ? accent('▶') : ' ';
|
|
1024
|
-
const idText = isSelected ? accent(listing.id) : text(listing.id);
|
|
1025
|
-
const line = ` ${cursor} ${idText}`;
|
|
1026
|
-
lines.push(makeRow(truncateToWidth(line, innerW), innerW, border));
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
if (listings.length > 20) {
|
|
1030
|
-
lines.push(
|
|
1031
|
-
makeRow(dim(` ... and ${listings.length - 20} more`), innerW, border),
|
|
1032
|
-
);
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// Footer
|
|
1036
|
-
lines.push(makeRow('', innerW, border));
|
|
1037
|
-
lines.push(
|
|
1038
|
-
makeRow(
|
|
1039
|
-
dim('↑↓ navigate enter compact shift+enter resume esc close'),
|
|
1040
|
-
innerW,
|
|
1041
|
-
border,
|
|
1042
|
-
),
|
|
1043
|
-
);
|
|
1044
|
-
|
|
1045
|
-
// Bottom border
|
|
1046
|
-
lines.push(`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`);
|
|
1047
|
-
|
|
1048
|
-
return lines;
|
|
1049
|
-
},
|
|
1050
|
-
|
|
1051
|
-
handleInput(data: string): void {
|
|
1052
|
-
if (mode === 'compact') {
|
|
1053
|
-
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
|
|
1054
|
-
mode = 'list';
|
|
1055
|
-
tui.requestRender();
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
if (matchesKey(data, Key.up)) {
|
|
1059
|
-
compactScroll = Math.max(0, compactScroll - 1);
|
|
1060
|
-
tui.requestRender();
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
if (matchesKey(data, Key.down)) {
|
|
1064
|
-
compactScroll += 1;
|
|
1065
|
-
tui.requestRender();
|
|
1066
|
-
return;
|
|
1067
|
-
}
|
|
1068
|
-
if (matchesKey(data, Key.pageUp)) {
|
|
1069
|
-
compactScroll = Math.max(0, compactScroll - COMPACT_VIEWPORT);
|
|
1070
|
-
tui.requestRender();
|
|
1071
|
-
return;
|
|
1072
|
-
}
|
|
1073
|
-
if (matchesKey(data, Key.pageDown)) {
|
|
1074
|
-
compactScroll += COMPACT_VIEWPORT;
|
|
1075
|
-
tui.requestRender();
|
|
1076
|
-
return;
|
|
1077
|
-
}
|
|
1078
|
-
return;
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
|
|
1082
|
-
done(undefined);
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
if (matchesKey(data, Key.up)) {
|
|
1087
|
-
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
1088
|
-
tui.requestRender();
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
if (matchesKey(data, Key.down)) {
|
|
1093
|
-
selectedIndex = Math.min(listings.length - 1, selectedIndex + 1);
|
|
1094
|
-
tui.requestRender();
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
if (matchesKey(data, Key.shift(Key.enter))) {
|
|
1099
|
-
resumePath = listings[selectedIndex]?.path;
|
|
1100
|
-
done(undefined);
|
|
1101
|
-
return;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
|
|
1105
|
-
const listing = listings[selectedIndex];
|
|
1106
|
-
if (!listing) return;
|
|
1107
|
-
compactTitle = listing.id;
|
|
1108
|
-
compactScroll = 0;
|
|
1109
|
-
try {
|
|
1110
|
-
const summary = goosedumpCompact(listing.id, { scope: 'lineage' }).trim();
|
|
1111
|
-
compactLines = summary
|
|
1112
|
-
? summary.split('\n')
|
|
1113
|
-
: ['No compact summary available for this session.'];
|
|
1114
|
-
} catch (err) {
|
|
1115
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1116
|
-
compactLines = [`Failed to compact session: ${msg}`];
|
|
1117
|
-
}
|
|
1118
|
-
mode = 'compact';
|
|
1119
|
-
tui.requestRender();
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1122
|
-
},
|
|
1123
|
-
|
|
1124
|
-
invalidate(): void {},
|
|
1125
|
-
};
|
|
1126
|
-
|
|
1127
|
-
function makeRow(content: string, innerW: number, border: string): string {
|
|
1128
|
-
const line = truncateToWidth(content, innerW);
|
|
1129
|
-
const pad = Math.max(0, innerW - visibleWidth(line));
|
|
1130
|
-
return `${border} ${line}${' '.repeat(pad)} ${border}`;
|
|
1131
|
-
}
|
|
1132
|
-
},
|
|
1133
|
-
{
|
|
1134
|
-
overlay: true,
|
|
1135
|
-
overlayOptions: {
|
|
1136
|
-
anchor: 'center',
|
|
1137
|
-
width: 72,
|
|
1138
|
-
margin: 2,
|
|
1139
|
-
},
|
|
1140
|
-
},
|
|
1141
|
-
);
|
|
1063
|
+
const resumePath = await runSessionBrowser(ctx, listings);
|
|
1142
1064
|
|
|
1143
1065
|
if (resumePath) {
|
|
1144
1066
|
await ctx.switchSession(resumePath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-goosedump",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Coding agent context data browser plugin for pi",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"goosedump",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@earendil-works/pi-tui": "^0.78.0",
|
|
32
|
-
"@jarkkojs/goosedump": "^0.5.
|
|
32
|
+
"@jarkkojs/goosedump": "^0.5.1",
|
|
33
33
|
"@sinclair/typebox": "^0.34.49"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|