@xynogen/pix-optimizer 1.1.4 → 1.1.6

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/opt.ts CHANGED
@@ -1,108 +1,215 @@
1
1
  /**
2
- * opt.ts — the single `/opt` command that fronts every optimizer tool.
2
+ * opt.ts — the single `/optimizer` command: an interactive overlay that fronts
3
+ * every optimizer tool.
3
4
  *
4
- * caveman / rtk / json each register their own lifecycle hooks but expose an
5
- * OptimizerHandle for command dispatch. This router wires them under one
6
- * command so there's exactly one entry point:
5
+ * caveman / rtk / toon / ponytail each register their own lifecycle hooks but
6
+ * expose an OptimizerHandle (name · values · current() · run()). This overlay
7
+ * renders one SettingsList row per tool:
7
8
  *
8
- * /opt → status + help
9
- * /opt caveman <level> → caveman subcommand
10
- * /opt rtk [on|off] → rtk subcommand
11
- * /opt toon [on|off] → toon subcommand
9
+ * ↑↓ move between tools
10
+ * ←→ cycle the selected tool's value (off → level/on → …)
11
+ * esc close
12
+ *
13
+ * Selecting a value calls the handle's run(), which persists + repaints the
14
+ * shared status cell. There is no text-arg form — the overlay is the only UI.
12
15
  */
13
16
 
