@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/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 { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
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: /opt ponytail <level>",
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
- const on = level !== "off" && config.showStatus;
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 state on session load --
160
+ // -- Restore live level from the session log on load --
247
161
 
248
162
  pi.on("session_start", async (_event, ctx) => {
249
- await ensureConfigLoaded();
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
- sessionLevel = (entry.data as { level: Level })?.level ?? null;
167
+ level = (entry.data as { level: Level })?.level ?? level;
255
168
  }
256
169
  }
257
-
258
- if (sessionLevel !== null) {
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
- // -- Subcommand handler (dispatched by the merged /opt router) --
183
+ // -- Overlay value handler (called by the /optimizer overlay) --
277
184
 
278
185
  async function run(
279
- args: string,
186
+ value: string,
280
187
  ctx: ExtensionCommandContext,
281
188
  ): Promise<void> {
282
- const arg = args.trim().toLowerCase();
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 <1|2|3|lite|full|ultra|off|config> — lazy senior dev (minimal code)",
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
- // -- Subcommand handler (dispatched by the merged /opt router) --
307
+ // -- Overlay value handler (called by the /optimizer overlay) --
304
308
 
305
309
  async function run(
306
- args: string,
310
+ value: string,
307
311
  ctx: ExtensionCommandContext,
308
312
  ): Promise<void> {
309
- const arg = args.trim().toLowerCase();
310
- if (arg === "on") enabled = true;
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 [on|off] — prefix shell commands with rtk (token-optimized)",
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 / json each toggle independently, but they're all the same
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/json) exposes a handle so the single `/opt`
30
- * router can dispatch subcommands to it without knowing its internals.
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
- /** Subcommand name, e.g. "caveman" / "rtk" / "toon". */
27
+ /** Tool name, e.g. "caveman" / "rtk" / "toon". */
34
28
  name: OptimizerTool;
35
- /** One-line help shown by `/opt` with no args. */
29
+ /** One-line summary shown in the overlay row. */
36
30
  help: string;
37
- /** Handle `/opt <name> <args>`. `args` is everything after the name. */
38
- run(args: string, ctx: ExtensionCommandContext): Promise<void> | void;
39
- /** Completions for the second token (`/opt <name> <prefix>`). */
40
- complete(prefix: string): CompletionItem[] | null;
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,