pi-blackhole 0.4.2 → 0.4.3

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 (87) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +11660 -0
  3. package/dist/index.js.map +1 -0
  4. package/example-config.json +1 -1
  5. package/index.ts +37 -63
  6. package/package.json +21 -9
  7. package/src/commands/cleanup.ts +279 -240
  8. package/src/commands/memory.ts +236 -184
  9. package/src/commands/pi-vcc.ts +202 -152
  10. package/src/commands/vcc-recall.ts +126 -95
  11. package/src/core/brief.ts +167 -33
  12. package/src/core/build-sections.ts +8 -2
  13. package/src/core/config-env.ts +117 -0
  14. package/src/core/content.ts +31 -7
  15. package/src/core/drill-down.ts +41 -11
  16. package/src/core/filter-noise.ts +9 -3
  17. package/src/core/format-recall.ts +15 -6
  18. package/src/core/format.ts +14 -4
  19. package/src/core/lineage.ts +9 -3
  20. package/src/core/load-messages.ts +24 -5
  21. package/src/core/normalize.ts +38 -14
  22. package/src/core/recall-scope.ts +11 -3
  23. package/src/core/render-entries.ts +22 -6
  24. package/src/core/sanitize.ts +5 -1
  25. package/src/core/search-entries.ts +111 -19
  26. package/src/core/settings.ts +1 -3
  27. package/src/core/summarize.ts +42 -21
  28. package/src/core/unified-config.ts +549 -411
  29. package/src/extract/commits.ts +4 -2
  30. package/src/extract/files.ts +10 -5
  31. package/src/extract/goals.ts +7 -2
  32. package/src/hooks/before-compact.ts +210 -88
  33. package/src/om/agents/dropper/agent.ts +380 -265
  34. package/src/om/agents/dropper/coverage.ts +102 -82
  35. package/src/om/agents/observer/agent.ts +242 -206
  36. package/src/om/agents/reflector/agent.ts +212 -153
  37. package/src/om/cleanup.ts +239 -218
  38. package/src/om/clipboard.ts +59 -51
  39. package/src/om/compaction-trigger.ts +448 -333
  40. package/src/om/config.ts +13 -6
  41. package/src/om/configure-overlay.ts +518 -355
  42. package/src/om/consolidation.ts +1460 -953
  43. package/src/om/cooldown.ts +75 -65
  44. package/src/om/debug-log.ts +86 -68
  45. package/src/om/ids.ts +1 -1
  46. package/src/om/ledger/fold.ts +89 -78
  47. package/src/om/ledger/progress.ts +181 -153
  48. package/src/om/ledger/projection.ts +248 -185
  49. package/src/om/ledger/recall.ts +247 -196
  50. package/src/om/ledger/render-summary.ts +79 -50
  51. package/src/om/ledger/types.ts +146 -117
  52. package/src/om/model-budget.ts +23 -13
  53. package/src/om/pending.ts +243 -179
  54. package/src/om/provider-stream.ts +52 -7
  55. package/src/om/retryable-error.ts +12 -16
  56. package/src/om/reverse-recall.ts +97 -91
  57. package/src/om/runtime.ts +474 -375
  58. package/src/om/serialize.ts +190 -166
  59. package/src/om/status-overlay.ts +246 -195
  60. package/src/om/tokens.ts +28 -21
  61. package/src/pi-base/blackhole-settings.ts +437 -0
  62. package/src/pi-base/config-manager.ts +440 -0
  63. package/src/pi-base/config.ts +469 -0
  64. package/src/pi-base/env.ts +43 -0
  65. package/src/pi-base/paths.ts +47 -0
  66. package/src/pi-base/settings/body.ts +1648 -0
  67. package/src/pi-base/settings/fields/action.ts +43 -0
  68. package/src/pi-base/settings/fields/boolean.ts +47 -0
  69. package/src/pi-base/settings/fields/custom.ts +72 -0
  70. package/src/pi-base/settings/fields/enum.ts +310 -0
  71. package/src/pi-base/settings/fields/index.ts +46 -0
  72. package/src/pi-base/settings/fields/model.ts +452 -0
  73. package/src/pi-base/settings/fields/string.ts +527 -0
  74. package/src/pi-base/settings/fields/text.ts +115 -0
  75. package/src/pi-base/settings/frame.ts +197 -0
  76. package/src/pi-base/settings/index.ts +77 -0
  77. package/src/pi-base/settings/inline-edit.ts +313 -0
  78. package/src/pi-base/settings/modal.ts +152 -0
  79. package/src/pi-base/settings/types.ts +500 -0
  80. package/src/pi-base/settings/validate-field.ts +113 -0
  81. package/src/pi-base/shell.ts +117 -0
  82. package/src/pi-base/types.ts +6 -0
  83. package/src/pi-base/ui.ts +32 -0
  84. package/src/tools/recall.ts +347 -225
  85. package/src/types.ts +20 -3
  86. package/tsup.config.ts +23 -0
  87. package/vitest.config.ts +15 -15
