pi-goosedump 0.8.0 → 0.8.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 (3) hide show
  1. package/README.md +2 -3
  2. package/index.ts +87 -51
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -86,13 +86,12 @@ current session to its goosedump id and runs `goosedump compact <id>` with Pi's
86
86
  compaction range (`--from`, `--until`, and `--scope`) so the generated summary
87
87
  matches the entries Pi is about to replace.
88
88
 
89
- Optional goosedump counters can also trigger compaction. `0` disables a counter;
90
- when either counter fires, both counters reset to their configured values.
89
+ An optional goosedump counter can also trigger compaction. `0` disables the counter;
90
+ when it fires, it resets to its configured value.
91
91
 
92
92
  ```json
93
93
  {
94
94
  "goosedump": {
95
- "stepsBeforeCompact": 0,
96
95
  "turnsBeforeCompact": 0
97
96
  }
98
97
  }
package/index.ts CHANGED
@@ -19,9 +19,13 @@ import { homedir } from 'node:os';
19
19
  import { basename, dirname, extname, isAbsolute, join, relative } from 'node:path';
20
20
 
21
21
  import {
22
+ type Component,
23
+ Input,
22
24
  Key,
23
25
  SettingsList,
24
26
  type SettingItem,
27
+ decodeKittyPrintable,
28
+ getKeybindings,
25
29
  matchesKey,
26
30
  truncateToWidth,
27
31
  visibleWidth,
@@ -31,7 +35,7 @@ import { Type } from '@sinclair/typebox';
31
35
 
32
36
  const require = createRequire(import.meta.url);
33
37
 
34
- const GOOSEDUMP_VERSION = [0, 8, 1] as const;
38
+ const GOOSEDUMP_VERSION = [0, 8, 2] as const;
35
39
 
36
40
  // One element of goosedump 0.8's `list` JSON output.
37
41
  interface ListEntryJson {
@@ -119,7 +123,6 @@ interface CompactJson {
119
123
  }
120
124
 
121
125
  interface GoosedumpSettings {
122
- stepsBeforeCompact: number;
123
126
  turnsBeforeCompact: number;
124
127
  }
125
128
 
@@ -127,12 +130,9 @@ type SettingsScope = 'global' | 'project';
127
130
  type GoosedumpSettingKey = keyof GoosedumpSettings;
128
131
 
129
132
  const DEFAULT_GOOSEDUMP_SETTINGS: GoosedumpSettings = {
130
- stepsBeforeCompact: 0,
131
133
  turnsBeforeCompact: 0,
132
134
  };
133
135
 
134
- const COMPACT_SETTING_VALUES = ['0', '5', '10', '20', '50', '100'];
135
-
136
136
  function piAgentDir(): string {
137
137
  return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
138
138
  }
@@ -165,11 +165,9 @@ function readGoosedumpSettingsFrom(path: string): Partial<GoosedumpSettings> {
165
165
  if (!isRecord(raw)) return {};
166
166
 
167
167
  const settings: Partial<GoosedumpSettings> = {};
168
- for (const key of ['stepsBeforeCompact', 'turnsBeforeCompact'] as const) {
169
- const value = raw[key];
170
- if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) {
171
- settings[key] = value;
172
- }
168
+ const value = raw.turnsBeforeCompact;
169
+ if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) {
170
+ settings.turnsBeforeCompact = value;
173
171
  }
174
172
  return settings;
175
173
  }
@@ -747,6 +745,64 @@ function buildGoosedumpCompaction(
747
745
  };
748
746
  }
749
747
 
748
+ function isControlChar(char: string): boolean {
749
+ const code = char.charCodeAt(0);
750
+ return code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f);
751
+ }
752
+
753
+ class NumberInputSubmenu implements Component {
754
+ private readonly input = new Input();
755
+ private readonly hint: (text: string) => string;
756
+ private readonly onSubmitValue: (value: string) => void;
757
+ private readonly onCancelEdit: () => void;
758
+ private readonly title: string;
759
+
760
+ constructor(
761
+ title: string,
762
+ initialValue: string,
763
+ onSubmit: (value: string) => void,
764
+ onCancel: () => void,
765
+ ) {
766
+ this.title = title;
767
+ this.onSubmitValue = onSubmit;
768
+ this.onCancelEdit = onCancel;
769
+ this.input.setValue(initialValue);
770
+ this.hint = getSettingsListTheme().hint;
771
+ }
772
+
773
+ handleInput(data: string): void {
774
+ const kb = getKeybindings();
775
+ if (kb.matches(data, 'tui.select.confirm') || data === '\n') {
776
+ const digits = this.input.getValue().replace(/\D/g, '');
777
+ const parsed = Number(digits);
778
+ this.onSubmitValue(digits !== '' && Number.isSafeInteger(parsed) ? String(parsed) : '0');
779
+ return;
780
+ }
781
+ if (kb.matches(data, 'tui.select.cancel')) {
782
+ this.onCancelEdit();
783
+ return;
784
+ }
785
+ const kittyChar = decodeKittyPrintable(data);
786
+ const printable = kittyChar ?? (data.length === 1 && !isControlChar(data) ? data : undefined);
787
+ if (printable !== undefined && !/^[0-9]$/.test(printable)) return;
788
+ this.input.handleInput(data);
789
+ }
790
+
791
+ invalidate(): void {
792
+ this.input.invalidate();
793
+ }
794
+
795
+ render(width: number): string[] {
796
+ return [
797
+ this.hint(` ${this.title}`),
798
+ '',
799
+ ...this.input.render(width),
800
+ '',
801
+ this.hint(' enter submit · esc cancel · digits only'),
802
+ ];
803
+ }
804
+ }
805
+
750
806
  async function runSessionBrowser(
751
807
  ctx: ExtensionContext,
752
808
  listings: GoosedumpListing[],
@@ -757,7 +813,6 @@ async function runSessionBrowser(
757
813
  const LIST_VIEWPORT = 18;
758
814
  const COMPACT_VIEWPORT = 18;
759
815
  const location = ctx.cwd;
760
- const settingValues = COMPACT_SETTING_VALUES;
761
816
 
762
817
  function locationListings(): GoosedumpListing[] {
763
818
  return listings.filter((listing) => listing.cwd === location);
@@ -777,8 +832,7 @@ async function runSessionBrowser(
777
832
 
778
833
  function settingLabel(scope: SettingsScope, key: GoosedumpSettingKey): string {
779
834
  const scopeLabel = scope === 'global' ? 'Global' : 'Project';
780
- const keyLabel = key === 'stepsBeforeCompact' ? 'stepsBeforeCompact' : 'turnsBeforeCompact';
781
- return `${scopeLabel} ${keyLabel}`;
835
+ return `${scopeLabel} ${key}`;
782
836
  }
783
837
 
784
838
  function buildSettingItems(): SettingItem[] {
@@ -786,18 +840,19 @@ async function runSessionBrowser(
786
840
  const project = readScopedGoosedumpSettings(location, 'project');
787
841
  const settingsByScope = { global, project } satisfies Record<SettingsScope, GoosedumpSettings>;
788
842
 
789
- return (['global', 'project'] as const).flatMap((scope) =>
790
- (['stepsBeforeCompact', 'turnsBeforeCompact'] as const).map((key) => ({
791
- id: `${scope}:${key}`,
792
- label: settingLabel(scope, key),
793
- description:
794
- key === 'stepsBeforeCompact'
795
- ? 'Tool steps before goosedump starts compaction; 0 disables this trigger.'
796
- : 'LLM turns before goosedump starts compaction; 0 disables this trigger.',
797
- currentValue: String(settingsByScope[scope][key]),
798
- values: settingValues,
799
- })),
800
- );
843
+ return (['global', 'project'] as const).map((scope) => ({
844
+ id: `${scope}:turnsBeforeCompact`,
845
+ label: settingLabel(scope, 'turnsBeforeCompact'),
846
+ description: 'LLM turns before goosedump starts compaction; 0 disables this trigger.',
847
+ currentValue: String(settingsByScope[scope].turnsBeforeCompact),
848
+ submenu: (currentValue, done) =>
849
+ new NumberInputSubmenu(
850
+ settingLabel(scope, 'turnsBeforeCompact'),
851
+ currentValue,
852
+ (value) => done(value),
853
+ () => done(),
854
+ ),
855
+ }));
801
856
  }
802
857
 
803
858
  await ctx.ui.custom<void>(
@@ -835,8 +890,7 @@ async function runSessionBrowser(
835
890
  tui.requestRender();
836
891
  },
837
892
  () => {
838
- tab = 'sessions';
839
- tui.requestRender();
893
+ done(undefined);
840
894
  },
841
895
  );
842
896
  }
@@ -892,15 +946,13 @@ async function runSessionBrowser(
892
946
  const effective = resolveGoosedumpSettings(location);
893
947
  lines.push(
894
948
  makeRow(
895
- dim(
896
- `Effective: steps=${formatCompactSetting(effective.stepsBeforeCompact)} turns=${formatCompactSetting(effective.turnsBeforeCompact)}`,
897
- ),
949
+ dim(`Effective: turns=${formatCompactSetting(effective.turnsBeforeCompact)}`),
898
950
  innerW,
899
951
  border,
900
952
  ),
901
953
  );
902
954
  lines.push(
903
- makeRow(dim('tab switch ↑↓ select enter/space cycle esc close'), innerW, border),
955
+ makeRow(dim('tab switch ↑↓ select enter/space edit esc close'), innerW, border),
904
956
  );
905
957
  lines.push(`${borderFg('╰')}${borderFg('─'.repeat(width - 2))}${borderFg('╯')}`);
906
958
  return lines;
@@ -1051,10 +1103,6 @@ async function runSessionBrowser(
1051
1103
  }
1052
1104
 
1053
1105
  if (tab === 'settings') {
1054
- if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl('c'))) {
1055
- done(undefined);
1056
- return;
1057
- }
1058
1106
  settingsList.handleInput(data);
1059
1107
  tui.requestRender();
1060
1108
  return;
@@ -1152,19 +1200,17 @@ export function createGoosedumpIntegration(
1152
1200
  let goosedumpAvailable = false;
1153
1201
  let compactSettings = DEFAULT_GOOSEDUMP_SETTINGS;
1154
1202
  let settingsSignature = '';
1155
- let stepsRemaining = 0;
1156
1203
  let turnsRemaining = 0;
1157
1204
  let counterTriggered = false;
1158
1205
  let compactionQueued = false;
1159
1206
 
1160
1207
  function compactSettingsSignature(settings: GoosedumpSettings): string {
1161
- return `${settings.stepsBeforeCompact}:${settings.turnsBeforeCompact}`;
1208
+ return String(settings.turnsBeforeCompact);
1162
1209
  }
1163
1210
 
1164
1211
  function resetCompactCounters(cwd: string): void {
1165
1212
  compactSettings = resolveGoosedumpSettings(cwd);
1166
1213
  settingsSignature = compactSettingsSignature(compactSettings);
1167
- stepsRemaining = compactSettings.stepsBeforeCompact;
1168
1214
  turnsRemaining = compactSettings.turnsBeforeCompact;
1169
1215
  counterTriggered = false;
1170
1216
  }
@@ -1175,15 +1221,6 @@ export function createGoosedumpIntegration(
1175
1221
  if (nextSignature !== settingsSignature) resetCompactCounters(cwd);
1176
1222
  }
1177
1223
 
1178
- function noteCompletedStep(ctx: ExtensionContext): void {
1179
- if (!goosedumpAvailable || !shouldOverrideDefaultCompaction) return;
1180
- reloadCompactSettings(ctx.cwd);
1181
- if (compactSettings.stepsBeforeCompact === 0) return;
1182
-
1183
- stepsRemaining -= 1;
1184
- if (stepsRemaining <= 0) counterTriggered = true;
1185
- }
1186
-
1187
1224
  function noteCompletedTurn(ctx: ExtensionContext): void {
1188
1225
  if (!goosedumpAvailable || !shouldOverrideDefaultCompaction) return;
1189
1226
  reloadCompactSettings(ctx.cwd);
@@ -1193,6 +1230,9 @@ export function createGoosedumpIntegration(
1193
1230
  }
1194
1231
 
1195
1232
  if (!counterTriggered || compactionQueued) return;
1233
+ // ctx.compact() uses manual compaction semantics and aborts the current run.
1234
+ // Wait until no queued work remains before triggering plugin-driven compaction.
1235
+ if (ctx.hasPendingMessages()) return;
1196
1236
 
1197
1237
  resetCompactCounters(ctx.cwd);
1198
1238
  compactionQueued = true;
@@ -1509,10 +1549,6 @@ export function createGoosedumpIntegration(
1509
1549
  }
1510
1550
  });
1511
1551
 
1512
- pi.on('tool_execution_end', async (_event, ctx) => {
1513
- noteCompletedStep(ctx);
1514
- });
1515
-
1516
1552
  pi.on('turn_end', async (_event, ctx) => {
1517
1553
  noteCompletedTurn(ctx);
1518
1554
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-goosedump",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
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.8.1",
32
+ "@jarkkojs/goosedump": "^0.8.2",
33
33
  "@sinclair/typebox": "^0.34.49"
34
34
  },
35
35
  "devDependencies": {