pi-goosedump 0.5.0 → 0.5.1

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 +249 -359
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -37,9 +37,8 @@ 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
  }
@@ -87,10 +86,6 @@ interface SearchJson {
87
86
  hits: HitJson[];
88
87
  }
89
88
 
90
- interface GrepJson {
91
- hits: HitJson[];
92
- }
93
-
94
89
  interface CompactJson {
95
90
  goals: string[];
96
91
  files: { modified: string[]; created: string[]; read: string[] };
@@ -233,6 +228,14 @@ function formatMessagesFull(messages: GoosedumpMessage[]): string {
233
228
  return messages.map((m) => `[${m.id}] ${m.role}:\n${m.content}`).join('\n\n---\n\n');
234
229
  }
235
230
 
231
+ function textResult(text: string): AgentToolResult<void> {
232
+ return { content: [{ type: 'text', text }], details: undefined };
233
+ }
234
+
235
+ function missingContextId(action: string): AgentToolResult<void> {
236
+ return textResult(`contextId is required for ${action} when no current session is available`);
237
+ }
238
+
236
239
  function runGoosedump(args: string[]): string {
237
240
  return execFileSync(process.execPath, [binaryPath(), ...args], {
238
241
  encoding: 'utf-8',
@@ -247,59 +250,31 @@ function goosedumpList(): GoosedumpListing[] {
247
250
  }));
248
251
  }
249
252
 
250
- function goosedumpContext(
253
+ function goosedumpSearch(
251
254
  contextId: string,
252
- options: {
253
- scope?: string;
254
- grep?: string;
255
- ids?: string;
256
- rank?: boolean;
257
- page?: number;
258
- } = {},
259
- ): GoosedumpContextResult {
255
+ query: string,
256
+ options: { scope?: string; page?: number } = {},
257
+ ): GoosedumpSearchResult {
260
258
  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
- }
259
+ const args = ['search', `pi:${contextId}`, query];
260
+ if (options.page) args.push('--page', String(options.page));
276
261
  args.push('--scope', scope);
277
262
 
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 };
263
+ const json = JSON.parse(runGoosedump(args)) as SearchJson;
264
+ const messages = (json.hits ?? []).map(hitMessage);
265
+ return { messages, page: json.page ?? options.page ?? 1, totalPages: json.totalPages ?? 1 };
291
266
  }
292
267
 
293
- function goosedumpExpand(
268
+ function goosedumpShow(
294
269
  contextId: string,
295
- entryIds: string[],
296
- scope: string = 'lineage',
270
+ options: { scope?: string; ids?: string[] } = {},
297
271
  ): GoosedumpMessage[] {
298
- if (entryIds.length === 0) return [];
272
+ const scope = options.scope ?? 'lineage';
273
+ const args = ['show', `pi:${contextId}`];
274
+ if (options.ids && options.ids.length > 0) args.push('--ids', options.ids.join(','));
275
+ args.push('--scope', scope);
299
276
 
300
- const json = JSON.parse(
301
- runGoosedump(['show', `pi:${contextId}`, '--ids', entryIds.join(','), '--scope', scope]),
302
- ) as ShowJson;
277
+ const json = JSON.parse(runGoosedump(args)) as ShowJson;
303
278
  return (json.messages ?? []).map(showMessage);
304
279
  }
305
280
 
@@ -592,14 +567,204 @@ function buildGoosedumpCompaction(
592
567
  };
593
568
  }
594
569
 
