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.
Files changed (2) hide show
  1. package/index.ts +292 -370
  2. 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, 0] as const;
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 GoosedumpContextResult {
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 json = JSON.parse(runGoosedump(['list', 'pi:*'])) as ListJson;
244
- return (json.sessions ?? []).map((session) => ({
245
- id: session.id,
246
- path: session.path ?? null,
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
- function goosedumpContext(
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
- options: {
253
- scope?: string;
254
- grep?: string;
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 hasQuery = typeof options.grep === 'string';
262
- const isSearch = hasQuery && options.rank !== false;
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 output = runGoosedump(args);
279
-
280
- if (hasQuery) {
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 goosedumpExpand(
300
+ function goosedumpShow(
294
301
  contextId: string,
295
- entryIds: string[],
296
- scope: string = 'lineage',
302
+ options: { scope?: string; ids?: string[] } = {},
297
303
  ): GoosedumpMessage[] {
298
- if (entryIds.length === 0) return [];
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 goosedumpReady = false;
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 (!goosedumpEnabled || !goosedumpReady) {
694
- return {
695
- content: [
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 = goosedumpContext(contextId, {
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 = goosedumpExpand(
798
- contextId,
799
- params.ids,
800
- params.scope ?? 'lineage',
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 result = goosedumpContext(contextId, {
831
- scope: params.scope ?? 'lineage',
832
- });
960
+ const messages = goosedumpShow(contextId, { scope: params.scope ?? 'lineage' });
833
961
 
834
- if (result.messages.length === 0) {
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
- goosedumpReady = checkGoosedump(ctx);
867
- goosedumpEnabled = goosedumpReady;
982
+ goosedumpAvailable = checkGoosedump(ctx);
868
983
 
869
- if (goosedumpReady) {
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 || !goosedumpEnabled || !goosedumpReady) return;
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 (!goosedumpEnabled || !goosedumpReady || !event.fromExtension) return;
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 (!goosedumpEnabled || !goosedumpReady) {
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
- let resumePath: string | null | undefined;
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.0",
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.0",
32
+ "@jarkkojs/goosedump": "^0.5.1",
33
33
  "@sinclair/typebox": "^0.34.49"
34
34
  },
35
35
  "devDependencies": {