14
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
15
- import type {
16
- CompletionItem,
17
- OptimizerHandle,
18
- OptimizerTool,
18
+ import { frameLines } from "@xynogen/pix-pretty/modal-frame";
19
+ import {
20
+ type OptimizerHandle,
21
+ type OptimizerStatus,
22
+ type OptimizerTool,
23
+ TOOL_ICONS,
19
24
  } from "./status.ts";
20
25
 
21
- /** Split raw args into the subcommand name and the rest. */
22
- export function parseInvocation(args: string): { name: string; rest: string } {
23
- const trimmed = args.trim();
24
- if (!trimmed) return { name: "", rest: "" };
25
- const idx = trimmed.search(/\s/);
26
- if (idx === -1) return { name: trimmed.toLowerCase(), rest: "" };
27
- return {
28
- name: trimmed.slice(0, idx).toLowerCase(),
29
- rest: trimmed.slice(idx + 1),
30
- };
26
+ /** Min/max content width for the overlay box (excludes 4 cols of chrome). */
27
+ const MIN_CONTENT = 28;
28
+ const MAX_CONTENT = 60;
29
+
30
+ /** Fixed render order — matches the status-bar cell. */
31
+ const TOOL_ORDER: readonly OptimizerTool[] = [
32
+ "caveman",
33
+ "rtk",
34
+ "toon",
35
+ "ponytail",
36
+ ];
37
+
38
+ /** Strip the leading "name [args] — " prefix from a handle's help string. */
39
+ function helpSummary(help: string): string {
40
+ const dash = help.indexOf("—");
41
+ return dash === -1 ? help : help.slice(dash + 1).trim();
31
42
  }
32
43
 
44
+ /** Filled / empty bar segment glyphs. */
45
+ const BAR_FILLED = "▰";
46
+ const BAR_EMPTY = "▱";
47
+
33
48
  /**
34
- * Compute completions for `/opt ...`:
35
- * - first token → tool names (filtered by prefix)
36
- * - after a known tool name → delegate to that tool's complete()
49
+ * Render an intensity bar for a tool's current value. One segment per
50
+ * non-"off" step in `values`; segments up to and including the current value's
51
+ * position are filled, the rest empty. "off" → all empty.
52
+ *
53
+ * ponytail [off,lite,full,ultra]: off=▱▱▱ lite=▰▱▱ full=▰▰▱ ultra=▰▰▰
54
+ * rtk [off,on]: off=▱ on=▰
37
55
  *
38
56
  * Pure + exported for tests.
39
57
  */
40
- export function completeInvocation(
41
- args: string,
42
- handles: Record<OptimizerTool, OptimizerHandle>,
43
- ): CompletionItem[] | null {
44
- // No space yet → still completing the tool name.
45
- if (!/\s/.test(args.trimStart())) {
46
- const prefix = args.trim().toLowerCase();
47
- const names = (Object.keys(handles) as OptimizerTool[])
48
- .filter((n) => n.startsWith(prefix))
49
- .map((n) => ({
50
- value: n,
51
- label: n,
52
- description: handles[n].help,
53
- }));
54
- return names.length > 0 ? names : null;
55
- }
56
-
57
- // Past the tool name → delegate to the tool's own completer.
58
- const { name, rest } = parseInvocation(args);
59
- const handle = handles[name as OptimizerTool];
60
- if (!handle) return null;
61
- return handle.complete(rest);
58
+ export function levelBar(current: string, values: readonly string[]): string {
59
+ const segments = values.length - 1; // exclude "off"
60
+ if (segments < 1) return "";
61
+ const idx = values.indexOf(current); // 0 = off → 0 filled
62
+ const filled = Math.max(0, idx);
63
+ return BAR_FILLED.repeat(filled) + BAR_EMPTY.repeat(segments - filled);
62
64
  }
63
65
 
64
- /** Build the `/opt` help text listing every tool. */
66
+ /** Plain-text status fallback when there's no custom-UI host (headless/tests). */
65
67
  export function buildOptHelp(
66
68
  handles: Record<OptimizerTool, OptimizerHandle>,
67
69
  ): string {
68
- const lines = (Object.keys(handles) as OptimizerTool[]).map(
69
- (n) => ` /opt ${handles[n].help}`,
70
+ const lines = TOOL_ORDER.map(
71
+ (n) => ` ${n}: ${handles[n].current()} — ${helpSummary(handles[n].help)}`,
70
72
  );
71
- return [
72
- "pix-optimizer — token tools",
73
- "",
74
- "Usage: /opt <tool> [args]",
75
- ...lines,
76
- ].join("\n");
73
+ return ["pix-optimizer — token tools", "", ...lines].join("\n");
77
74
  }
78
75
 
79
76
  export function registerOptCommand(
80
77
  pi: ExtensionAPI,
81
78
  handles: Record<OptimizerTool, OptimizerHandle>,
79
+ _status: OptimizerStatus,
82
80
  ): void {
83
- pi.registerCommand("opt", {
81
+ pi.registerCommand("optimizer", {
84
82
  description: "pix-optimizer: caveman / rtk / toon / ponytail tools",
85
- getArgumentCompletions: (prefix: string) =>
86
- completeInvocation(prefix, handles),
87
- handler: async (args, ctx) => {
88
- const { name, rest } = parseInvocation(args ?? "");
83
+ handler: async (_args, ctx) => {
84
+ const ui = ctx.ui as unknown as {
85
+ theme: {
86
+ fg(c: string, t: string): string;
87
+ bg(c: string, t: string): string;
88
+ bold(t: string): string;
89
+ };
90
+ custom?: <T>(
91
+ f: unknown,
92
+ opts?: {
93
+ overlay?: boolean;
94
+ overlayOptions?: {
95
+ anchor?: string;
96
+ width?: number;
97
+ maxHeight?: string;
98
+ };
99
+ },
100
+ ) => Promise<T>;
101
+ notify(m: string, t?: "info" | "warning" | "error"): void;
102
+ };
89
103
 
90
- // No subcommand → show help.
91
- if (!name) {
92
- ctx.ui.notify(buildOptHelp(handles), "info");
104
+ // No custom-UI host (headless/tests) → plain status text.
105
+ if (typeof ui.custom !== "function") {
106
+ ui.notify(buildOptHelp(handles), "info");
93
107
  return;
94
108
  }
95
109
 
96
- const handle = handles[name as OptimizerTool];
97
- if (!handle) {
98
- ctx.ui.notify(
99
- `Unknown tool: "${name}". Try: ${Object.keys(handles).join(", ")}`,
100
- "error",
101
- );
102
- return;
103
- }
110
+ const nameWidth = Math.max(...TOOL_ORDER.map((n) => n.length));
111
+ // Widest value across all tools, so value columns line up.
112
+ const valueWidth = Math.max(
113
+ ...TOOL_ORDER.flatMap((t) => handles[t].values.map((v) => v.length)),
114
+ );
115
+ // Fixed content width: widest help summary (the box's longest line) vs
116
+ // title, clamped. Fixed so the box doesn't resize as values/selection
117
+ // change and so the overlay can be centered at exactly this width.
118
+ const widestHelp = Math.max(
119
+ ...TOOL_ORDER.map((t) => helpSummary(handles[t].help).length),
120
+ "󱎫 Optimizer".length,
121
+ );
122
+ const content = Math.min(MAX_CONTENT, Math.max(MIN_CONTENT, widestHelp));
123
+ const boxW = content + 4; // + chrome (2 border + 2 padding)
124
+
125
+ await ui.custom<null>(
126
+ (
127
+ tui: { requestRender(): void },
128
+ theme: typeof ui.theme,
129
+ _kb: unknown,
130
+ done: (v: null) => void,
131
+ ) => {
132
+ let selected = 0;
133
+
134
+ const cycle = (direction: -1 | 1) => {
135
+ const tool = TOOL_ORDER[selected]!;
136
+ const values = handles[tool].values;
137
+ const cur = values.indexOf(handles[tool].current());
138
+ const next = (cur + direction + values.length) % values.length;
139
+ handles[tool].run(values[next]!, ctx);
140
+ };
141
+
142
+ const move = (direction: -1 | 1) => {
143
+ selected =
144
+ (selected + direction + TOOL_ORDER.length) % TOOL_ORDER.length;
145
+ };
104
146
 
105
- await handle.run(rest, ctx);
147
+ return {
148
+ render: () => {
149
+ const rows = TOOL_ORDER.map((tool, i) => {
150
+ const on = handles[tool].current() !== "off";
151
+ const sel = i === selected;
152
+ const cursor = sel ? theme.fg("accent", "→") : " ";
153
+ const icon = theme.fg(on ? "accent" : "dim", TOOL_ICONS[tool]);
154
+ const name = theme.fg(
155
+ sel ? "accent" : on ? "text" : "muted",
156
+ tool.padEnd(nameWidth),
157
+ );
158
+ const cur = handles[tool].current();
159
+ const value = theme.fg(
160
+ on ? "success" : "dim",
161
+ cur.padEnd(valueWidth),
162
+ );
163
+ const bar = theme.fg(
164
+ on ? "accent" : "dim",
165
+ levelBar(cur, handles[tool].values),
166
+ );
167
+ return `${cursor} ${icon} ${name} ${value} ${bar}`;
168
+ });
169
+ const lines = [
170
+ theme.fg("accent", theme.bold("󱎫 Optimizer")),
171
+ "",
172
+ ...rows,
173
+ "",
174
+ theme.fg(
175
+ "dim",
176
+ helpSummary(handles[TOOL_ORDER[selected]!].help),
177
+ ),
178
+ "",
179
+ theme.fg("dim", "←→ change · ↑↓ move · esc close"),
180
+ ];
181
+ return frameLines({
182
+ width: boxW,
183
+ lines,
184
+ color: (s) => theme.fg("accent", s),
185
+ bg: (s) => theme.bg("customMessageBg", s),
186
+ });
187
+ },
188
+ invalidate: () => {},
189
+ handleInput: (data: string) => {
190
+ if (data === "k" || data === "\u001b[A") move(-1);
191
+ else if (data === "j" || data === "\u001b[B") move(1);
192
+ else if (data === "h" || data === "\u001b[D") cycle(-1);
193
+ else if (
194
+ data === "l" ||
195
+ data === "\u001b[C" ||
196
+ data === " " ||
197
+ data === "\r"
198
+ )
199
+ cycle(1);
200
+ else if (data === "\u001b" || data === "q") {
201
+ done(null);
202
+ return;
203
+ } else return;
204
+ tui.requestRender();
205
+ },
206
+ };
207
+ },
208
+ {
209
+ overlay: true,
210
+ overlayOptions: { anchor: "center", width: boxW, maxHeight: "80%" },
211
+ },
212
+ );
106
213
  },
107
214
  });
108
215
  }
@@ -2,11 +2,9 @@ import { describe, expect, it } from "bun:test";
2
2
  import {
3
3
  buildHelp,
4
4
  buildPrompt,
5
- DEFAULT_CONFIG,
6
5
  LEVEL_NUMBERS,
7
6
  LEVELS,
8
7
  type Level,
9
- parseConfig,
10
8
  resolveLevel,
11
9
  STATUS_LABELS,
12
10
  STOP_ALIASES,
@@ -195,42 +193,3 @@ describe("buildPrompt", () => {
195
193
  expect(lite).not.toContain("YAGNI extremist");
196
194
  });
197
195
  });
198
-
199
- // ── parseConfig ───────────────────────────────────────────────────────────────
200
-
201
- describe("parseConfig", () => {
202
- it("parses valid config", () => {
203
- const cfg = parseConfig({ defaultLevel: "ultra", showStatus: false });
204
- expect(cfg.defaultLevel).toBe("ultra");
205
- expect(cfg.showStatus).toBe(false);
206
- });
207
-
208
- it("falls back to defaults for invalid defaultLevel", () => {
209
- const cfg = parseConfig({ defaultLevel: "invalid", showStatus: true });
210
- expect(cfg.defaultLevel).toBe(DEFAULT_CONFIG.defaultLevel);
211
- });
212
-
213
- it("default level is full", () => {
214
- expect(DEFAULT_CONFIG.defaultLevel).toBe("full");
215
- });
216
-
217
- it("falls back to defaults for missing fields", () => {
218
- const cfg = parseConfig({});
219
- expect(cfg.defaultLevel).toBe(DEFAULT_CONFIG.defaultLevel);
220
- expect(cfg.showStatus).toBe(DEFAULT_CONFIG.showStatus);
221
- });
222
-
223
- it("falls back to defaults for null/undefined", () => {
224
- const cfg = parseConfig(null);
225
- expect(cfg.defaultLevel).toBe(DEFAULT_CONFIG.defaultLevel);
226
- expect(cfg.showStatus).toBe(DEFAULT_CONFIG.showStatus);
227
- });
228
-
229
- it("accepts all valid levels as defaultLevel", () => {
230
- for (const l of LEVELS) {
231
- expect(
232
- parseConfig({ defaultLevel: l, showStatus: true }).defaultLevel,
233
- ).toBe(l);
234
- }
235
- });
236
- });
package/src/ponytail.ts CHANGED
@@ -9,21 +9,11 @@
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";
27
17
  import type { OptimizerHandle, OptimizerStatus } from "./status.ts";
28
18
 
29
19
  // ── Levels ────────────────────────────────────────────────────────────────────
@@ -41,19 +31,6 @@ export const LEVEL_NUMBERS: Record<string, Level> = {
41
31
  "3": "ultra",
42
32
  };
43
33
 
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
34
  // ── Status labels ─────────────────────────────────────────────────────────────
58
35
 
59
36
  export const STATUS_LABELS: Record<Exclude<Level, "off">, string> = {
@@ -140,7 +117,7 @@ export function buildHelp(current: Level): string {
140
117
  return [
141
118
  `Ponytail mode: ${statusLine}`,
142
119
  "",
143
- "Usage: /opt ponytail <level>",
120
+ "Usage: /optimizer ponytail <level>",
144
121
  " 1 lite - name the lazier alternative, you pick",
145
122
  " 2 full - the ladder enforced (default)",
146
123
  " 3 ultra - YAGNI extremist",
@@ -157,55 +134,6 @@ export function toggleLevel(current: Level): Level {
157
134
  return current === "off" ? "full" : "off";
158
135
  }
159
136
 
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
137
  // ── Pi extension ────────────────────────────────────────────────────────────
210
138
 
211
139
  export function ponytail(
@@ -213,26 +141,11 @@ export function ponytail(
213
141
  status: OptimizerStatus,
214
142
  ): OptimizerHandle {
215
143
  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
144
 
231
145
  // -- Status: report into the shared optimizer indicator. --
232
146
 
233
147
  function syncStatus(ctx: Pick<ExtensionContext, "ui">) {
234
- const on = level !== "off" && config.showStatus;
235
- status.set("ponytail", on, ctx);
148
+ status.set("ponytail", level !== "off", ctx);
236
149
  }
237
150
 
238
151
  // Inject ponytail prompt via before_agent_start
@@ -243,25 +156,14 @@ export function ponytail(
243
156
  return { systemPrompt: `${prompt}\n\n${existing}` };
244
157
  });
245
158
 
246
- // -- Restore state on session load --
159
+ // -- Restore live level from the session log on load --
247
160
 
248
161
  pi.on("session_start", async (_event, ctx) => {
249
- await ensureConfigLoaded();
250
-
251
- let sessionLevel: Level | null = null;
252
162
  for (const entry of ctx.sessionManager.getEntries()) {
253
163
  if (entry.type === "custom" && entry.customType === "ponytail-level") {
254
- sessionLevel = (entry.data as { level: Level })?.level ?? null;
164
+ level = (entry.data as { level: Level })?.level ?? level;
255
165
  }
256
166
  }
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
-
265
167
  syncStatus(ctx);
266
168
  });
267
169
 
@@ -273,33 +175,14 @@ export function ponytail(
273
175
  });
274
176
  pi.on("session_shutdown", async () => {});
275
177
 
276
- // -- Subcommand handler (dispatched by the merged /opt router) --
178
+ // -- Overlay value handler (called by the /optimizer overlay) --
277
179
 
278
180
  async function run(
279
- args: string,
181
+ value: string,
280
182
  ctx: ExtensionCommandContext,
281
183
  ): 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
- }
184
+ const resolved = resolveLevel(value);
185
+ if (resolved === null) return;
303
186
  level = resolved;
304
187
 
305
188
  pi.appendEntry("ponytail-level", { level });
@@ -313,127 +196,11 @@ export function ponytail(
313
196
  );
314
197
  }
315
198
 
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
199
  return {
434
200
  name: "ponytail",
435
- help: "ponytail <1|2|3|lite|full|ultra|off|config> — lazy senior dev (minimal code)",
201
+ help: "ponytail — lazy senior dev (minimal code)",
202
+ values: LEVELS,
203
+ current: () => level,
436
204
  run,
437
- complete,
438
205
  };
439
206
  }