570
+ async function runSessionBrowser(
571
+ ctx: ExtensionContext,
572
+ listings: GoosedumpListing[],
573
+ ): Promise<string | null | undefined> {
574
+ let resumePath: string | null | undefined;
575
+
576
+ await ctx.ui.custom<void>(
577
+ (tui, theme, _kb, done) => {
578
+ let selectedIndex = 0;
579
+ let mode: 'list' | 'compact' = 'list';
580
+ let compactLines: string[] = [];
581
+ let compactScroll = 0;
582
+ let compactTitle = '';
583
+
584
+ const COMPACT_VIEWPORT = 20;
585
+
586
+ return {
587
+ render(width: number): string[] {
588
+ const innerW = width - 4;
589
+ const border = theme.fg('border', '│');
590
+ const borderFg = (s: string) => theme.fg('border', s);
591
+ const dim = (s: string) => theme.fg('dim', s);
592
+ const accent = (s: string) => theme.fg('accent', s);
593
+ const text = (s: string) => theme.fg('text', s);
594
+ const muted = (s: string) => theme.fg('muted', s);
595
+
596
+ const lines: string[] = [];
597
+
598
+ if (mode === 'compact') {
599
+ const title = accent(` Compact: ${truncateToWidth(compactTitle, 40)} `);
600
+ const topFill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(title))));
601
+ lines.push(`${borderFg('╭─')}${title}${topFill}${borderFg('─╮')}`);
602
+
603
+ const total = compactLines.length;
604
+ const maxScroll = Math.max(0, total - COMPACT_VIEWPORT);
605
+ if (compactScroll > maxScroll) compactScroll = maxScroll;
606
+ const window = compactLines.slice(compactScroll, compactScroll + COMPACT_VIEWPORT);
607
+ for (const row of window) {
608
+ lines.push(makeRow(text(truncateToWidth(row, innerW)), innerW, border));
609
+ }
610
+ for (let i = window.length; i < COMPACT_VIEWPORT; i++) {
611
+ lines.push(makeRow('', innerW, border));
612
+ }
613
+
614
+ const position =
615
+ total > COMPACT_VIEWPORT
616
+ ? `${compactScroll + 1}-${Math.min(compactScroll + COMPACT_VIEWPORT, total)} of ${total}`
617
+ : `${total} line${total === 1 ? '' : 's'}`;
618
+ lines.push(makeRow('', innerW, border));
619
+ lines.push(makeRow(dim(`↑↓ scroll esc back (${position})`), innerW, border));
620
+ lines.push(`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`);
621
+ return lines;
622
+ }
623
+
624
+ // Top border
625
+ const title = accent(' Goosedump Sessions ');
626
+ const topFill = borderFg('─'.repeat(Math.max(0, width - 4 - visibleWidth(title))));
627
+ lines.push(`${borderFg('╭─')}${title}${topFill}${borderFg('─╮')}`);
628
+
629
+ // Session count
630
+ const countInfo = `${listings.length} session${listings.length === 1 ? '' : 's'}`;
631
+ lines.push(makeRow(`${muted(countInfo)}`, innerW, border));
632
+
633
+ lines.push(makeRow('', innerW, border));
634
+
635
+ // Session list
636
+ const maxItems = Math.min(listings.length, 20);
637
+ for (let i = 0; i < maxItems; i++) {
638
+ const listing = listings[i];
639
+ const isSelected = i === selectedIndex;
640
+ const cursor = isSelected ? accent('▶') : ' ';
641
+ const idText = isSelected ? accent(listing.id) : text(listing.id);
642
+ const line = ` ${cursor} ${idText}`;
643
+ lines.push(makeRow(truncateToWidth(line, innerW), innerW, border));
644
+ }
645
+
646
+ if (listings.length > 20) {
647
+ lines.push(makeRow(dim(` ... and ${listings.length - 20} more`), innerW, border));
648
+ }
649
+
650
+ // Footer
651
+ lines.push(makeRow('', innerW, border));
652
+ lines.push(
653
+ makeRow(
654
+ dim('↑↓ navigate enter compact shift+enter resume esc close'),
655
+ innerW,
656
+ border,
657
+ ),
658
+ );
659
+
660
+ // Bottom border
661
+ lines.push(`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`);
662
+
663
+ return lines;
664
+ },
665
+
666
+ handleInput(data: string): void {
667
+ if (mode === 'compact') {
668
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
669
+ mode = 'list';
670
+ tui.requestRender();
671
+ return;
672
+ }
673
+ if (matchesKey(data, Key.up)) {
674
+ compactScroll = Math.max(0, compactScroll - 1);
675
+ tui.requestRender();
676
+ return;
677
+ }
678
+ if (matchesKey(data, Key.down)) {
679
+ compactScroll += 1;
680
+ tui.requestRender();
681
+ return;
682
+ }
683
+ if (matchesKey(data, Key.pageUp)) {
684
+ compactScroll = Math.max(0, compactScroll - COMPACT_VIEWPORT);
685
+ tui.requestRender();
686
+ return;
687
+ }
688
+ if (matchesKey(data, Key.pageDown)) {
689
+ compactScroll += COMPACT_VIEWPORT;
690
+ tui.requestRender();
691
+ return;
692
+ }
693
+ return;
694
+ }
695
+
696
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
697
+ done(undefined);
698
+ return;
699
+ }
700
+
701
+ if (matchesKey(data, Key.up)) {
702
+ selectedIndex = Math.max(0, selectedIndex - 1);
703
+ tui.requestRender();
704
+ return;
705
+ }
706
+
707
+ if (matchesKey(data, Key.down)) {
708
+ selectedIndex = Math.min(listings.length - 1, selectedIndex + 1);
709
+ tui.requestRender();
710
+ return;
711
+ }
712
+
713
+ if (matchesKey(data, Key.shift(Key.enter))) {
714
+ resumePath = listings[selectedIndex]?.path;
715
+ done(undefined);
716
+ return;
717
+ }
718
+
719
+ if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
720
+ const listing = listings[selectedIndex];
721
+ if (!listing) return;
722
+ compactTitle = listing.id;
723
+ compactScroll = 0;
724
+ try {
725
+ const summary = goosedumpCompact(listing.id, { scope: 'lineage' }).trim();
726
+ compactLines = summary
727
+ ? summary.split('\n')
728
+ : ['No compact summary available for this session.'];
729
+ } catch (err) {
730
+ const msg = err instanceof Error ? err.message : String(err);
731
+ compactLines = [`Failed to compact session: ${msg}`];
732
+ }
733
+ mode = 'compact';
734
+ tui.requestRender();
735
+ return;
736
+ }
737
+ },
738
+
739
+ invalidate(): void {},
740
+ };
741
+
742
+ function makeRow(content: string, innerW: number, border: string): string {
743
+ const line = truncateToWidth(content, innerW);
744
+ const pad = Math.max(0, innerW - visibleWidth(line));
745
+ return `${border} ${line}${' '.repeat(pad)} ${border}`;
746
+ }
747
+ },
748
+ {
749
+ overlay: true,
750
+ overlayOptions: {
751
+ anchor: 'center',
752
+ width: 72,
753
+ margin: 2,
754
+ },
755
+ },
756
+ );
757
+
758
+ return resumePath;
759
+ }
760
+
595
761
  export function createGoosedumpIntegration(
596
762
  options: GoosedumpIntegrationOptions = {},
597
763
  ): GoosedumpIntegration {
598
764
  const shouldRegisterTool = options.registerTool ?? true;
599
765
  const shouldOverrideDefaultCompaction = options.overrideDefaultCompaction ?? true;
600
766
 
601
- let goosedumpReady = false;
602
- let goosedumpEnabled = false;
767
+ let goosedumpAvailable = false;
603
768
 
604
769
  function checkGoosedump(ctx?: ExtensionContext): boolean {
605
770
  const version = goosedumpVersion();
@@ -690,16 +855,10 @@ export function createGoosedumpIntegration(
690
855
  ),
691
856
  }),
