pi-goosedump 0.6.4 → 0.7.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 +92 -9
  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,
@@ -662,8 +664,46 @@ async function runSessionBrowser(
662
664
 
663
665
  const OVERLAY_WIDTH = 72;
664
666
 
667
+ // Build tree from flat listings using parent relationships.
668
+ const listingIds = new Set(listings.map((l) => l.id));
669
+ const roots = listings.filter((l) => l.parent === null || !listingIds.has(l.parent));
670
+ const childrenMap = new Map<string, GoosedumpListing[]>();
671
+ for (const l of listings) {
672
+ if (l.parent !== null) {
673
+ const siblings = childrenMap.get(l.parent) ?? [];
674
+ siblings.push(l);
675
+ childrenMap.set(l.parent, siblings);
676
+ }
677
+ }
678
+
679
+ function truncateCwd(cwd: string, maxLen: number): string {
680
+ if (cwd.length <= maxLen) return cwd;
681
+ const head = Math.floor(maxLen / 2) + 2;
682
+ const tail = maxLen - head - 3;
683
+ if (tail < 4) return cwd.slice(0, maxLen - 3) + '...';
684
+ return cwd.slice(0, head) + '...' + cwd.slice(cwd.length - tail);
685
+ }
686
+
687
+ function flattenTree(
688
+ nodes: GoosedumpListing[],
689
+ expanded: Set<string>,
690
+ depth: number,
691
+ ): { listing: GoosedumpListing; depth: number; hasChildren: boolean }[] {
692
+ const result: { listing: GoosedumpListing; depth: number; hasChildren: boolean }[] = [];
693
+ for (const node of nodes) {
694
+ const children = childrenMap.get(node.id) ?? [];
695
+ result.push({ listing: node, depth, hasChildren: children.length > 0 });
696
+ if (children.length > 0 && expanded.has(node.id)) {
697
+ result.push(...flattenTree(children, expanded, depth + 1));
698
+ }
699
+ }
700
+ return result;
701
+ }
702
+
665
703
  await ctx.ui.custom<void>(
666
704
  (tui, theme, _kb, done) => {
705
+ let expandedNodes = new Set<string>();
706
+ let displayList = flattenTree(roots, expandedNodes, 0);
667
707
  let selectedIndex = 0;
668
708
  let listScroll = 0;
669
709
  let mode: 'list' | 'compact' = 'list';
@@ -675,6 +715,10 @@ async function runSessionBrowser(
675
715
  const LIST_VIEWPORT = 20;
676
716
  const wrapWidth = OVERLAY_WIDTH - 4;
677
717
 
718
+ function rebuildDisplayList(): void {
719
+ displayList = flattenTree(roots, expandedNodes, 0);
720
+ }
721
+
678
722
  return {
679
723
  render(width: number): string[] {
680
724
  const innerW = width - 4;
@@ -723,15 +767,29 @@ async function runSessionBrowser(
723
767
  lines.push(makeRow('', innerW, border));
724
768
 
725
769
  // Session list
726
- const total = listings.length;
727
- const window = listings.slice(listScroll, listScroll + LIST_VIEWPORT);
770
+ const total = displayList.length;
771
+ const window = displayList.slice(listScroll, listScroll + LIST_VIEWPORT);
728
772
  for (let i = 0; i < window.length; i++) {
729
773
  const absolute = listScroll + i;
730
- const listing = window[i];
774
+ const entry = window[i];
731
775
  const isSelected = absolute === selectedIndex;
776
+
732
777
  const cursor = isSelected ? accent('▶') : ' ';
733
- const idText = isSelected ? accent(listing.id) : text(listing.id);
734
- const line = ` ${cursor} ${idText}`;
778
+ const indent = ' '.repeat(entry.depth * 2);
779
+ const toggle = entry.hasChildren
780
+ ? expandedNodes.has(entry.listing.id)
781
+ ? muted('−')
782
+ : muted('+')
783
+ : ' ';
784
+ const idText = isSelected ? accent(entry.listing.id) : text(entry.listing.id);
785
+
786
+ // Prefix: " ▶" (3) + indent + toggle + " " + id (8) + " "
787
+ const prefixLen = 3 + entry.depth * 2 + 1 + 1 + 8 + 1;
788
+ const cwdMax = Math.max(8, innerW - prefixLen);
789
+ const cwdTruncated = truncateCwd(entry.listing.cwd, cwdMax);
790
+ const cwdText = isSelected ? accent(cwdTruncated) : muted(cwdTruncated);
791
+
792
+ const line = ` ${cursor}${indent}${toggle} ${idText} ${cwdText}`;
735
793
  lines.push(makeRow(line, innerW, border));
736
794
  }
737
795
 
@@ -744,7 +802,7 @@ async function runSessionBrowser(
744
802
  lines.push(makeRow('', innerW, border));
745
803
  lines.push(
746
804
  makeRow(
747
- dim('↑↓ navigate enter compact shift+enter resume esc close'),
805
+ dim('↑↓ navigate space/+/− fold enter compact shift+enter resume esc close'),
748
806
  innerW,
749
807
  border,
750
808
  ),
@@ -800,7 +858,7 @@ async function runSessionBrowser(
800
858
  }
801
859
 
802
860
  if (matchesKey(data, Key.down)) {
803
- selectedIndex = Math.min(listings.length - 1, selectedIndex + 1);
861
+ selectedIndex = Math.min(displayList.length - 1, selectedIndex + 1);
804
862
  if (selectedIndex >= listScroll + LIST_VIEWPORT) {
805
863
  listScroll = selectedIndex - LIST_VIEWPORT + 1;
806
864
  }
@@ -808,14 +866,39 @@ async function runSessionBrowser(
808
866
  return;
809
867
  }
810
868
 
869
+ // Expand/collapse: space, +, or - key
870
+ const entry = displayList[selectedIndex];
871
+ if (entry && entry.hasChildren && (data === ' ' || data === '+' || data === '-')) {
872
+ const id = entry.listing.id;
873
+ if (data === '+') {
874
+ expandedNodes.add(id);
875
+ } else if (data === '-') {
876
+ expandedNodes.delete(id);
877
+ } else {
878
+ // space = toggle
879
+ if (expandedNodes.has(id)) {
880
+ expandedNodes.delete(id);
881
+ } else {
882
+ expandedNodes.add(id);
883
+ }
884
+ }
885
+ rebuildDisplayList();
886
+ // Keep selection on the same entry
887
+ const newIndex = displayList.findIndex((d) => d.listing.id === id);
888
+ if (newIndex >= 0) selectedIndex = newIndex;
889
+ if (selectedIndex < listScroll) listScroll = selectedIndex;
890
+ tui.requestRender();
891
+ return;
892
+ }
893
+
811
894
  if (matchesKey(data, Key.shift(Key.enter))) {
812
- resumePath = listings[selectedIndex]?.path;
895
+ resumePath = displayList[selectedIndex]?.listing.path;
813
896
  done(undefined);
814
897
  return;
815
898
  }
816
899
 
817
900
  if (matchesKey(data, Key.enter) || matchesKey(data, Key.return)) {
818
- const listing = listings[selectedIndex];
901
+ const listing = displayList[selectedIndex]?.listing;
819
902
  if (!listing) return;
820
903
  compactTitle = listing.id;
821
904
  compactScroll = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.6.4",
3
+ "version": "0.7.1",
4
4
  "description": "Coding agent context data browser plugin for pi",
5
5
  "keywords": [
6
6
  "goosedump",