pi-blackhole 0.3.2 → 0.3.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.
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Status Overlay — shows current compaction config, OM state, and pipeline status.
3
+ *
4
+ * Used by `/blackhole-memory` to display runtime state.
5
+ * Opens as a floating overlay via ctx.ui.custom({ overlay: true }).
6
+ * Press Enter/Space on "Open configure overlay" to open config overlay.
7
+ * Esc to close.
8
+ */
9
+
10
+ import { matchKey, visibleWidth } from "./key-matcher.js";
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Theme shape (duck-typed from what pi provides)
14
+ // ---------------------------------------------------------------------------
15
+
16
+ type ThemeShim = {
17
+ fg: (style: string, text: string) => string;
18
+ };
19
+
20
+ const EMPTY_THEME: ThemeShim = {
21
+ fg: (_s: string, t: string) => t,
22
+ };
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Status display
26
+ // ---------------------------------------------------------------------------
27
+
28
+ export interface StatusInfo {
29
+ compaction: "auto" | "manual" | "off";
30
+ compactionEngine: "blackhole" | "pi-default";
31
+ tailBehavior: "pi-default" | "minimal";
32
+ memory: boolean;
33
+ compactAfterTokens: number;
34
+ consolidationInFlight: boolean;
35
+ compactInFlight: boolean;
36
+ lastObserverError?: string;
37
+ lastReflectorError?: string;
38
+ lastDropperError?: string;
39
+ }
40
+
41
+ export interface StatusResult {
42
+ action: "configure" | "close";
43
+ }
44
+
45
+ /**
46
+ * Create a StatusOverlay instance to pass to ctx.ui.custom().
47
+ *
48
+ * Usage:
49
+ * ```ts
50
+ * const result = await ctx.ui.custom<StatusResult | undefined>(
51
+ * (tui, theme, _kb, done) => createStatusOverlay(info, theme, tui, done),
52
+ * { overlay: true },
53
+ * );
54
+ * if (result?.action === "configure") {
55
+ * // open configure overlay
56
+ * }
57
+ * ```
58
+ */
59
+ export function createStatusOverlay(
60
+ info: StatusInfo,
61
+ theme: unknown,
62
+ tui: { requestRender: () => void },
63
+ done: (result: StatusResult | undefined) => void,
64
+ ): { render(width: number): string[]; handleInput(data: string): void; invalidate(): void; dispose(): void } {
65
+ const th: ThemeShim = (theme && typeof (theme as Record<string, unknown>).fg === "function")
66
+ ? theme as ThemeShim
67
+ : EMPTY_THEME;
68
+
69
+ const fg = (style: string, text: string) => th.fg(style, text);
70
+ const dim = (s: string) => fg("dim", s);
71
+ const accent = (s: string) => fg("accent", s);
72
+ const success = (s: string) => fg("success", s);
73
+ const warning = (s: string) => fg("warning", s);
74
+ const error = (s: string) => fg("error", s);
75
+ const muted = (s: string) => fg("muted", s);
76
+
77
+ const configItems: { label: string; value: string }[] = [
78
+ { label: "Compaction", value: info.compaction },
79
+ { label: "Engine", value: info.compactionEngine },
80
+ { label: "Tail behavior", value: info.tailBehavior },
81
+ { label: "Memory", value: info.memory ? "Enabled" : "Disabled" },
82
+ { label: "Threshold", value: `${info.compactAfterTokens.toLocaleString()} tok` },
83
+ ];
84
+
85
+ const pipelineItems: { label: string; value: string }[] = [
86
+ { label: "Compaction in flight", value: info.compactInFlight ? "yes" : "no" },
87
+ { label: "Consolidation in flight", value: info.consolidationInFlight ? "yes" : "no" },
88
+ ];
89
+
90
+ const errorItems: { label: string; value: string }[] = [];
91
+ if (info.lastObserverError) errorItems.push({ label: "Observer", value: info.lastObserverError });
92
+ if (info.lastReflectorError) errorItems.push({ label: "Reflector", value: info.lastReflectorError });
93
+ if (info.lastDropperError) errorItems.push({ label: "Dropper", value: info.lastDropperError });
94
+
95
+ const actionItems = [
96
+ { label: "Open configure overlay", value: "configure" as const },
97
+ { label: "Close", value: "close" as const },
98
+ ];
99
+
100
+ // Contiguous selectable item model: no section headers in nav indices
101
+ const selectableConfigCount = configItems.length;
102
+ const selectablePipelineCount = pipelineItems.length;
103
+ const selectableErrorCount = errorItems.length;
104
+ const selectableActionCount = actionItems.length;
105
+ const totalSelectable = selectableConfigCount + selectablePipelineCount + selectableErrorCount + selectableActionCount;
106
+
107
+ // Render helpers: map a contiguous nav index to the display positions
108
+ // (section headers are inserted at render time, not in the nav model)
109
+ const navIsAction = (idx: number) => idx >= selectableConfigCount + selectablePipelineCount + selectableErrorCount;
110
+
111
+ let selectedIndex = 0;
112
+ let cachedLines: string[] | undefined;
113
+
114
+ function invalidate(): void {
115
+ cachedLines = undefined;
116
+ try { tui.requestRender(); } catch { /* no-op */ }
117
+ }
118
+
119
+ function handleInput(data: string): void {
120
+ if (matchKey(data, "escape")) {
121
+ done({ action: "close" });
122
+ return;
123
+ }
124
+
125
+ if (matchKey(data, "enter") || matchKey(data, "space")) {
126
+ if (navIsAction(selectedIndex)) {
127
+ const actionIdx = selectedIndex - selectableConfigCount - selectablePipelineCount - selectableErrorCount;
128
+ const action = actionItems[actionIdx]!;
129
+ done({ action: action.value });
130
+ return;
131
+ }
132
+ return;
133
+ }
134
+
135
+ if (matchKey(data, "up")) {
136
+ selectedIndex = Math.max(0, selectedIndex - 1);
137
+ invalidate();
138
+ return;
139
+ }
140
+ if (matchKey(data, "down")) {
141
+ selectedIndex = Math.min(totalSelectable - 1, selectedIndex + 1);
142
+ invalidate();
143
+ return;
144
+ }
145
+ }
146
+
147
+ function render(width: number): string[] {
148
+ if (cachedLines) return cachedLines;
149
+
150
+ const w = Math.max(2, Math.min(width - 2, 74));
151
+ const innerW = w - 4;
152
+
153
+ const pad = (s: string, len: number) => {
154
+ const vis = visibleWidth(s);
155
+ return s + " ".repeat(Math.max(0, len - vis));
156
+ };
157
+
158
+ const line = (content: string) => `│ ${pad(content, innerW)} │`;
159
+ const border = (s: string) => fg("border", s);
160
+
161
+ const lines: string[] = [];
162
+
163
+ // Top border + header
164
+ lines.push(fg("border", `╭${"─".repeat(w - 2)}╮`));
165
+ lines.push(fg("border", `│ ${accent("Blackhole Status")}${" ".repeat(Math.max(0, innerW + 1 - 16))}│`));
166
+ lines.push(fg("border", `├${"─".repeat(w - 2)}┤`));
167
+
168
+ // Config section
169
+ lines.push(border(line(` ${dim("─── Compaction Config ───")}`)));
170
+ for (let i = 0; i < configItems.length; i++) {
171
+ const item = configItems[i]!;
172
+ const isSelected = selectedIndex === i;
173
+ const prefix = isSelected ? accent(" ›") : " ";
174
+ const label = isSelected ? accent(`${item.label}:`) : `${item.label}:`;
175
+ let value: string;
176
+ switch (item.label) {
177
+ case "Compaction":
178
+ value = item.value === "auto" ? accent(item.value) : item.value === "off" ? muted(item.value) : dim(item.value);
179
+ break;
180
+ case "Engine":
181
+ value = item.value === "blackhole" ? success(item.value) : dim(item.value);
182
+ break;
183
+ case "Memory":
184
+ value = item.value === "Enabled" ? success("Enabled") : muted("Disabled");
185
+ break;
186
+ default:
187
+ value = dim(item.value);
188
+ }
189
+ lines.push(border(line(`${prefix} ${pad(label, 16)} ${value}`)));
190
+ }
191
+
192
+ // Pipeline section
193
+ if (pipelineItems.length > 0) {
194
+ lines.push(border(line(` ${dim("─── Pipeline ───")}`)));
195
+ for (let i = 0; i < pipelineItems.length; i++) {
196
+ const item = pipelineItems[i]!;
197
+ const isSelected = selectedIndex === selectableConfigCount + i;
198
+ const prefix = isSelected ? accent(" ›") : " ";
199
+ const label = isSelected ? accent(`${item.label}:`) : `${item.label}:`;
200
+ const value = item.value === "yes" ? warning("yes") : dim("no");
201
+ lines.push(border(line(`${prefix} ${pad(label, 16)} ${value}`)));
202
+ }
203
+ }
204
+
205
+ // Errors section
206
+ if (errorItems.length > 0) {
207
+ lines.push(border(line(` ${dim("─── Last Errors ───")}`)));
208
+ for (let i = 0; i < errorItems.length; i++) {
209
+ const item = errorItems[i]!;
210
+ const isSelected = selectedIndex === selectableConfigCount + selectablePipelineCount + i;
211
+ const prefix = isSelected ? accent(" ›") : " ";
212
+ const label = isSelected ? accent(`${item.label}:`) : `${item.label}:`;
213
+ const truncated = item.value.length > 40
214
+ ? item.value.slice(0, 37) + "..."
215
+ : item.value;
216
+ lines.push(border(line(`${prefix} ${pad(label, 16)} ${error(truncated)}`)));
217
+ }
218
+ }
219
+
220
+ // Separator
221
+ lines.push(border(line(` ${dim("─── Actions ───")}`)));
222
+
223
+ // Action items
224
+ for (let i = 0; i < actionItems.length; i++) {
225
+ const item = actionItems[i]!;
226
+ const isSelected = selectedIndex === selectableConfigCount + selectablePipelineCount + selectableErrorCount + i;
227
+ const prefix = isSelected ? accent(" ▶") : " ";
228
+ const text = isSelected ? accent(item.label) : dim(item.label);
229
+ lines.push(border(line(`${prefix} ${text}`)));
230
+ }
231
+
232
+ // Bottom hints
233
+ lines.push(border(line("")));
234
+ lines.push(border(dim(" ↑↓ navigate Enter/Space select Esc close ")));
235
+ lines.push(fg("border", `╰${"─".repeat(w - 2)}╯`));
236
+
237
+ cachedLines = lines;
238
+ return lines;
239
+ }
240
+
241
+ return { render, handleInput, invalidate, dispose: () => {} };
242
+ }