692
857
  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
- };
858
+ if (!goosedumpAvailable) {
859
+ return textResult(
860
+ 'goosedump is not available. Install with: npm install @jarkkojs/goosedump',
861
+ );
703
862
  }
704
863
 
705
864
  try {
@@ -707,10 +866,7 @@ export function createGoosedumpIntegration(
707
866
  case 'list': {
708
867
  const listings = goosedumpList();
709
868
  if (listings.length === 0) {
710
- return {
711
- content: [{ type: 'text', text: 'No sessions found.' }],
712
- details: undefined,
713
- };
869
+ return textResult('No sessions found.');
714
870
  }
715
871
 
716
872
  const lines = ['Available sessions:', ''];
@@ -718,45 +874,22 @@ export function createGoosedumpIntegration(
718
874
  lines.push(` ${listing.id}`);
719
875
  }
720
876
 
721
- return {
722
- content: [{ type: 'text', text: lines.join('\n') }],
723
- details: undefined,
724
- };
877
+ return textResult(lines.join('\n'));
725
878
  }
726
879
 
727
880
  case 'search': {
728
881
  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
- }
882
+ if (!contextId) return missingContextId('search');
740
883
 
741
- if (!params.query) {
742
- return {
743
- content: [{ type: 'text', text: 'query is required for search' }],
744
- details: undefined,
745
- };
746
- }
884
+ if (!params.query) return textResult('query is required for search');
747
885
 
748
- const result = goosedumpContext(contextId, {
886
+ const result = goosedumpSearch(contextId, params.query, {
749
887
  scope: params.scope ?? 'lineage',
750
- grep: params.query,
751
- rank: true,
752
888
  page: params.page ?? 1,
753
889
  });
754
890
 
755
891
  if (result.messages.length === 0) {
756
- return {
757
- content: [{ type: 'text', text: `No results for "${params.query}"` }],
758
- details: undefined,
759
- };
892
+ return textResult(`No results for "${params.query}"`);
760
893
  }
761
894
 
762
895
  const useCompact = params.compact ?? true;
@@ -765,97 +898,48 @@ export function createGoosedumpIntegration(
765
898
  : formatMessagesFull(result.messages);
766
899
 
767
900
  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
- };
901
+ return textResult(header + text);
772
902
  }
773
903
 
774
904
  case 'expand': {
775
905
  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
- }
906
+ if (!contextId) return missingContextId('expand');
787
907
 
788
908
  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
- };
909
+ return textResult('ids is required for expand (array of entry IDs)');
795
910
  }
796
911
 
797
- const messages = goosedumpExpand(
798
- contextId,
799
- params.ids,
800
- params.scope ?? 'lineage',
801
- );
912
+ const messages = goosedumpShow(contextId, {
913
+ scope: params.scope ?? 'lineage',
914
+ ids: params.ids,
915
+ });
802
916
 
803
917
  if (messages.length === 0) {
804
- return {
805
- content: [{ type: 'text', text: 'No entries found for the given IDs.' }],
806
- details: undefined,
807
- };
918
+ return textResult('No entries found for the given IDs.');
808
919
  }
809
920
 
810
- return {
811
- content: [{ type: 'text', text: formatMessagesFull(messages) }],
812
- details: undefined,
813
- };
921
+ return textResult(formatMessagesFull(messages));
814
922
  }
