pi-goosedump 0.5.4 → 0.5.5

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 (3) hide show
  1. package/README.md +2 -0
  2. package/index.ts +77 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -27,6 +27,7 @@ The agent can browse the current session by default or a named session with `con
27
27
  | -------- | ---------------------------------------- |
28
28
  | `list` | List all available sessions |
29
29
  | `search` | Rank messages by query relevance |
30
+ | `grep` | Filter messages by glob pattern |
30
31
  | `expand` | Show full content for specific entry IDs |
31
32
  | `view` | View the full session transcript |
32
33
 
@@ -36,6 +37,7 @@ Examples:
36
37
  goosedump({ action: "list" })
37
38
  goosedump({ action: "search", query: "bug fix" })
38
39
  goosedump({ action: "search", contextId: "abc123", query: "bug fix" })
40
+ goosedump({ action: "grep", pattern: "*rand*" })
39
41
  goosedump({ action: "expand", ids: ["entry-a", "entry-b"] })
40
42
  goosedump({ action: "view" })
41
43
  ```
package/index.ts CHANGED
@@ -18,7 +18,13 @@ import { createRequire } from 'node:module';
18
18
  import { homedir } from 'node:os';
19
19
  import { basename, join } from 'node:path';
20
20
 
21
- import { Key, matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
21
+ import {
22
+ Key,
23
+ matchesKey,
24
+ truncateToWidth,
25
+ visibleWidth,
26
+ wrapTextWithAnsi,
27
+ } from '@earendil-works/pi-tui';
22
28
  import { Type } from '@sinclair/typebox';
23
29
 
24
30
  const require = createRequire(import.meta.url);
@@ -212,12 +218,13 @@ function hitMessage(h: HitJson): GoosedumpMessage {
212
218
  return { id: h.entryId, role: h.role, content: h.text, score: h.score };
213
219
  }
214
220
 
221
+ function compactLine(content: string): string {
222
+ const collapsed = content.replace(/\s+/g, ' ').trim();
223
+ return truncateToWidth(collapsed, 80).replaceAll(String.fromCharCode(27) + '[0m', '');
224
+ }
225
+
215
226
  function formatMessagesCompact(messages: GoosedumpMessage[]): string {
216
- return messages
217
- .map(
218
- (m) => `[${m.id}] ${m.role}: ${m.content.slice(0, 80)}${m.content.length > 80 ? '...' : ''}`,
219
- )
220
- .join('\n');
227
+ return messages.map((m) => `[${m.id}] ${m.role}: ${compactLine(m.content)}`).join('\n');
221
228
  }
222
229
 
223
230
  function formatMessagesFull(messages: GoosedumpMessage[]): string {
@@ -297,6 +304,18 @@ function goosedumpSearch(
297
304
  return { messages, page: json.page ?? options.page ?? 1, totalPages: json.totalPages ?? 1 };
298
305
  }
299
306
 
307
+ function goosedumpGrep(
308
+ contextId: string,
309
+ pattern: string,
310
+ options: { scope?: string } = {},
311
+ ): GoosedumpMessage[] {
312
+ const scope = options.scope ?? 'lineage';
313
+ const json = JSON.parse(runGoosedump(['grep', `pi:${contextId}`, pattern, '--scope', scope])) as {
314
+ hits?: HitJson[];
315
+ };
316
+ return (json.hits ?? []).map(hitMessage);
317
+ }
318
+
300
319
  function goosedumpShow(
301
320
  contextId: string,
302
321
  options: { scope?: string; ids?: string[] } = {},
@@ -605,15 +624,20 @@ async function runSessionBrowser(
605
624
  ): Promise<string | null | undefined> {
606
625
  let resumePath: string | null | undefined;
607
626
 
627
+ const OVERLAY_WIDTH = 72;
628
+
608
629
  await ctx.ui.custom<void>(
609
630
  (tui, theme, _kb, done) => {
610
631
  let selectedIndex = 0;
632
+ let listScroll = 0;
611
633
  let mode: 'list' | 'compact' = 'list';
612
634
  let compactLines: string[] = [];
613
635
  let compactScroll = 0;
614
636
  let compactTitle = '';
615
637
 
616
638
  const COMPACT_VIEWPORT = 20;
639
+ const LIST_VIEWPORT = 20;
640
+ const wrapWidth = OVERLAY_WIDTH - 4;
617
641
 
618
642
  return {
619
643
  render(width: number): string[] {
@@ -633,8 +657,6 @@ async function runSessionBrowser(
633
657
  lines.push(`${borderFg('╭─')}${title}${topFill}${borderFg('─╮')}`);
634
658
 
635
659
  const total = compactLines.length;
636
- const maxScroll = Math.max(0, total - COMPACT_VIEWPORT);
637
- if (compactScroll > maxScroll) compactScroll = maxScroll;
638
660
  const window = compactLines.slice(compactScroll, compactScroll + COMPACT_VIEWPORT);
639
661
  for (const row of window) {
640
662
  lines.push(makeRow(text(truncateToWidth(row, innerW)), innerW, border));
@@ -665,18 +687,21 @@ async function runSessionBrowser(
665
687
  lines.push(makeRow('', innerW, border));
666
688
 
667
689
  // 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;
690
+ const total = listings.length;
691
+ const window = listings.slice(listScroll, listScroll + LIST_VIEWPORT);
692
+ for (let i = 0; i < window.length; i++) {
693
+ const absolute = listScroll + i;
694
+ const listing = window[i];
695
+ const isSelected = absolute === selectedIndex;
672
696
  const cursor = isSelected ? accent('▶') : ' ';
673
697
  const idText = isSelected ? accent(listing.id) : text(listing.id);
674
698
  const line = ` ${cursor} ${idText}`;
675
699
  lines.push(makeRow(truncateToWidth(line, innerW), innerW, border));
676
700
  }
677
701
 
678
- if (listings.length > 20) {
679
- lines.push(makeRow(dim(` ... and ${listings.length - 20} more`), innerW, border));
702
+ if (total > LIST_VIEWPORT) {
703
+ const position = `${listScroll + 1}-${Math.min(listScroll + LIST_VIEWPORT, total)} of ${total}`;
704
+ lines.push(makeRow(dim(` (${position})`), innerW, border));
680
705
  }
681
706
 
682
707
  // Footer
@@ -697,6 +722,7 @@ async function runSessionBrowser(
697
722
 
698
723
  handleInput(data: string): void {
699
724
  if (mode === 'compact') {
725
+ const maxScroll = Math.max(0, compactLines.length - COMPACT_VIEWPORT);
700
726
  if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
701
727
  mode = 'list';
702
728
  tui.requestRender();
@@ -708,7 +734,7 @@ async function runSessionBrowser(
708
734
  return;
709
735
  }
710
736
  if (matchesKey(data, Key.down)) {
711
- compactScroll += 1;
737
+ compactScroll = Math.min(maxScroll, compactScroll + 1);
712
738
  tui.requestRender();
713
739
  return;
714
740
  }
@@ -718,7 +744,7 @@ async function runSessionBrowser(
718
744
  return;
719
745
  }
720
746
  if (matchesKey(data, Key.pageDown)) {
721
- compactScroll += COMPACT_VIEWPORT;
747
+ compactScroll = Math.min(maxScroll, compactScroll + COMPACT_VIEWPORT);
722
748
  tui.requestRender();
723
749
  return;
724
750
  }
@@ -732,12 +758,16 @@ async function runSessionBrowser(
732
758
 
733
759
  if (matchesKey(data, Key.up)) {
734
760
  selectedIndex = Math.max(0, selectedIndex - 1);
761
+ if (selectedIndex < listScroll) listScroll = selectedIndex;
735
762
  tui.requestRender();
736
763
  return;
737
764
  }
738
765
 
739
766
  if (matchesKey(data, Key.down)) {
740
767
  selectedIndex = Math.min(listings.length - 1, selectedIndex + 1);
768
+ if (selectedIndex >= listScroll + LIST_VIEWPORT) {
769
+ listScroll = selectedIndex - LIST_VIEWPORT + 1;
770
+ }
741
771
  tui.requestRender();
742
772
  return;
743
773
  }
@@ -756,11 +786,11 @@ async function runSessionBrowser(
756
786
  try {
757
787
  const summary = goosedumpCompact(listing.id, { scope: 'lineage' }).trim();
758
788
  compactLines = summary
759
- ? summary.split('\n')
789
+ ? wrapTextWithAnsi(summary, wrapWidth)
760
790
  : ['No compact summary available for this session.'];
761
791
  } catch (err) {
762
792
  const msg = err instanceof Error ? err.message : String(err);
763
- compactLines = [`Failed to compact session: ${msg}`];
793
+ compactLines = wrapTextWithAnsi(`Failed to compact session: ${msg}`, wrapWidth);
764
794
  }
765
795
  mode = 'compact';
766
796
  tui.requestRender();
@@ -781,7 +811,7 @@ async function runSessionBrowser(
781
811
  overlay: true,
782
812
  overlayOptions: {
783
813
  anchor: 'center',
784
- width: 72,
814
+ width: OVERLAY_WIDTH,
785
815
  margin: 2,
786
816
  },
787
817
  },
@@ -830,7 +860,7 @@ export function createGoosedumpIntegration(
830
860
  name: 'goosedump',
831
861
  label: 'goosedump',
832
862
  description:
833
- 'Browse coding agent session history. List all sessions, or view/search the current or a named session. Supports ranked search, compact overview, and result expansion. Default scope is active lineage.',
863
+ 'Browse coding agent session history. List all sessions, or view/search/grep the current or a named session. Supports ranked search, glob filtering, compact overview, and result expansion. Default scope is active lineage.',
834
864
  promptSnippet:
835
865
  'goosedump({ action: "search", query, contextId?, scope?, page? }) - search session history; omit contextId for current session',
836
866
  promptGuidelines: [
@@ -844,12 +874,13 @@ export function createGoosedumpIntegration(
844
874
  [
845
875
  Type.Literal('list'),
846
876
  Type.Literal('search'),
877
+ Type.Literal('grep'),
847
878
  Type.Literal('expand'),
848
879
  Type.Literal('view'),
849
880
  ],
850
881
  {
851
882
  description:
852
- 'Operation: list sessions, search within a session, expand entries, or view full session',
883
+ 'Operation: list sessions, search within a session, grep by glob pattern, expand entries, or view full session',
853
884
  },
854
885
  ),
855
886
  contextId: Type.Optional(
@@ -861,6 +892,9 @@ export function createGoosedumpIntegration(
861
892
  query: Type.Optional(
862
893
  Type.String({ description: 'Search query to rank messages by relevance' }),
863
894
  ),
895
+ pattern: Type.Optional(
896
+ Type.String({ description: 'Glob pattern to filter messages (e.g. *rand*)' }),
897
+ ),
864
898
  scope: Type.Optional(
865
899
  Type.Union([Type.Literal('lineage'), Type.Literal('all')], {
866
900
  default: 'lineage',
@@ -933,6 +967,28 @@ export function createGoosedumpIntegration(
933
967
  return textResult(header + text);
934
968
  }
935
969
 
970
+ case 'grep': {
971
+ const contextId = params.contextId ?? currentSessionContextId(ctx);
972
+ if (!contextId) return missingContextId('grep');
973
+
974
+ if (!params.pattern) return textResult('pattern is required for grep');
975
+
976
+ const messages = goosedumpGrep(contextId, params.pattern, {
977
+ scope: params.scope ?? 'lineage',
978
+ });
979
+
980
+ if (messages.length === 0) {
981
+ return textResult(`No matches for "${params.pattern}"`);
982
+ }
983
+
984
+ const useCompact = params.compact ?? true;
985
+ const text = useCompact
986
+ ? formatMessagesCompact(messages)
987
+ : formatMessagesFull(messages);
988
+
989
+ return textResult(text);
990
+ }
991
+
936
992
  case 'expand': {
937
993
  const contextId = params.contextId ?? currentSessionContextId(ctx);
938
994
  if (!contextId) return missingContextId('expand');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "Coding agent context data browser plugin for pi",
5
5
  "keywords": [
6
6
  "goosedump",