@xynogen/pix-optimizer 1.1.4 → 1.1.7
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/package.json +4 -1
- package/src/caveman.test.ts +0 -37
- package/src/caveman.ts +17 -252
- package/src/index.ts +4 -4
- package/src/json.ts +13 -26
- package/src/opt.test.ts +35 -78
- package/src/opt.ts +180 -73
- package/src/persist.test.ts +56 -0
- package/src/persist.ts +50 -0
- package/src/ponytail.test.ts +0 -41
- package/src/ponytail.ts +17 -244
- package/src/rtk.ts +11 -18
- package/src/status.ts +17 -16
package/src/ponytail.ts
CHANGED
|
@@ -9,21 +9,12 @@
|
|
|
9
9
|
* skill. We inject it as a system-prompt fragment — no external hooks/files.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
13
|
-
import { homedir } from "node:os";
|
|
14
|
-
import { join } from "node:path";
|
|
15
12
|
import type {
|
|
16
13
|
ExtensionAPI,
|
|
17
14
|
ExtensionCommandContext,
|
|
18
15
|
ExtensionContext,
|
|
19
16
|
} from "@earendil-works/pi-coding-agent";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
Container,
|
|
23
|
-
type SettingItem,
|
|
24
|
-
SettingsList,
|
|
25
|
-
Text,
|
|
26
|
-
} from "@earendil-works/pi-tui";
|
|
17
|
+
import { loadOptValue, saveOptValue } from "./persist.ts";
|
|
27
18
|
import type { OptimizerHandle, OptimizerStatus } from "./status.ts";
|
|
28
19
|
|
|
29
20
|
// ── Levels ────────────────────────────────────────────────────────────────────
|
|
@@ -41,19 +32,6 @@ export const LEVEL_NUMBERS: Record<string, Level> = {
|
|
|
41
32
|
"3": "ultra",
|
|
42
33
|
};
|
|
43
34
|
|
|
44
|
-
const PONYTAIL_COMMAND_OPTIONS = [
|
|
45
|
-
{ value: "1", label: "1 (lite)", description: "Name the lazier alternative" },
|
|
46
|
-
{ value: "2", label: "2 (full)", description: "The ladder enforced" },
|
|
47
|
-
{ value: "3", label: "3 (ultra)", description: "YAGNI extremist" },
|
|
48
|
-
{ value: "lite", label: "lite", description: "Name the lazier alternative" },
|
|
49
|
-
{ value: "full", label: "full", description: "The ladder enforced" },
|
|
50
|
-
{ value: "ultra", label: "ultra", description: "YAGNI extremist" },
|
|
51
|
-
{ value: "off", label: "off", description: "Disable ponytail mode" },
|
|
52
|
-
{ value: "stop", label: "stop", description: "Disable ponytail mode" },
|
|
53
|
-
{ value: "quit", label: "quit", description: "Disable ponytail mode" },
|
|
54
|
-
{ value: "config", label: "config", description: "Open settings dialog" },
|
|
55
|
-
] as const;
|
|
56
|
-
|
|
57
35
|
// ── Status labels ─────────────────────────────────────────────────────────────
|
|
58
36
|
|
|
59
37
|
export const STATUS_LABELS: Record<Exclude<Level, "off">, string> = {
|
|
@@ -140,7 +118,7 @@ export function buildHelp(current: Level): string {
|
|
|
140
118
|
return [
|
|
141
119
|
`Ponytail mode: ${statusLine}`,
|
|
142
120
|
"",
|
|
143
|
-
"Usage: /
|
|
121
|
+
"Usage: /optimizer ponytail <level>",
|
|
144
122
|
" 1 lite - name the lazier alternative, you pick",
|
|
145
123
|
" 2 full - the ladder enforced (default)",
|
|
146
124
|
" 3 ultra - YAGNI extremist",
|
|
@@ -157,55 +135,6 @@ export function toggleLevel(current: Level): Level {
|
|
|
157
135
|
return current === "off" ? "full" : "off";
|
|
158
136
|
}
|
|
159
137
|
|
|
160
|
-
// ── Config ────────────────────────────────────────────────────────────────────
|
|
161
|
-
|
|
162
|
-
export interface PonytailConfig {
|
|
163
|
-
/** Level to apply on new sessions. "off" means don't auto-enable. */
|
|
164
|
-
defaultLevel: Level;
|
|
165
|
-
/** Whether to show the status bar indicator. */
|
|
166
|
-
showStatus: boolean;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export const DEFAULT_CONFIG: PonytailConfig = {
|
|
170
|
-
defaultLevel: "full",
|
|
171
|
-
showStatus: true,
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const CONFIG_PATH = join(homedir(), ".pi", "agent", "ponytail.json");
|
|
175
|
-
|
|
176
|
-
export function parseConfig(raw: unknown): PonytailConfig {
|
|
177
|
-
const parsed = raw as Record<string, unknown>;
|
|
178
|
-
return {
|
|
179
|
-
defaultLevel: LEVELS.includes(parsed?.defaultLevel as Level)
|
|
180
|
-
? (parsed.defaultLevel as Level)
|
|
181
|
-
: DEFAULT_CONFIG.defaultLevel,
|
|
182
|
-
showStatus:
|
|
183
|
-
typeof parsed?.showStatus === "boolean"
|
|
184
|
-
? parsed.showStatus
|
|
185
|
-
: DEFAULT_CONFIG.showStatus,
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
let _saveQueue: Promise<void> = Promise.resolve();
|
|
190
|
-
|
|
191
|
-
async function loadConfig(): Promise<PonytailConfig> {
|
|
192
|
-
try {
|
|
193
|
-
const raw = await readFile(CONFIG_PATH, "utf8");
|
|
194
|
-
return parseConfig(JSON.parse(raw));
|
|
195
|
-
} catch {
|
|
196
|
-
return { ...DEFAULT_CONFIG };
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async function saveConfig(config: PonytailConfig): Promise<void> {
|
|
201
|
-
const snapshot = `${JSON.stringify(config, null, 2)}\n`;
|
|
202
|
-
_saveQueue = _saveQueue.then(async () => {
|
|
203
|
-
await mkdir(join(homedir(), ".pi", "agent"), { recursive: true });
|
|
204
|
-
await writeFile(CONFIG_PATH, snapshot, "utf8");
|
|
205
|
-
});
|
|
206
|
-
return _saveQueue;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
138
|
// ── Pi extension ────────────────────────────────────────────────────────────
|
|
210
139
|
|
|
211
140
|
export function ponytail(
|
|
@@ -213,26 +142,11 @@ export function ponytail(
|
|
|
213
142
|
status: OptimizerStatus,
|
|
214
143
|
): OptimizerHandle {
|
|
215
144
|
let level: Level = "off";
|
|
216
|
-
let config: PonytailConfig = { ...DEFAULT_CONFIG };
|
|
217
|
-
let configLoadPromise: Promise<void> | null = null;
|
|
218
|
-
|
|
219
|
-
const ensureConfigLoaded = async () => {
|
|
220
|
-
if (!configLoadPromise) {
|
|
221
|
-
configLoadPromise = (async () => {
|
|
222
|
-
config = await loadConfig();
|
|
223
|
-
if (level === "off" && config.defaultLevel !== "off") {
|
|
224
|
-
level = config.defaultLevel;
|
|
225
|
-
}
|
|
226
|
-
})();
|
|
227
|
-
}
|
|
228
|
-
await configLoadPromise;
|
|
229
|
-
};
|
|
230
145
|
|
|
231
146
|
// -- Status: report into the shared optimizer indicator. --
|
|
232
147
|
|
|
233
148
|
function syncStatus(ctx: Pick<ExtensionContext, "ui">) {
|
|
234
|
-
|
|
235
|
-
status.set("ponytail", on, ctx);
|
|
149
|
+
status.set("ponytail", level !== "off", ctx);
|
|
236
150
|
}
|
|
237
151
|
|
|
238
152
|
// Inject ponytail prompt via before_agent_start
|
|
@@ -243,25 +157,18 @@ export function ponytail(
|
|
|
243
157
|
return { systemPrompt: `${prompt}\n\n${existing}` };
|
|
244
158
|
});
|
|
245
159
|
|
|
246
|
-
// -- Restore
|
|
160
|
+
// -- Restore live level from the session log on load --
|
|
247
161
|
|
|
248
162
|
pi.on("session_start", async (_event, ctx) => {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
let sessionLevel: Level | null = null;
|
|
163
|
+
// Session log first (in-session branch nav), then disk (survives a full
|
|
164
|
+
// quit/restart). Disk wins when present.
|
|
252
165
|
for (const entry of ctx.sessionManager.getEntries()) {
|
|
253
166
|
if (entry.type === "custom" && entry.customType === "ponytail-level") {
|
|
254
|
-
|
|
167
|
+
level = (entry.data as { level: Level })?.level ?? level;
|
|
255
168
|
}
|
|
256
169
|
}
|
|
257
|
-
|
|
258
|
-
if (
|
|
259
|
-
level = sessionLevel;
|
|
260
|
-
} else if (config.defaultLevel !== "off") {
|
|
261
|
-
level = config.defaultLevel;
|
|
262
|
-
pi.appendEntry("ponytail-level", { level });
|
|
263
|
-
}
|
|
264
|
-
|
|
170
|
+
const saved = loadOptValue("ponytail");
|
|
171
|
+
if (saved && LEVELS.includes(saved as Level)) level = saved as Level;
|
|
265
172
|
syncStatus(ctx);
|
|
266
173
|
});
|
|
267
174
|
|
|
@@ -273,36 +180,18 @@ export function ponytail(
|
|
|
273
180
|
});
|
|
274
181
|
pi.on("session_shutdown", async () => {});
|
|
275
182
|
|
|
276
|
-
// --
|
|
183
|
+
// -- Overlay value handler (called by the /optimizer overlay) --
|
|
277
184
|
|
|
278
185
|
async function run(
|
|
279
|
-
|
|
186
|
+
value: string,
|
|
280
187
|
ctx: ExtensionCommandContext,
|
|
281
188
|
): Promise<void> {
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
// No argument → show help
|
|
285
|
-
if (!arg) {
|
|
286
|
-
ctx.ui.notify(buildHelp(level), "info");
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (arg === "config") {
|
|
291
|
-
await openConfig(ctx);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const resolved = resolveLevel(arg);
|
|
296
|
-
if (resolved === null) {
|
|
297
|
-
ctx.ui.notify(
|
|
298
|
-
`Unknown: "${arg}". Use 1/2/3, ${LEVELS.join(", ")}, stop, quit, or config`,
|
|
299
|
-
"error",
|
|
300
|
-
);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
189
|
+
const resolved = resolveLevel(value);
|
|
190
|
+
if (resolved === null) return;
|
|
303
191
|
level = resolved;
|
|
304
192
|
|
|
305
193
|
pi.appendEntry("ponytail-level", { level });
|
|
194
|
+
saveOptValue("ponytail", level);
|
|
306
195
|
syncStatus(ctx);
|
|
307
196
|
|
|
308
197
|
ctx.ui.notify(
|
|
@@ -313,127 +202,11 @@ export function ponytail(
|
|
|
313
202
|
);
|
|
314
203
|
}
|
|
315
204
|
|
|
316
|
-
function complete(prefix: string) {
|
|
317
|
-
const normalized = prefix.trim().toLowerCase();
|
|
318
|
-
const items = PONYTAIL_COMMAND_OPTIONS.filter((item) =>
|
|
319
|
-
item.value.startsWith(normalized),
|
|
320
|
-
);
|
|
321
|
-
return items.length > 0 ? items.map((i) => ({ ...i })) : null;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// -- config: interactive SettingsList --
|
|
325
|
-
|
|
326
|
-
async function openConfig(ctx: ExtensionContext) {
|
|
327
|
-
await ensureConfigLoaded();
|
|
328
|
-
|
|
329
|
-
await ctx.ui.custom((_tui, theme, _kb, done) => {
|
|
330
|
-
const items: SettingItem[] = [
|
|
331
|
-
{
|
|
332
|
-
id: "defaultLevel",
|
|
333
|
-
label: "Default level for new sessions",
|
|
334
|
-
currentValue: config.defaultLevel,
|
|
335
|
-
values: [...LEVELS],
|
|
336
|
-
},
|
|
337
|
-
{
|
|
338
|
-
id: "showStatus",
|
|
339
|
-
label: "Show status bar",
|
|
340
|
-
currentValue: config.showStatus ? "on" : "off",
|
|
341
|
-
values: ["on", "off"],
|
|
342
|
-
},
|
|
343
|
-
];
|
|
344
|
-
|
|
345
|
-
const container = new Container();
|
|
346
|
-
container.addChild(
|
|
347
|
-
new Text(theme.fg("accent", theme.bold(" Ponytail Config")), 0, 0),
|
|
348
|
-
);
|
|
349
|
-
container.addChild(
|
|
350
|
-
new Text(theme.fg("dim", " Saved to ~/.pi/agent/ponytail.json"), 0, 0),
|
|
351
|
-
);
|
|
352
|
-
container.addChild(
|
|
353
|
-
new Text(
|
|
354
|
-
theme.fg("dim", " Default level applies to future sessions."),
|
|
355
|
-
0,
|
|
356
|
-
0,
|
|
357
|
-
),
|
|
358
|
-
);
|
|
359
|
-
container.addChild(new Text("", 0, 0));
|
|
360
|
-
|
|
361
|
-
const applySettingChange = (id: string, newValue: string) => {
|
|
362
|
-
if (id === "defaultLevel" && LEVELS.includes(newValue as Level)) {
|
|
363
|
-
config.defaultLevel = newValue as Level;
|
|
364
|
-
} else if (id === "showStatus") {
|
|
365
|
-
config.showStatus = newValue === "on";
|
|
366
|
-
}
|
|
367
|
-
saveConfig(config);
|
|
368
|
-
syncStatus(ctx);
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
const settingsList = new SettingsList(
|
|
372
|
-
items,
|
|
373
|
-
Math.min(items.length + 2, 10),
|
|
374
|
-
getSettingsListTheme(),
|
|
375
|
-
applySettingChange,
|
|
376
|
-
() => done(undefined),
|
|
377
|
-
);
|
|
378
|
-
|
|
379
|
-
container.addChild(settingsList);
|
|
380
|
-
container.addChild(
|
|
381
|
-
new Text(
|
|
382
|
-
theme.fg("dim", " ←→/hl/tab change • ↑↓/jk move • esc close"),
|
|
383
|
-
0,
|
|
384
|
-
0,
|
|
385
|
-
),
|
|
386
|
-
);
|
|
387
|
-
|
|
388
|
-
const cycleSelectedValue = (direction: -1 | 1) => {
|
|
389
|
-
const selectedIndex = (
|
|
390
|
-
settingsList as unknown as { selectedIndex: number }
|
|
391
|
-
).selectedIndex;
|
|
392
|
-
const item = items[selectedIndex];
|
|
393
|
-
if (!item?.values?.length) return;
|
|
394
|
-
const currentIndex = item.values.indexOf(item.currentValue);
|
|
395
|
-
const nextIndex =
|
|
396
|
-
(currentIndex + direction + item.values.length) % item.values.length;
|
|
397
|
-
const newValue = item.values[nextIndex]!;
|
|
398
|
-
item.currentValue = newValue;
|
|
399
|
-
settingsList.updateValue(item.id, newValue);
|
|
400
|
-
applySettingChange(item.id, newValue);
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
return {
|
|
404
|
-
render: (w: number) => container.render(w),
|
|
405
|
-
invalidate: () => container.invalidate(),
|
|
406
|
-
handleInput: (data: string) => {
|
|
407
|
-
if (data === "j") data = "\u001b[B";
|
|
408
|
-
else if (data === "k") data = "\u001b[A";
|
|
409
|
-
else if (data === "h") {
|
|
410
|
-
cycleSelectedValue(-1);
|
|
411
|
-
_tui.requestRender();
|
|
412
|
-
return;
|
|
413
|
-
} else if (data === "l" || data === "\u001b[C" || data === "\t") {
|
|
414
|
-
cycleSelectedValue(1);
|
|
415
|
-
_tui.requestRender();
|
|
416
|
-
return;
|
|
417
|
-
} else if (data === "\u001b[D") {
|
|
418
|
-
cycleSelectedValue(-1);
|
|
419
|
-
_tui.requestRender();
|
|
420
|
-
return;
|
|
421
|
-
} else if (data === "\u001b" || data === "q") {
|
|
422
|
-
done(undefined);
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
settingsList.handleInput(data);
|
|
427
|
-
_tui.requestRender();
|
|
428
|
-
},
|
|
429
|
-
};
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
|
|
433
205
|
return {
|
|
434
206
|
name: "ponytail",
|
|
435
|
-
help: "ponytail
|
|
207
|
+
help: "ponytail — lazy senior dev (minimal code)",
|
|
208
|
+
values: LEVELS,
|
|
209
|
+
current: () => level,
|
|
436
210
|
run,
|
|
437
|
-
complete,
|
|
438
211
|
};
|
|
439
212
|
}
|
package/src/rtk.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
ExtensionCommandContext,
|
|
12
12
|
ExtensionContext,
|
|
13
13
|
} from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { loadOptValue, saveOptValue } from "./persist.ts";
|
|
14
15
|
import type { OptimizerHandle, OptimizerStatus } from "./status.ts";
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -283,6 +284,9 @@ export function rtk(
|
|
|
283
284
|
// Keep the status indicator in sync across the agent lifecycle. Probe
|
|
284
285
|
// availability on session start so the icon reflects reality immediately.
|
|
285
286
|
pi.on("session_start", async (_event, ctx) => {
|
|
287
|
+
// Restore the user's on/off choice from disk (survives quit/restart).
|
|
288
|
+
const saved = loadOptValue("rtk");
|
|
289
|
+
if (saved === "on" || saved === "off") enabled = saved === "on";
|
|
286
290
|
const probe = await checkRtkAvailability();
|
|
287
291
|
if (!probe.available && !warnedMissing) {
|
|
288
292
|
ctx.ui.notify(
|
|
@@ -300,32 +304,20 @@ export function rtk(
|
|
|
300
304
|
syncStatus(ctx);
|
|
301
305
|
});
|
|
302
306
|
|
|
303
|
-
// --
|
|
307
|
+
// -- Overlay value handler (called by the /optimizer overlay) --
|
|
304
308
|
|
|
305
309
|
async function run(
|
|
306
|
-
|
|
310
|
+
value: string,
|
|
307
311
|
ctx: ExtensionCommandContext,
|
|
308
312
|
): Promise<void> {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
else if (arg === "off") enabled = false;
|
|
312
|
-
else enabled = !enabled; // bare toggles
|
|
313
|
+
enabled = value === "on";
|
|
314
|
+
saveOptValue("rtk", enabled ? "on" : "off");
|
|
313
315
|
|
|
314
316
|
await checkRtkAvailability();
|
|
315
317
|
syncStatus(ctx);
|
|
316
318
|
ctx.ui.notify(`RTK rewriting ${enabled ? "on" : "off"}.`, "info");
|
|
317
319
|
}
|
|
318
320
|
|
|
319
|
-
function complete(prefix: string) {
|
|
320
|
-
const items = [
|
|
321
|
-
{ value: "on", label: "on", description: "Force RTK rewriting on" },
|
|
322
|
-
{ value: "off", label: "off", description: "Force RTK rewriting off" },
|
|
323
|
-
];
|
|
324
|
-
const n = prefix.trim().toLowerCase();
|
|
325
|
-
const filtered = items.filter((i) => i.value.startsWith(n));
|
|
326
|
-
return filtered.length > 0 ? filtered : null;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
321
|
// Rewrite bash commands to add rtk prefix.
|
|
330
322
|
//
|
|
331
323
|
// The SDK fires a single `tool_call` event for every tool. The bash variant
|
|
@@ -373,8 +365,9 @@ export function rtk(
|
|
|
373
365
|
|
|
374
366
|
return {
|
|
375
367
|
name: "rtk",
|
|
376
|
-
help: "rtk
|
|
368
|
+
help: "rtk — prefix shell commands with rtk (token-optimized)",
|
|
369
|
+
values: ["off", "on"],
|
|
370
|
+
current: () => (enabled ? "on" : "off"),
|
|
377
371
|
run,
|
|
378
|
-
complete,
|
|
379
372
|
};
|
|
380
373
|
}
|
package/src/status.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* status.ts — shared status-bar indicator for the optimizer suite.
|
|
3
3
|
*
|
|
4
|
-
* caveman / rtk /
|
|
4
|
+
* caveman / rtk / toon / ponytail each toggle independently, but they're all the same
|
|
5
5
|
* class of thing (token-optimization tools), so they share ONE status cell
|
|
6
6
|
* instead of three. Each tool reports its on/off state into a single registry;
|
|
7
7
|
* the cell renders only the icons whose tool is currently enabled, in a fixed
|
|
@@ -18,26 +18,22 @@ import type {
|
|
|
18
18
|
ThemeColor,
|
|
19
19
|
} from "@earendil-works/pi-coding-agent";
|
|
20
20
|
|
|
21
|
-
/** Argument-completion item shape used by the merged /opt command. */
|
|
22
|
-
export interface CompletionItem {
|
|
23
|
-
value: string;
|
|
24
|
-
label: string;
|
|
25
|
-
description: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
21
|
/**
|
|
29
|
-
* Each optimizer tool (caveman/rtk/
|
|
30
|
-
*
|
|
22
|
+
* Each optimizer tool (caveman/rtk/toon/ponytail) exposes a handle so the
|
|
23
|
+
* single `/optimizer` overlay can render one row per tool and apply value
|
|
24
|
+
* changes without knowing the tool's internals.
|
|
31
25
|
*/
|
|
32
26
|
export interface OptimizerHandle {
|
|
33
|
-
/**
|
|
27
|
+
/** Tool name, e.g. "caveman" / "rtk" / "toon". */
|
|
34
28
|
name: OptimizerTool;
|
|
35
|
-
/** One-line
|
|
29
|
+
/** One-line summary shown in the overlay row. */
|
|
36
30
|
help: string;
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
|
|
31
|
+
/** Cyclable values for this tool's overlay row, in display order. */
|
|
32
|
+
values: readonly string[];
|
|
33
|
+
/** Current value string (must be one of `values`). */
|
|
34
|
+
current(): string;
|
|
35
|
+
/** Apply a chosen value (persists + repaints status). `value` is one of `values`. */
|
|
36
|
+
run(value: string, ctx: ExtensionCommandContext): Promise<void> | void;
|
|
41
37
|
}
|
|
42
38
|
|
|
43
39
|
/** Stable status-bar key shared by every optimizer tool. */
|
|
@@ -94,6 +90,11 @@ export function renderStatus(
|
|
|
94
90
|
export class OptimizerStatus {
|
|
95
91
|
private states: Partial<Record<OptimizerTool, boolean>> = {};
|
|
96
92
|
|
|
93
|
+
/** Current enabled state for one tool (undefined until first set). */
|
|
94
|
+
get(tool: OptimizerTool): boolean | undefined {
|
|
95
|
+
return this.states[tool];
|
|
96
|
+
}
|
|
97
|
+
|
|
97
98
|
/** Update one tool's enabled state and repaint the shared cell. */
|
|
98
99
|
set(
|
|
99
100
|
tool: OptimizerTool,
|