815
923
 
816
924
  case 'view': {
817
925
  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
- }
926
+ if (!contextId) return missingContextId('view');
829
927
 
830
- const result = goosedumpContext(contextId, {
831
- scope: params.scope ?? 'lineage',
832
- });
928
+ const messages = goosedumpShow(contextId, { scope: params.scope ?? 'lineage' });
833
929
 
834
- if (result.messages.length === 0) {
835
- return {
836
- content: [{ type: 'text', text: `Session "${contextId}" has no messages.` }],
837
- details: undefined,
838
- };
930
+ if (messages.length === 0) {
931
+ return textResult(`Session "${contextId}" has no messages.`);
839
932
  }
840
933
 
841
- return {
842
- content: [{ type: 'text', text: formatMessagesFull(result.messages) }],
843
- details: undefined,
844
- };
934
+ return textResult(formatMessagesFull(messages));
845
935
  }
846
936
 
847
937
  default:
848
- return {
849
- content: [{ type: 'text', text: `Unknown action: ${params.action}` }],
850
- details: undefined,
851
- };
938
+ return textResult(`Unknown action: ${params.action}`);
852
939
  }
853
940
  } catch (err) {
854
941
  const message = err instanceof Error ? err.message : String(err);
855
- return {
856
- content: [{ type: 'text', text: `goosedump error: ${message}` }],
857
- details: undefined,
858
- };
942
+ return textResult(`goosedump error: ${message}`);
859
943
  }
860
944
  },
861
945
  }),
@@ -863,10 +947,9 @@ export function createGoosedumpIntegration(
863
947
  }
864
948
 
865
949
  pi.on('session_start', async (_event, ctx) => {
866
- goosedumpReady = checkGoosedump(ctx);
867
- goosedumpEnabled = goosedumpReady;
950
+ goosedumpAvailable = checkGoosedump(ctx);
868
951
 
869
- if (goosedumpReady) {
952
+ if (goosedumpAvailable) {
870
953
  const theme = ctx.ui.theme;
871
954
  const dot = theme.fg('success', '●');
872
955
  const label = theme.fg('text', 'Goosedump');
@@ -875,7 +958,7 @@ export function createGoosedumpIntegration(
875
958
  });
876
959
 
877
960
  pi.on('session_before_compact', async (event, ctx) => {
878
- if (!shouldOverrideDefaultCompaction || !goosedumpEnabled || !goosedumpReady) return;
961
+ if (!shouldOverrideDefaultCompaction || !goosedumpAvailable) return;
879
962
  if (event.customInstructions?.trim()) return;
880
963
  if (event.signal.aborted) return;
881
964
 
@@ -905,7 +988,7 @@ export function createGoosedumpIntegration(
905
988
  });
906
989
 
907
990
  pi.on('session_compact', async (event, ctx) => {
908
- if (!goosedumpEnabled || !goosedumpReady || !event.fromExtension) return;
991
+ if (!goosedumpAvailable || !event.fromExtension) return;
909
992
 
910
993
  const compactionEntry = event.compactionEntry;
911
994
  if (!compactionEntry) return;
@@ -929,7 +1012,7 @@ export function createGoosedumpIntegration(
929
1012
  maybePi.registerCommand?.('goosedump', {
930
1013
  description: 'Browse coding agent session history',
931
1014
  handler: async (_args, ctx) => {
932
- if (!goosedumpEnabled || !goosedumpReady) {
1015
+ if (!goosedumpAvailable) {
933
1016
  ctx.ui.notify(
934
1017
  'goosedump is not available. Install with: npm install @jarkkojs/goosedump',
935
1018
  'error',
@@ -945,200 +1028,7 @@ export function createGoosedumpIntegration(
945
1028
  return;
946
1029
  }
947
1030
 
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
- );
1031
+ const resumePath = await runSessionBrowser(ctx, listings);
1142
1032
 
1143
1033
  if (resumePath) {
1144
1034
  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.1",
4
4
  "description": "Coding agent context data browser plugin for pi",
5
5
  "keywords": [
6
6
  "goosedump",