pi-goosedump 0.7.0 → 0.7.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 +180 -10
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -49,6 +49,7 @@ interface GoosedumpListing {
49
49
  provider: string;
50
50
  nativeId: string;
51
51
  cwd: string;
52
+ parent: string | null;
52
53
  label: string | null;
53
54
  path: string | null;
54
55
  }
@@ -273,6 +274,7 @@ function goosedumpList(provider = 'pi'): GoosedumpListing[] {
273
274
  provider: entry.provider,
274
275
  nativeId: entry.native_id,
275
276
  cwd: entry.cwd,
277
+ parent: entry.parent,
276
278
  label: entry.label ?? null,
277
279
  // Only pi sessions map back to a local file we can resume.
278
280
  path: provider === 'pi' ? piSessionFilePath(entry.native_id) : null,
@@ -414,6 +416,13 @@ function goosedumpCompact(
414
416
  return renderCompactSummary(json);
415
417
  }
416
418
 
419
+ function goosedumpDelete(id: string, options: { dryRun?: boolean } = {}): string {
420
+ const args = ['delete'];
421
+ if (options.dryRun) args.push('--dry-run');
422
+ args.push(id);
423
+ return runGoosedump(args);
424
+ }
425
+
417
426
  export interface GoosedumpIntegrationOptions {
418
427
  registerTool?: boolean;
419
428
  overrideDefaultCompaction?: boolean;
@@ -662,19 +671,96 @@ async function runSessionBrowser(
662
671
 
663
672
  const OVERLAY_WIDTH = 72;
664
673
 
674
+ // Build tree from flat listings using parent relationships.
675
+ let listingIds = new Set(listings.map((l) => l.id));
676
+ let roots = listings.filter((l) => l.parent === null || !listingIds.has(l.parent));
677
+ let childrenMap = new Map<string, GoosedumpListing[]>();
678
+ for (const l of listings) {
679
+ if (l.parent !== null) {
680
+ const siblings = childrenMap.get(l.parent) ?? [];
681
+ siblings.push(l);
682
+ childrenMap.set(l.parent, siblings);
683
+ }
684
+ }
685
+
686
+ function rebuildTree(): void {
687
+ listingIds = new Set(listings.map((l) => l.id));
688
+ roots = listings.filter((l) => l.parent === null || !listingIds.has(l.parent));
689
+ childrenMap = new Map<string, GoosedumpListing[]>();
690
+ for (const l of listings) {
691
+ if (l.parent !== null) {
692
+ const siblings = childrenMap.get(l.parent) ?? [];
693
+ siblings.push(l);
694
+ childrenMap.set(l.parent, siblings);
695
+ }
696
+ }
697
+ }
698
+
699
+ function truncateCwd(cwd: string, maxLen: number): string {
700
+ if (cwd.length <= maxLen) return cwd;
701
+ const head = Math.floor(maxLen / 2) + 2;
702
+ const tail = maxLen - head - 3;
703
+ if (tail < 4) return cwd.slice(0, maxLen - 3) + '...';
704
+ return cwd.slice(0, head) + '...' + cwd.slice(cwd.length - tail);
705
+ }
706
+
707
+ function flattenTree(
708
+ nodes: GoosedumpListing[],
709
+ expanded: Set<string>,
710
+ depth: number,
711
+ ): { listing: GoosedumpListing; depth: number; hasChildren: boolean }[] {
712
+ const result: { listing: GoosedumpListing; depth: number; hasChildren: boolean }[] = [];
713
+ for (const node of nodes) {
714
+ const children = childrenMap.get(node.id) ?? [];
715
+ result.push({ listing: node, depth, hasChildren: children.length > 0 });
716
+ if (children.length > 0 && expanded.has(node.id)) {
717
+ result.push(...flattenTree(children, expanded, depth + 1));
718
+ }
719
+ }
720
+ return result;
721
+ }
722
+
665
723
  await ctx.ui.custom<void>(
666
724
  (tui, theme, _kb, done) => {
725
+ let expandedNodes = new Set<string>();
726
+ let displayList = flattenTree(roots, expandedNodes, 0);
667
727
  let selectedIndex = 0;
668
728
  let listScroll = 0;
669
729
  let mode: 'list' | 'compact' = 'list';
670
730
  let compactLines: string[] = [];
671
731
  let compactScroll = 0;
672
732
  let compactTitle = '';
733
+ let pendingDeletionId: string | null = null;
673
734
 
674
735
  const COMPACT_VIEWPORT = 20;
675
736
  const LIST_VIEWPORT = 20;
676
737
  const wrapWidth = OVERLAY_WIDTH - 4;
677
738
 
739
+ function rebuildDisplayList(): void {
740
+ displayList = flattenTree(roots, expandedNodes, 0);
741
+ }
742
+
743
+ function executeDeletion(): void {
744
+ try {
745
+ const id = pendingDeletionId!;
746
+ goosedumpDelete(id);
747
+ // Remove from listings and rebuild tree
748
+ const idx = listings.findIndex((l) => l.id === id);
749
+ if (idx >= 0) listings.splice(idx, 1);
750
+ rebuildTree();
751
+ rebuildDisplayList();
752
+ if (selectedIndex >= displayList.length) {
753
+ selectedIndex = Math.max(0, displayList.length - 1);
754
+ }
755
+ pendingDeletionId = null;
756
+ } catch (err) {
757
+ const msg = err instanceof Error ? err.message : String(err);
758
+ ctx.ui.notify(`Failed to delete session: ${msg}`, 'error');
759
+ pendingDeletionId = null;
760
+ }
761
+ tui.requestRender();
762
+ }
763
+
678
764
  return {
679
765
  render(width: number): string[] {
680
766
  const innerW = width - 4;
@@ -723,15 +809,38 @@ async function runSessionBrowser(
723
809
  lines.push(makeRow('', innerW, border));
724
810
 
725
811
  // Session list
726
- const total = listings.length;
727
- const window = listings.slice(listScroll, listScroll + LIST_VIEWPORT);
812
+ const total = displayList.length;
813
+ const window = displayList.slice(listScroll, listScroll + LIST_VIEWPORT);
728
814
  for (let i = 0; i < window.length; i++) {
729
815
  const absolute = listScroll + i;
730
- const listing = window[i];
816
+ const entry = window[i];
731
817
  const isSelected = absolute === selectedIndex;
732
- const cursor = isSelected ? accent('▶') : ' ';
733
- const idText = isSelected ? accent(listing.id) : text(listing.id);
734
- const line = ` ${cursor} ${idText}`;
818
+ const deletePending =
819
+ pendingDeletionId !== null && entry.listing.id === pendingDeletionId;
820
+ const cursor = deletePending ? dim('␡ ') : isSelected ? accent('▶') : ' ';
821
+ const indent = ' '.repeat(entry.depth * 2);
822
+ const toggle = entry.hasChildren
823
+ ? expandedNodes.has(entry.listing.id)
824
+ ? muted('−')
825
+ : muted('+')
826
+ : ' ';
827
+ const idText = deletePending
828
+ ? dim(entry.listing.id)
829
+ : isSelected
830
+ ? accent(entry.listing.id)
831
+ : text(entry.listing.id);
832
+
833
+ // Prefix: " ▶" (3) or "␡ " (3) + indent + toggle + " " + id (8) + " "
834
+ const prefixLen = 3 + entry.depth * 2 + 1 + 1 + 8 + 1;
835
+ const cwdMax = Math.max(8, innerW - prefixLen);
836
+ const cwdTruncated = truncateCwd(entry.listing.cwd, cwdMax);
837
+ const cwdText = deletePending
838
+ ? muted(cwdTruncated)
839
+ : isSelected
840
+ ? accent(cwdTruncated)
841
+ : muted(cwdTruncated);
842
+
843
+ const line = ` ${cursor}${indent}${toggle} ${idText} ${cwdText}`;
735
844
  lines.push(makeRow(line, innerW, border));
736
845
  }
737
846
 
@@ -740,11 +849,25 @@ async function runSessionBrowser(
740
849
  lines.push(makeRow(dim(` (${position})`), innerW, border));
741
850
  }
742
851
 
852
+ // Deletion confirmation
853
+ if (pendingDeletionId) {
854
+ lines.push(makeRow('', innerW, border));
855
+ lines.push(
856
+ makeRow(
857
+ dim(` Delete session ${pendingDeletionId}? enter=confirm esc=cancel`),
858
+ innerW,
859
+ border,
860
+ ),
861
+ );
862
+ }
863
+
743
864
  // Footer
744
865
  lines.push(makeRow('', innerW, border));
745
866
  lines.push(
746
867
  makeRow(
747
- dim('↑↓ navigate enter compact shift+enter resume esc close'),
868
+ dim(
869
+ '↑↓ navigate space/+/− fold enter compact shift+enter resume ctrl+d delete esc close',
870
+ ),
748
871
  innerW,
749
872
  border,
750
873
  ),
@@ -800,7 +923,7 @@ async function runSessionBrowser(
800
923
  }
801
924
 
802
925
  if (matchesKey(data, Key.down)) {
803
- selectedIndex = Math.min(listings.length - 1, selectedIndex + 1);
926
+ selectedIndex = Math.min(displayList.length - 1, selectedIndex + 1);
804
927
  if (selectedIndex >= listScroll + LIST_VIEWPORT) {
805
928
  listScroll = selectedIndex - LIST_VIEWPORT + 1;
806
929
  }
@@ -808,14 +931,61 @@ async function runSessionBrowser(
808
931
  return;
809
932
  }
810
933
 
934
+ // Deletion confirmation: enter confirms, esc cancels
935
+ if (pendingDeletionId) {
936
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
937
+ pendingDeletionId = null;
938
+ tui.requestRender();
939
+ return;
940
+ }
941
+ if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
942
+ executeDeletion();
943
+ return;
944
+ }
945
+ return;
946
+ }
947
+
948
+ // Ctrl+D marks session for deletion
949
+ if (matchesKey(data, Key.ctrl('d'))) {
950
+ const listing = displayList[selectedIndex]?.listing;
951
+ if (listing) pendingDeletionId = listing.id;
952
+ tui.requestRender();
953
+ return;
954
+ }
955
+
956
+ // Expand/collapse: space, +, or - key
957
+ const entry = displayList[selectedIndex];
958
+ if (entry && entry.hasChildren && (data === ' ' || data === '+' || data === '-')) {
959
+ const id = entry.listing.id;
960
+ if (data === '+') {
961
+ expandedNodes.add(id);
962
+ } else if (data === '-') {
963
+ expandedNodes.delete(id);
964
+ } else {
965
+ // space = toggle
966
+ if (expandedNodes.has(id)) {
967
+ expandedNodes.delete(id);
968
+ } else {
969
+ expandedNodes.add(id);
970
+ }
971
+ }
972
+ rebuildDisplayList();
973
+ // Keep selection on the same entry
974
+ const newIndex = displayList.findIndex((d) => d.listing.id === id);
975
+ if (newIndex >= 0) selectedIndex = newIndex;
976
+ if (selectedIndex < listScroll) listScroll = selectedIndex;
977
+ tui.requestRender();
978
+ return;
979
+ }
980
+
811
981
  if (matchesKey(data, Key.shift(Key.enter))) {
812
- resumePath = listings[selectedIndex]?.path;
982
+ resumePath = displayList[selectedIndex]?.listing.path;
813
983
  done(undefined);
814
984
  return;
815
985
  }
816
986
 
817
987
  if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
818
- const listing = listings[selectedIndex];
988
+ const listing = displayList[selectedIndex]?.listing;
819
989
  if (!listing) return;
820
990
  compactTitle = listing.id;
821
991
  compactScroll = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Coding agent context data browser plugin for pi",
5
5
  "keywords": [
6
6
  "goosedump",