@xynogen/pix-optimizer 1.1.3 → 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/package.json +4 -1
- package/src/caveman.test.ts +0 -37
- package/src/caveman.ts +10 -252
- package/src/index.ts +4 -4
- package/src/json.ts +8 -26
- package/src/opt.test.ts +35 -78
- package/src/opt.ts +180 -73
- package/src/ponytail.test.ts +0 -41
- package/src/ponytail.ts +11 -244
- package/src/rtk.test.ts +5 -0
- package/src/rtk.ts +7 -20
- package/src/status.ts +17 -16
package/src/opt.ts
CHANGED
|
@@ -1,108 +1,215 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* opt.ts — the single `/
|
|
2
|
+
* opt.ts — the single `/optimizer` command: an interactive overlay that fronts
|
|
3
|
+
* every optimizer tool.
|
|
3
4
|
*
|
|
4
|
-
* caveman / rtk /
|
|
5
|
-
* OptimizerHandle
|
|
6
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
16
|
-
|
|
17
|
-
OptimizerHandle,
|
|
18
|
-
|
|
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
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
/**
|
|
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 =
|
|
69
|
-
(n) => `
|
|
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("
|
|
81
|
+
pi.registerCommand("optimizer", {
|
|
84
82
|
description: "pix-optimizer: caveman / rtk / toon / ponytail tools",
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
91
|
-
if (
|
|
92
|
-
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/ponytail.test.ts
CHANGED
|
@@ -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: /
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
// --
|
|
178
|
+
// -- Overlay value handler (called by the /optimizer overlay) --
|
|
277
179
|
|
|
278
180
|
async function run(
|
|
279
|
-
|
|
181
|
+
value: string,
|
|
280
182
|
ctx: ExtensionCommandContext,
|
|
281
183
|
): 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
|
-
}
|
|
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
|
|
201
|
+
help: "ponytail — lazy senior dev (minimal code)",
|
|
202
|
+
values: LEVELS,
|
|
203
|
+
current: () => level,
|
|
436
204
|
run,
|
|
437
|
-
complete,
|
|
438
205
|
};
|
|
439
206
|
}
|
package/src/rtk.test.ts
CHANGED
|
@@ -68,6 +68,11 @@ describe("rewriteChain", () => {
|
|
|
68
68
|
expect(rewriteChain("echo hi && mkdir x")).toBe("echo hi && mkdir x");
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
it("does not prefix find (rtk find rejects -not/-exec)", () => {
|
|
72
|
+
const cmd = "find . -type f -not -path '*/node_modules/*'";
|
|
73
|
+
expect(rewriteChain(cmd)).toBe(cmd);
|
|
74
|
+
});
|
|
75
|
+
|
|
71
76
|
it("only prefixes known segments in a mixed chain", () => {
|
|
72
77
|
expect(rewriteChain("cd /tmp && git status")).toBe(
|
|
73
78
|
"cd /tmp && rtk git status",
|