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.
- package/README.md +63 -62
- package/package.json +1 -1
- package/src/commands/memory.ts +5 -5
- package/src/commands/pi-vcc.ts +44 -7
- package/src/core/settings.ts +17 -2
- package/src/core/unified-config.ts +178 -19
- package/src/hooks/before-compact.ts +148 -9
- package/src/om/compaction-trigger.ts +26 -12
- package/src/om/configure-overlay.ts +381 -0
- package/src/om/consolidation.ts +42 -1
- package/src/om/key-matcher.ts +72 -0
- package/src/om/model-budget.ts +22 -0
- package/src/om/status-overlay.ts +242 -0
|
@@ -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
|
+
}
|