@@ -18,11 +18,11 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
18
18
  import { matchesKey, decodeKittyPrintable } from "@earendil-works/pi-tui";
19
19
  import { visibleWidth } from "../om/key-matcher.js";
20
20
  import {
21
- analyzeOrphaned,
22
- deletePendingFiles,
23
- deleteOrphanedBatch,
24
- describeFile,
25
- type PendingFile,
21
+ analyzeOrphaned,
22
+ deletePendingFiles,
23
+ deleteOrphanedBatch,
24
+ describeFile,
25
+ type PendingFile,
26
26
  } from "../om/cleanup.js";
27
27
 
28
28
  // ── TUI Picker Component ────────────────────────────────────────────────────
@@ -31,29 +31,34 @@ const LIST_ROWS = 10;
31
31
 
32
32
  /** Clamp a list index to [0, length-1]. */
33
33
  function clampIndex(idx: number, length: number): number {
34
- if (length === 0) return 0;
35
- return Math.max(0, Math.min(idx, length - 1));
34
+ if (length === 0) return 0;
35
+ return Math.max(0, Math.min(idx, length - 1));
36
36
  }
37
37
 
38
38
  /** Build the framed top border with optional title and right-aligned info. */
39
39
  function buildTopBorder(
40
- width: number,
41
- border: (s: string) => string,
42
- dim: (s: string) => string,
43
- title?: string,
44
- right?: string,
40
+ width: number,
41
+ border: (s: string) => string,
42
+ dim: (s: string) => string,
43
+ title?: string,
44
+ right?: string,
45
45
  ): string {
46
- const innerW = Math.max(1, width - 2);
47
- if (!title && !right) {
48
- return border(`┏${"━".repeat(innerW)}┓`);
49
- }
50
- const rightText = right ? ` ${right} ` : "";
51
- const titleBudget = Math.max(1, innerW - visibleWidth(rightText) - 1);
52
- const titleFitted = title
53
- ? ` ${title.slice(0, Math.max(1, titleBudget - 2))}${title.length > titleBudget - 2 ? "…" : ""} `
54
- : "";
55
- const fill = Math.max(1, innerW - visibleWidth(titleFitted) - visibleWidth(rightText));
56
- return border(`┏${titleFitted}${"━".repeat(fill)}${right ? dim(rightText) : ""}┓`);
46
+ const innerW = Math.max(1, width - 2);
47
+ if (!title && !right) {
48
+ return border(`┏${"━".repeat(innerW)}┓`);
49
+ }
50
+ const rightText = right ? ` ${right} ` : "";
51
+ const titleBudget = Math.max(1, innerW - visibleWidth(rightText) - 1);
52
+ const titleFitted = title
53
+ ? ` ${title.slice(0, Math.max(1, titleBudget - 2))}${title.length > titleBudget - 2 ? "…" : ""} `
54
+ : "";
55
+ const fill = Math.max(
56
+ 1,
57
+ innerW - visibleWidth(titleFitted) - visibleWidth(rightText),
58
+ );
59
+ return border(
60
+ `┏${titleFitted}${"━".repeat(fill)}${right ? dim(rightText) : ""}┓`,
61
+ );
57
62
  }
58
63
 
59
64
  // ── Component ───────────────────────────────────────────────────────────────
@@ -65,183 +70,216 @@ function buildTopBorder(
65
70
  * deleted (immediate feedback). The caller creates a copy if needed.
66
71
  */
67
72
  function createCleanupPicker(
68
- orphaned: PendingFile[],
69
- theme: { fg: (s: string, t: string) => string; bg: (s: string, t: string) => string },
70
- done: (result: "deleteAll" | "cancel") => void,
71
- ): { render(width: number): string[]; handleInput(data: string): void; invalidate(): void } {
72
- let selectedIndex = 0;
73
- let scrollOffset = 0;
74
- let confirmDeleteAll = false;
73
+ orphaned: PendingFile[],
74
+ theme: {
75
+ fg: (s: string, t: string) => string;
76
+ bg: (s: string, t: string) => string;
77
+ },
78
+ done: (result: "deleteAll" | "cancel") => void,
79
+ ): {
80
+ render(width: number): string[];
81
+ handleInput(data: string): void;
82
+ invalidate(): void;
83
+ } {
84
+ let selectedIndex = 0;
85
+ let scrollOffset = 0;
86
+ let confirmDeleteAll = false;
75
87
 
76
- function clampScroll(): void {
77
- const count = orphaned.length;
78
- selectedIndex = clampIndex(selectedIndex, count);
79
- const maxOffset = Math.max(0, count - LIST_ROWS);
80
- scrollOffset = Math.max(0, Math.min(scrollOffset, maxOffset));
81
- if (selectedIndex < scrollOffset) scrollOffset = selectedIndex;
82
- if (selectedIndex >= scrollOffset + LIST_ROWS && count > 0) {
83
- scrollOffset = selectedIndex - LIST_ROWS + 1;
84
- }
85
- }
88
+ function clampScroll(): void {
89
+ const count = orphaned.length;
90
+ selectedIndex = clampIndex(selectedIndex, count);
91
+ const maxOffset = Math.max(0, count - LIST_ROWS);
92
+ scrollOffset = Math.max(0, Math.min(scrollOffset, maxOffset));
93
+ if (selectedIndex < scrollOffset) scrollOffset = selectedIndex;
94
+ if (selectedIndex >= scrollOffset + LIST_ROWS && count > 0) {
95
+ scrollOffset = selectedIndex - LIST_ROWS + 1;
96
+ }
97
+ }
86
98
 
87
- const bdr = (t: string) => theme.fg("border", t);
88
- const dim = (t: string) => theme.fg("dim", t);
89
- const accent = (t: string) => theme.fg("accent", t);
90
- const err = (t: string) => theme.fg("error", t);
91
- const now = Date.now();
99
+ const bdr = (t: string) => theme.fg("border", t);
100
+ const dim = (t: string) => theme.fg("dim", t);
101
+ const accent = (t: string) => theme.fg("accent", t);
102
+ const err = (t: string) => theme.fg("error", t);
103
+ const now = Date.now();
92
104
 
93
- function itemLine(pf: PendingFile, isSel: boolean, cw: number): string {
94
- const desc = describeFile(pf, now);
95
- const prefix = isSel ? accent(" ▶") : " ";
96
- const main = isSel ? accent(desc) : desc;
97
- const line = `${prefix} ${main}`;
98
- return ` ${line}${" ".repeat(Math.max(0, cw - visibleWidth(line)))} `;
99
- }
105
+ function itemLine(pf: PendingFile, isSel: boolean, cw: number): string {
106
+ const desc = describeFile(pf, now);
107
+ const prefix = isSel ? accent(" ▶") : " ";
108
+ const main = isSel ? accent(desc) : desc;
109
+ const line = `${prefix} ${main}`;
110
+ return ` ${line}${" ".repeat(Math.max(0, cw - visibleWidth(line)))} `;
111
+ }
100
112
 
101
- return {
102
- invalidate(): void {
103
- // No cache — render is always fresh
104
- },
113
+ return {
114
+ invalidate(): void {
115
+ // No cache — render is always fresh
116
+ },
105
117
 
106
- render(width: number): string[] {
107
- const w = Math.max(40, width);
108
- const innerW = Math.max(1, w - 2);
109
- const PX = 1;
110
- const cw = Math.max(1, innerW - PX * 2);
111
- const lines: string[] = [];
118
+ render(width: number): string[] {
119
+ const w = Math.max(40, width);
120
+ const innerW = Math.max(1, w - 2);
121
+ const PX = 1;
122
+ const cw = Math.max(1, innerW - PX * 2);
123
+ const lines: string[] = [];
112
124
 
113
- // ── Confirm-delete-all ──
114
- if (confirmDeleteAll) {
115
- const title = `Delete ${orphaned.length} orphaned file${orphaned.length === 1 ? "" : "s"}?`;
116
- lines.push(buildTopBorder(w, bdr, dim, "Cleanup Pending Files"));
117
- lines.push(bdr(`┃${" ".repeat(innerW)}┃`));
118
- lines.push(bdr(`┃ ${err(title)}${" ".repeat(Math.max(0, cw - visibleWidth(title)))} ┃`));
119
- lines.push(bdr(`┃${" ".repeat(innerW)}┃`));
120
- const hint = "Enter confirm · Esc cancel";
121
- lines.push(bdr(`┃ ${dim(hint)}${" ".repeat(Math.max(0, cw - visibleWidth(hint)))} ┃`));
122
- lines.push(bdr(`┗${"━".repeat(innerW)}┛`));
123
- return lines;
124
- }
125
+ // ── Confirm-delete-all ──
126
+ if (confirmDeleteAll) {
127
+ const title = `Delete ${orphaned.length} orphaned file${orphaned.length === 1 ? "" : "s"}?`;
128
+ lines.push(buildTopBorder(w, bdr, dim, "Cleanup Pending Files"));
129
+ lines.push(bdr(`┃${" ".repeat(innerW)}┃`));
130
+ lines.push(
131
+ bdr(
132
+ `┃ ${err(title)}${" ".repeat(Math.max(0, cw - visibleWidth(title)))} ┃`,
133
+ ),
134
+ );
135
+ lines.push(bdr(`┃${" ".repeat(innerW)}┃`));
136
+ const hint = "Enter confirm · Esc cancel";
137
+ lines.push(
138
+ bdr(
139
+ `┃ ${dim(hint)}${" ".repeat(Math.max(0, cw - visibleWidth(hint)))} ┃`,
140
+ ),
141
+ );
142
+ lines.push(bdr(`┗${"━".repeat(innerW)}┛`));
143
+ return lines;
144
+ }
125
145
 
126
- // ── List ──
127
- clampScroll();
146
+ // ── List ──
147
+ clampScroll();
128
148
 
129
- const sizeLabel = `${orphaned.length} file${orphaned.length === 1 ? "" : "s"}`;
130
- lines.push(buildTopBorder(w, bdr, dim, "Orphaned Pending Files", sizeLabel));
131
- lines.push(bdr(`┃${" ".repeat(innerW)}┃`));
149
+ const sizeLabel = `${orphaned.length} file${orphaned.length === 1 ? "" : "s"}`;
150
+ lines.push(
151
+ buildTopBorder(w, bdr, dim, "Orphaned Pending Files", sizeLabel),
152
+ );
153
+ lines.push(bdr(`┃${" ".repeat(innerW)}┃`));
132
154
 
133
- if (orphaned.length === 0) {
134
- lines.push(bdr(`┃ ${dim("No orphaned pending files found")}${" ".repeat(Math.max(0, cw - 29))} ┃`));
135
- } else {
136
- const visible = orphaned.slice(scrollOffset, scrollOffset + LIST_ROWS);
137
- for (let vi = 0; vi < visible.length; vi++) {
138
- const idx = scrollOffset + vi;
139
- const pf = visible[vi];
140
- if (!pf) continue;
141
- const isSel = idx === selectedIndex;
142
- const row = itemLine(pf, isSel, cw);
143
- lines.push(isSel
144
- ? theme.bg("selectedBg", bdr(`┃${row}┃`))
145
- : bdr(`┃${row}┃`));
146
- }
147
- }
155
+ if (orphaned.length === 0) {
156
+ lines.push(
157
+ bdr(
158
+ `┃ ${dim("No orphaned pending files found")}${" ".repeat(Math.max(0, cw - 29))} ┃`,
159
+ ),
160
+ );
161
+ } else {
162
+ const visible = orphaned.slice(scrollOffset, scrollOffset + LIST_ROWS);
163
+ for (let vi = 0; vi < visible.length; vi++) {
164
+ const idx = scrollOffset + vi;
165
+ const pf = visible[vi];
166
+ if (!pf) continue;
167
+ const isSel = idx === selectedIndex;
168
+ const row = itemLine(pf, isSel, cw);
169
+ lines.push(
170
+ isSel ? theme.bg("selectedBg", bdr(`┃${row}┃`)) : bdr(`┃${row}┃`),
171
+ );
172
+ }
173
+ }
148
174
 
149
- // Filler rows
150
- for (let i = Math.min(orphaned.length, LIST_ROWS); i < LIST_ROWS; i++) {
151
- lines.push(bdr(`┃${" ".repeat(innerW)}┃`));
152
- }
175
+ // Filler rows
176
+ for (let i = Math.min(orphaned.length, LIST_ROWS); i < LIST_ROWS; i++) {
177
+ lines.push(bdr(`┃${" ".repeat(innerW)}┃`));
178
+ }
153
179
 
154
- lines.push(bdr(`┃${" ".repeat(innerW)}┃`));
180
+ lines.push(bdr(`┃${" ".repeat(innerW)}┃`));
155
181
 
156
- if (orphaned.length > 0) {
157
- const totalBytes = orphaned.reduce((s, pf) => s + pf.sizeBytes, 0);
158
- const kb = totalBytes > 0 ? (totalBytes / 1024).toFixed(1) : "0.0";
159
- const totalStr = `Total: ${kb} KB`;
160
- const hint = "↑↓ navigate Enter delete D delete all Esc cancel";
161
- lines.push(bdr(`┃ ${dim(totalStr)}${" ".repeat(Math.max(0, cw - visibleWidth(totalStr)))} ┃`));
162
- lines.push(bdr(`┃ ${dim(hint)}${" ".repeat(Math.max(0, cw - visibleWidth(hint)))} ┃`));
163
- } else {
164
- const hint = "Esc close";
165
- lines.push(bdr(`┃ ${dim(hint)}${" ".repeat(Math.max(0, cw - visibleWidth(hint)))} ┃`));
166
- }
167
- lines.push(bdr(`┗${"━".repeat(innerW)}┛`));
168
- return lines;
169
- },
182
+ if (orphaned.length > 0) {
183
+ const totalBytes = orphaned.reduce((s, pf) => s + pf.sizeBytes, 0);
184
+ const kb = totalBytes > 0 ? (totalBytes / 1024).toFixed(1) : "0.0";
185
+ const totalStr = `Total: ${kb} KB`;
186
+ const hint = "↑↓ navigate Enter delete D delete all Esc cancel";
187
+ lines.push(
188
+ bdr(
189
+ `┃ ${dim(totalStr)}${" ".repeat(Math.max(0, cw - visibleWidth(totalStr)))} ┃`,
190
+ ),
191
+ );
192
+ lines.push(
193
+ bdr(
194
+ `┃ ${dim(hint)}${" ".repeat(Math.max(0, cw - visibleWidth(hint)))} ┃`,
195
+ ),
196
+ );
197
+ } else {
198
+ const hint = "Esc close";
199
+ lines.push(
200
+ bdr(
201
+ `┃ ${dim(hint)}${" ".repeat(Math.max(0, cw - visibleWidth(hint)))} ┃`,
202
+ ),
203
+ );
204
+ }
205
+ lines.push(bdr(`┗${"━".repeat(innerW)}┛`));
206
+ return lines;
207
+ },
170
208
 
171
- handleInput(data: string): void {
172
- const key = decodeKittyPrintable(data) ?? data;
209
+ handleInput(data: string): void {
210
+ const key = decodeKittyPrintable(data) ?? data;
173
211
 
174
- if (confirmDeleteAll) {
175
- if (matchesKey(data, "enter") || matchesKey(data, "return")) {
176
- done("deleteAll");
177
- return;
178
- }
179
- if (matchesKey(data, "escape")) {
180
- confirmDeleteAll = false;
181
- return;
182
- }
183
- return;
184
- }
212
+ if (confirmDeleteAll) {
213
+ if (matchesKey(data, "enter") || matchesKey(data, "return")) {
214
+ done("deleteAll");
215
+ return;
216
+ }
217
+ if (matchesKey(data, "escape")) {
218
+ confirmDeleteAll = false;
219
+ return;
220
+ }
221
+ return;
222
+ }
185
223
 
186
- if (orphaned.length === 0) {
187
- if (matchesKey(data, "escape")) done("cancel");
188
- return;
189
- }
224
+ if (orphaned.length === 0) {
225
+ if (matchesKey(data, "escape")) done("cancel");
226
+ return;
227
+ }
190
228
 
191
- if (matchesKey(data, "up")) {
192
- selectedIndex = clampIndex(selectedIndex - 1, orphaned.length);
193
- clampScroll();
194
- return;
195
- }
196
- if (matchesKey(data, "down")) {
197
- selectedIndex = clampIndex(selectedIndex + 1, orphaned.length);
198
- clampScroll();
199
- return;
200
- }
201
- if (matchesKey(data, "pageUp") || key === "-") {
202
- selectedIndex = clampIndex(selectedIndex - LIST_ROWS, orphaned.length);
203
- clampScroll();
204
- return;
205
- }
206
- if (matchesKey(data, "pageDown") || key === "=") {
207
- selectedIndex = clampIndex(selectedIndex + LIST_ROWS, orphaned.length);
208
- clampScroll();
209
- return;
210
- }
211
- if (matchesKey(data, "home")) {
212
- selectedIndex = 0;
213
- scrollOffset = 0;
214
- return;
215
- }
216
- if (matchesKey(data, "end")) {
217
- selectedIndex = Math.max(0, orphaned.length - 1);
218
- clampScroll();
219
- return;
220
- }
221
- if (matchesKey(data, "enter") || matchesKey(data, "return")) {
222
- const pf = orphaned[selectedIndex];
223
- if (pf) {
224
- deletePendingFiles(pf.sessionId);
225
- orphaned.splice(selectedIndex, 1);
226
- if (orphaned.length === 0) {
227
- done("cancel");
228
- return;
229
- }
230
- selectedIndex = clampIndex(selectedIndex, orphaned.length);
231
- clampScroll();
232
- }
233
- return;
234
- }
235
- if (key === "d" || key === "D") {
236
- confirmDeleteAll = true;
237
- return;
238
- }
239
- if (matchesKey(data, "escape")) {
240
- done("cancel");
241
- return;
242
- }
243
- },
244
- };
229
+ if (matchesKey(data, "up")) {
230
+ selectedIndex = clampIndex(selectedIndex - 1, orphaned.length);
231
+ clampScroll();
232
+ return;
233
+ }
234
+ if (matchesKey(data, "down")) {
235
+ selectedIndex = clampIndex(selectedIndex + 1, orphaned.length);
236
+ clampScroll();
237
+ return;
238
+ }
239
+ if (matchesKey(data, "pageUp") || key === "-") {
240
+ selectedIndex = clampIndex(selectedIndex - LIST_ROWS, orphaned.length);
241
+ clampScroll();
242
+ return;
243
+ }
244
+ if (matchesKey(data, "pageDown") || key === "=") {
245
+ selectedIndex = clampIndex(selectedIndex + LIST_ROWS, orphaned.length);
246
+ clampScroll();
247
+ return;
248
+ }
249
+ if (matchesKey(data, "home")) {
250
+ selectedIndex = 0;
251
+ scrollOffset = 0;
252
+ return;
253
+ }
254
+ if (matchesKey(data, "end")) {
255
+ selectedIndex = Math.max(0, orphaned.length - 1);
256
+ clampScroll();
257
+ return;
258
+ }
259
+ if (matchesKey(data, "enter") || matchesKey(data, "return")) {
260
+ const pf = orphaned[selectedIndex];
261
+ if (pf) {
262
+ deletePendingFiles(pf.sessionId);
263
+ orphaned.splice(selectedIndex, 1);
264
+ if (orphaned.length === 0) {
265
+ done("cancel");
266
+ return;
267
+ }
268
+ selectedIndex = clampIndex(selectedIndex, orphaned.length);
269
+ clampScroll();
270
+ }
271
+ return;
272
+ }
273
+ if (key === "d" || key === "D") {
274
+ confirmDeleteAll = true;
275
+ return;
276
+ }
277
+ if (matchesKey(data, "escape")) {
278
+ done("cancel");
279
+ return;
280
+ }
281
+ },
282
+ };
245
283
  }
246
284
 
247
285
  // ── Public handler ──────────────────────────────────────────────────────────
@@ -256,64 +294,65 @@ function createCleanupPicker(
256
294
  * without deleting anything.
257
295
  */
258
296
  export async function handleCleanup(ctx: ExtensionContext): Promise<void> {
259
- const { orphaned } = analyzeOrphaned();
297
+ const { orphaned } = analyzeOrphaned();
260
298
 
261
- if (orphaned.length === 0) {
262
- ctx.ui.notify("pi-blackhole: No orphaned pending files found.", "info");
263
- return;
264
- }
299
+ if (orphaned.length === 0) {
300
+ ctx.ui.notify("pi-blackhole: No orphaned pending files found.", "info");
301
+ return;
302
+ }
265
303
 
266
- const isRpc = ctx.mode === "rpc" || ctx.mode === "json" || ctx.mode === "print";
267
- if (isRpc) {
268
- // Non-TUI: list only
269
- const totalSize = orphaned.reduce((s, pf) => s + pf.sizeBytes, 0);
270
- const lines = [
271
- `Orphaned pending files: ${orphaned.length} (${(totalSize / 1024).toFixed(1)} KB)`,
272
- "",
273
- ...orphaned.map((pf) => ` ${describeFile(pf)}`),
274
- "",
275
- "Use /blackhole cleanup in TUI mode to delete these files.",
276
- ];
277
- ctx.ui.notify(lines.join("\n"), "warning");
278
- return;
279
- }
304
+ const isRpc =
305
+ ctx.mode === "rpc" || ctx.mode === "json" || ctx.mode === "print";
306
+ if (isRpc) {
307
+ // Non-TUI: list only
308
+ const totalSize = orphaned.reduce((s, pf) => s + pf.sizeBytes, 0);
309
+ const lines = [
310
+ `Orphaned pending files: ${orphaned.length} (${(totalSize / 1024).toFixed(1)} KB)`,
311
+ "",
312
+ ...orphaned.map((pf) => ` ${describeFile(pf)}`),
313
+ "",
314
+ "Use /blackhole cleanup in TUI mode to delete these files.",
315
+ ];
316
+ ctx.ui.notify(lines.join("\n"), "warning");
317
+ return;
318
+ }
280
319
 
281
- // Work on a copy — the picker mutates the array for inline deletes.
282
- const items = [...orphaned];
283
- const result = await ctx.ui.custom<"deleteAll" | "cancel" | null>(
284
- (_tui, theme, _kb, done) => {
285
- return createCleanupPicker(items, theme as any, done);
286
- },
287
- { overlay: true },
288
- );
320
+ // Work on a copy — the picker mutates the array for inline deletes.
321
+ const items = [...orphaned];
322
+ const result = await ctx.ui.custom<"deleteAll" | "cancel" | null>(
323
+ (_tui, theme, _kb, done) => {
324
+ return createCleanupPicker(items, theme as any, done);
325
+ },
326
+ { overlay: true },
327
+ );
289
328
 
290
- if (result === "deleteAll") {
291
- const deleted = deleteOrphanedBatch(items);
292
- const intended = items.length;
293
- if (deleted === intended) {
294
- ctx.ui.notify(
295
- `pi-blackhole: Deleted ${intended} orphaned pending file${intended === 1 ? "" : "s"}.`,
296
- "info",
297
- );
298
- } else {
299
- ctx.ui.notify(
300
- `pi-blackhole: Deleted ${deleted}/${intended} orphaned pending file${intended === 1 ? "" : "s"} (${intended - deleted} failed).`,
301
- "warning",
302
- );
303
- }
304
- } else if (items.length > 0 && items.length < orphaned.length) {
305
- // Some were deleted inline, some remain
306
- const remainingSize = items.reduce((s, pf) => s + pf.sizeBytes, 0);
307
- ctx.ui.notify(
308
- `pi-blackhole: ${orphaned.length - items.length} deleted, ${items.length} remain (${(remainingSize / 1024).toFixed(1)} KB).`,
309
- "info",
310
- );
311
- } else if (items.length === 0 && orphaned.length > 0) {
312
- // All were deleted inline
313
- ctx.ui.notify(
314
- `pi-blackhole: All ${orphaned.length} orphaned pending file${orphaned.length === 1 ? "" : "s"} removed.`,
315
- "info",
316
- );
317
- }
318
- // If nothing was deleted (user just pressed Esc), stay silent
329
+ if (result === "deleteAll") {
330
+ const deleted = deleteOrphanedBatch(items);
331
+ const intended = items.length;
332
+ if (deleted === intended) {
333
+ ctx.ui.notify(
334
+ `pi-blackhole: Deleted ${intended} orphaned pending file${intended === 1 ? "" : "s"}.`,
335
+ "info",
336
+ );
337
+ } else {
338
+ ctx.ui.notify(
339
+ `pi-blackhole: Deleted ${deleted}/${intended} orphaned pending file${intended === 1 ? "" : "s"} (${intended - deleted} failed).`,
340
+ "warning",
341
+ );
342
+ }
343
+ } else if (items.length > 0 && items.length < orphaned.length) {
344
+ // Some were deleted inline, some remain
345
+ const remainingSize = items.reduce((s, pf) => s + pf.sizeBytes, 0);
346
+ ctx.ui.notify(
347
+ `pi-blackhole: ${orphaned.length - items.length} deleted, ${items.length} remain (${(remainingSize / 1024).toFixed(1)} KB).`,
348
+ "info",
349
+ );
350
+ } else if (items.length === 0 && orphaned.length > 0) {
351
+ // All were deleted inline
352
+ ctx.ui.notify(
353
+ `pi-blackhole: All ${orphaned.length} orphaned pending file${orphaned.length === 1 ? "" : "s"} removed.`,
354
+ "info",
355
+ );
356
+ }
357
+ // If nothing was deleted (user just pressed Esc), stay silent
319
358
  }