@xynogen/pix-optimizer 1.1.9 → 1.1.10
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 +13 -17
- package/package.json +1 -1
- package/src/index.ts +2 -4
- package/src/opt.ts +14 -55
- package/src/persist.test.ts +1 -23
- package/src/persist.ts +2 -27
- package/src/status.test.ts +18 -90
- package/src/status.ts +32 -86
package/README.md
CHANGED
|
@@ -4,10 +4,10 @@ Token-optimization suite for Pi Coding Agent. Four tools wired into one
|
|
|
4
4
|
extension via `src/index.ts`, fronted by a single `/optimizer` command and one
|
|
5
5
|
shared status-bar cell:
|
|
6
6
|
|
|
7
|
-
- **Caveman** (
|
|
8
|
-
- **RTK** (
|
|
9
|
-
- **TOON** (
|
|
10
|
-
- **Ponytail** (
|
|
7
|
+
- **Caveman** (`Cv`) — terse-output system prompt
|
|
8
|
+
- **RTK** (`Rk`) — prefixes shell commands with `rtk` + injects RTK prompt
|
|
9
|
+
- **TOON** (`Tn`) — jq + TOON guidance for dense JSON (skill lives in pix-skills)
|
|
10
|
+
- **Ponytail** (`Pt`) — lazy-senior-dev system prompt (minimal code, YAGNI)
|
|
11
11
|
|
|
12
12
|
## Command
|
|
13
13
|
|
|
@@ -30,13 +30,13 @@ state: **accent** when the tool is enabled, **dim** when disabled.
|
|
|
30
30
|
|
|
31
31
|
### Icon style (Nerd Font, Unicode, or ASCII)
|
|
32
32
|
|
|
33
|
-
The default glyphs (
|
|
33
|
+
The default glyphs (Nerd Font PUA codepoints) are **Nerd Font** symbols and require a patched
|
|
34
34
|
font (e.g. MesloLGS NF). Terminals without one render them as missing-glyph
|
|
35
35
|
“tofu” boxes. Two font-independent fallbacks are available:
|
|
36
36
|
|
|
37
37
|
| Mode | Glyphs | Needs Nerd Font? |
|
|
38
38
|
|---|---|---|
|
|
39
|
-
| `nerd` (default) |
|
|
39
|
+
| `nerd` (default) | Nerd Font PUA glyphs | yes |
|
|
40
40
|
| `unicode` | `♤ ♡ ♢ ♧` (outline card suits) | no |
|
|
41
41
|
| `ascii` | `Cv Rk Tn Pt` | no |
|
|
42
42
|
|
|
@@ -44,16 +44,12 @@ Switch the style live from the **`/optimizer` overlay** — a fifth `icons` row
|
|
|
44
44
|
at the bottom cycles `nerd → unicode → ascii` with `←→`; the choice persists to
|
|
45
45
|
`~/.pi/agent/optimizer.json`.
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
export OPTIMIZER_ICONS=unicode # nerd | unicode | ascii
|
|
51
|
-
export PRETTY_ICONS=none # stack-wide: also maps the cell to ASCII
|
|
52
|
-
```
|
|
47
|
+
Icons follow the **global** `pix-pretty` mode — set via `/pretty` or `PRETTY_ICONS`
|
|
48
|
+
env var. The optimizer no longer has its own toggle.
|
|
53
49
|
|
|
54
50
|
## Features
|
|
55
51
|
|
|
56
|
-
### Caveman Mode (
|
|
52
|
+
### Caveman Mode (`Cv`)
|
|
57
53
|
|
|
58
54
|
Cuts ~75% of output tokens while keeping full technical accuracy.
|
|
59
55
|
|
|
@@ -67,7 +63,7 @@ Cuts ~75% of output tokens while keeping full technical accuracy.
|
|
|
67
63
|
The `/optimizer` overlay opens a settings dialog when needed. Default level
|
|
68
64
|
for new sessions is restored from `~/.pi/agent/optimizer.json`.
|
|
69
65
|
|
|
70
|
-
### RTK Tool Rewriting (
|
|
66
|
+
### RTK Tool Rewriting (`Rk`)
|
|
71
67
|
|
|
72
68
|
Two layers, both active automatically:
|
|
73
69
|
|
|
@@ -88,7 +84,7 @@ Two layers, both active automatically:
|
|
|
88
84
|
cargo install rtk-ai
|
|
89
85
|
```
|
|
90
86
|
|
|
91
|
-
### TOON / JSON Compression (
|
|
87
|
+
### TOON / JSON Compression (`Tn`)
|
|
92
88
|
|
|
93
89
|
Guidance for handling information-dense JSON via `jq` (query/reshape) and
|
|
94
90
|
`toon` (compress). The system-prompt nudge is injected **only when the user
|
|
@@ -105,7 +101,7 @@ The `toon-json` skill (full workflow + when-NOT-to-use guidance) is bundled in
|
|
|
105
101
|
npm i -g @toon-format/cli
|
|
106
102
|
```
|
|
107
103
|
|
|
108
|
-
### Ponytail Mode (
|
|
104
|
+
### Ponytail Mode (`Pt`)
|
|
109
105
|
|
|
110
106
|
"Lazy senior dev" mode. Governs **what** the agent builds (minimal code,
|
|
111
107
|
YAGNI), orthogonal to Caveman which governs **how** it talks — they pair. Before
|
|
@@ -140,7 +136,7 @@ pi install npm:@xynogen/pix-optimizer
|
|
|
140
136
|
|-------------------|-----------------------------------------------------------|
|
|
141
137
|
| `src/index.ts` | Wires the four tools + shared status, registers `/optimizer` |
|
|
142
138
|
| `src/opt.ts` | The `/optimizer` overlay UI (keyboard nav + cycling) |
|
|
143
|
-
| `src/status.ts` | Shared status-bar cell
|
|
139
|
+
| `src/status.ts` | Shared status-bar cell (`toolIcon()` → shared `pix-pretty` catalog) |
|
|
144
140
|
| `src/caveman.ts` | Caveman logic, levels, prompt |
|
|
145
141
|
| `src/rtk.ts` | RTK prompt + bash command rewriting |
|
|
146
142
|
| `src/json.ts` | jq+TOON guidance, heuristics, system-prompt injection |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-optimizer",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
4
4
|
"description": "Performance optimization suite for Pi Coding Agent - caveman mode + RTK tool rewriting + jq/TOON JSON compression + ponytail lazy-dev mode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -17,11 +17,9 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
17
17
|
import { caveman } from "./caveman.ts";
|
|
18
18
|
import { json } from "./json.ts";
|
|
19
19
|
import { registerOptCommand } from "./opt.ts";
|
|
20
|
-
import { loadIconMode } from "./persist.ts";
|
|
21
20
|
import { ponytail } from "./ponytail.ts";
|
|
22
21
|
import { rtk } from "./rtk.ts";
|
|
23
22
|
import {
|
|
24
|
-
envIconMode,
|
|
25
23
|
type OptimizerHandle,
|
|
26
24
|
OptimizerStatus,
|
|
27
25
|
type OptimizerTool,
|
|
@@ -29,8 +27,8 @@ import {
|
|
|
29
27
|
import { filterModelWarnings } from "./tool-result-filter.ts";
|
|
30
28
|
|
|
31
29
|
export default function optimizer(pi: ExtensionAPI) {
|
|
32
|
-
//
|
|
33
|
-
const status = new OptimizerStatus(
|
|
30
|
+
// Icons follow the global pix-pretty mode (set via /pretty); no local state.
|
|
31
|
+
const status = new OptimizerStatus();
|
|
34
32
|
|
|
35
33
|
// Each module registers its own lifecycle hooks and returns a handle the
|
|
36
34
|
// /optimizer overlay renders + drives.
|
package/src/opt.ts
CHANGED
|
@@ -16,22 +16,12 @@
|
|
|
16
16
|
|
|
17
17
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
18
18
|
import { frameLines } from "@xynogen/pix-pretty/modal-frame";
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
type IconMode,
|
|
24
|
-
type OptimizerHandle,
|
|
25
|
-
type OptimizerStatus,
|
|
26
|
-
type OptimizerTool,
|
|
19
|
+
import type {
|
|
20
|
+
OptimizerHandle,
|
|
21
|
+
OptimizerStatus,
|
|
22
|
+
OptimizerTool,
|
|
27
23
|
} from "./status.ts";
|
|
28
|
-
|
|
29
|
-
/** Glyph for the icon-mode row itself (uses the active mode's diamond/Tn). */
|
|
30
|
-
const ICON_ROW_GLYPH: Record<IconMode, string> = {
|
|
31
|
-
nerd: "", // gear-ish; distinct from the four tool glyphs
|
|
32
|
-
unicode: "\u25C8\uFE0E", // ◈ diamond-in-diamond
|
|
33
|
-
ascii: "Ic",
|
|
34
|
-
};
|
|
24
|
+
import { toolIcon } from "./status.ts";
|
|
35
25
|
|
|
36
26
|
/** Min/max content width for the overlay box (excludes 4 cols of chrome). */
|
|
37
27
|
const MIN_CONTENT = 28;
|
|
@@ -86,7 +76,7 @@ export function buildOptHelp(
|
|
|
86
76
|
export function registerOptCommand(
|
|
87
77
|
pi: ExtensionAPI,
|
|
88
78
|
handles: Record<OptimizerTool, OptimizerHandle>,
|
|
89
|
-
|
|
79
|
+
_status: OptimizerStatus,
|
|
90
80
|
): void {
|
|
91
81
|
pi.registerCommand("optimizer", {
|
|
92
82
|
description: "pix-optimizer: caveman / rtk / toon / ponytail tools",
|
|
@@ -139,26 +129,9 @@ export function registerOptCommand(
|
|
|
139
129
|
_kb: unknown,
|
|
140
130
|
done: (v: null) => void,
|
|
141
131
|
) => {
|
|
142
|
-
// Rows = 4 tools + 1 icon-mode row at index TOOL_ORDER.length.
|
|
143
|
-
const ICON_ROW = TOOL_ORDER.length;
|
|
144
|
-
const ROW_COUNT = TOOL_ORDER.length + 1;
|
|
145
|
-
const ICON_HELP = "status-cell icon style (nerd needs a Nerd Font)";
|
|
146
132
|
let selected = 0;
|
|
147
133
|
|
|
148
|
-
const cycleIconMode = (direction: -1 | 1) => {
|
|
149
|
-
const cur = ICON_MODES.indexOf(status.mode);
|
|
150
|
-
const next =
|
|
151
|
-
(cur + direction + ICON_MODES.length) % ICON_MODES.length;
|
|
152
|
-
const mode = ICON_MODES[next]!;
|
|
153
|
-
status.setMode(mode, ctx);
|
|
154
|
-
saveIconMode(mode);
|
|
155
|
-
};
|
|
156
|
-
|
|
157
134
|
const cycle = (direction: -1 | 1) => {
|
|
158
|
-
if (selected === ICON_ROW) {
|
|
159
|
-
cycleIconMode(direction);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
135
|
const tool = TOOL_ORDER[selected]!;
|
|
163
136
|
const values = handles[tool].values;
|
|
164
137
|
const cur = values.indexOf(handles[tool].current());
|
|
@@ -167,17 +140,17 @@ export function registerOptCommand(
|
|
|
167
140
|
};
|
|
168
141
|
|
|
169
142
|
const move = (direction: -1 | 1) => {
|
|
170
|
-
selected =
|
|
143
|
+
selected =
|
|
144
|
+
(selected + direction + TOOL_ORDER.length) % TOOL_ORDER.length;
|
|
171
145
|
};
|
|
172
146
|
|
|
173
147
|
return {
|
|
174
148
|
render: () => {
|
|
175
|
-
const icons = getIcons(status.mode);
|
|
176
149
|
const rows = TOOL_ORDER.map((tool, i) => {
|
|
177
150
|
const on = handles[tool].current() !== "off";
|
|
178
151
|
const sel = i === selected;
|
|
179
152
|
const cursor = sel ? theme.fg("accent", "→") : " ";
|
|
180
|
-
const
|
|
153
|
+
const glyph = theme.fg(on ? "accent" : "dim", toolIcon(tool));
|
|
181
154
|
const name = theme.fg(
|
|
182
155
|
sel ? "accent" : on ? "text" : "muted",
|
|
183
156
|
tool.padEnd(nameWidth),
|
|
@@ -191,31 +164,17 @@ export function registerOptCommand(
|
|
|
191
164
|
on ? "accent" : "dim",
|
|
192
165
|
levelBar(cur, handles[tool].values),
|
|
193
166
|
);
|
|
194
|
-
return `${cursor} ${
|
|
167
|
+
return `${cursor} ${glyph} ${name} ${value} ${bar}`;
|
|
195
168
|
});
|
|
196
|
-
// Icon-mode row: glyph + "icons" + current mode name.
|
|
197
|
-
const iconSel = selected === ICON_ROW;
|
|
198
|
-
const iconCursor = iconSel ? theme.fg("accent", "→") : " ";
|
|
199
|
-
const iconGlyph = theme.fg("accent", ICON_ROW_GLYPH[status.mode]);
|
|
200
|
-
const iconName = theme.fg(
|
|
201
|
-
iconSel ? "accent" : "text",
|
|
202
|
-
"icons".padEnd(nameWidth),
|
|
203
|
-
);
|
|
204
|
-
const iconValue = theme.fg(
|
|
205
|
-
"success",
|
|
206
|
-
status.mode.padEnd(valueWidth),
|
|
207
|
-
);
|
|
208
|
-
const iconRow = `${iconCursor} ${iconGlyph} ${iconName} ${iconValue}`;
|
|
209
|
-
const helpText = iconSel
|
|
210
|
-
? ICON_HELP
|
|
211
|
-
: helpSummary(handles[TOOL_ORDER[selected]!].help);
|
|
212
169
|
const lines = [
|
|
213
170
|
theme.fg("accent", theme.bold(" Optimizer")),
|
|
214
171
|
"",
|
|
215
172
|
...rows,
|
|
216
|
-
iconRow,
|
|
217
173
|
"",
|
|
218
|
-
theme.fg(
|
|
174
|
+
theme.fg(
|
|
175
|
+
"dim",
|
|
176
|
+
helpSummary(handles[TOOL_ORDER[selected]!].help),
|
|
177
|
+
),
|
|
219
178
|
"",
|
|
220
179
|
theme.fg("dim", "←→ change · ↑↓ move · esc close"),
|
|
221
180
|
];
|
package/src/persist.test.ts
CHANGED
|
@@ -9,12 +9,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
|
9
9
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import {
|
|
13
|
-
loadIconMode,
|
|
14
|
-
loadOptValue,
|
|
15
|
-
saveIconMode,
|
|
16
|
-
saveOptValue,
|
|
17
|
-
} from "./persist.ts";
|
|
12
|
+
import { loadOptValue, saveOptValue } from "./persist.ts";
|
|
18
13
|
|
|
19
14
|
let tmpAgentDir: string;
|
|
20
15
|
|
|
@@ -58,21 +53,4 @@ describe("optimizer persistence", () => {
|
|
|
58
53
|
expect(loadOptValue("caveman")).toBe("ultra");
|
|
59
54
|
expect(loadOptValue("ponytail")).toBe("full");
|
|
60
55
|
});
|
|
61
|
-
|
|
62
|
-
test("icon mode round-trips and is independent of tool values", () => {
|
|
63
|
-
expect(loadIconMode()).toBeUndefined();
|
|
64
|
-
saveIconMode("unicode");
|
|
65
|
-
expect(loadIconMode()).toBe("unicode");
|
|
66
|
-
// Did not disturb existing tool state…
|
|
67
|
-
expect(loadOptValue("caveman")).toBe("ultra");
|
|
68
|
-
// …and a later tool write does not disturb the icon mode.
|
|
69
|
-
saveOptValue("rtk", "on");
|
|
70
|
-
expect(loadIconMode()).toBe("unicode");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("rejects an invalid persisted icon mode", () => {
|
|
74
|
-
saveOptValue("caveman", "lite"); // unrelated write
|
|
75
|
-
saveIconMode("ascii");
|
|
76
|
-
expect(loadIconMode()).toBe("ascii");
|
|
77
|
-
});
|
|
78
56
|
});
|
package/src/persist.ts
CHANGED
|
@@ -7,21 +7,14 @@
|
|
|
7
7
|
* restart. Each tool reads its value on session_start and writes on run().
|
|
8
8
|
*
|
|
9
9
|
* ~/.pi/agent/optimizer.json → { "caveman": "lite", "rtk": "on", ... }
|
|
10
|
-
*
|
|
11
|
-
* A reserved "iconMode" key (not a tool) stores the status-cell icon style.
|
|
12
10
|
*/
|
|
13
11
|
|
|
14
12
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
13
|
import { dirname, join } from "node:path";
|
|
16
14
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
/** Reserved (non-tool) key holding the icon presentation mode. */
|
|
20
|
-
const ICON_MODE_KEY = "iconMode";
|
|
15
|
+
import type { OptimizerTool } from "./status.ts";
|
|
21
16
|
|
|
22
|
-
type OptimizerConfig = Partial<Record<OptimizerTool, string
|
|
23
|
-
[ICON_MODE_KEY]?: string;
|
|
24
|
-
};
|
|
17
|
+
type OptimizerConfig = Partial<Record<OptimizerTool, string>>;
|
|
25
18
|
|
|
26
19
|
function getStatePath(): string {
|
|
27
20
|
return join(getAgentDir(), "optimizer.json");
|
|
@@ -55,21 +48,3 @@ export function saveOptValue(tool: OptimizerTool, value: string): void {
|
|
|
55
48
|
console.warn(`optimizer: persist ${tool} failed:`, err);
|
|
56
49
|
}
|
|
57
50
|
}
|
|
58
|
-
|
|
59
|
-
/** Read the persisted icon mode, or undefined if unset/invalid. */
|
|
60
|
-
export function loadIconMode(): IconMode | undefined {
|
|
61
|
-
const raw = readAll()[ICON_MODE_KEY];
|
|
62
|
-
return ICON_MODES.includes(raw as IconMode) ? (raw as IconMode) : undefined;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Persist the icon mode, merging into the shared config file. */
|
|
66
|
-
export function saveIconMode(mode: IconMode): void {
|
|
67
|
-
try {
|
|
68
|
-
const sp = getStatePath();
|
|
69
|
-
mkdirSync(dirname(sp), { recursive: true });
|
|
70
|
-
const next = { ...readAll(), [ICON_MODE_KEY]: mode };
|
|
71
|
-
writeFileSync(sp, JSON.stringify(next, null, 2), "utf-8");
|
|
72
|
-
} catch (err) {
|
|
73
|
-
console.warn("optimizer: persist iconMode failed:", err);
|
|
74
|
-
}
|
|
75
|
-
}
|
package/src/status.test.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
-
envIconMode,
|
|
4
|
-
getIcons,
|
|
5
|
-
ICON_MODES,
|
|
6
3
|
OptimizerStatus,
|
|
7
4
|
renderStatus,
|
|
8
5
|
STATUS_KEY,
|
|
9
|
-
|
|
6
|
+
toolIcon,
|
|
10
7
|
} from "./status.ts";
|
|
11
8
|
|
|
12
9
|
/** Tagging colorizer: <accent>X</accent> / <dim>X</dim> for assertions. */
|
|
13
10
|
const tag = (c: string, t: string) => `<${c}>${t}</${c}>`;
|
|
14
11
|
|
|
12
|
+
// Icons resolve against the global pix-pretty mode (default "nerd" in tests).
|
|
13
|
+
const CV = toolIcon("caveman");
|
|
14
|
+
const RK = toolIcon("rtk");
|
|
15
|
+
const TN = toolIcon("toon");
|
|
16
|
+
const PT = toolIcon("ponytail");
|
|
17
|
+
|
|
15
18
|
describe("renderStatus", () => {
|
|
16
19
|
it("shows ALL icons in order, accent when enabled", () => {
|
|
17
20
|
expect(
|
|
@@ -20,7 +23,7 @@ describe("renderStatus", () => {
|
|
|
20
23
|
tag,
|
|
21
24
|
),
|
|
22
25
|
).toBe(
|
|
23
|
-
`<accent>${
|
|
26
|
+
`<accent>${CV}</accent> <accent>${RK}</accent> <accent>${TN}</accent> <accent>${PT}</accent> `,
|
|
24
27
|
);
|
|
25
28
|
});
|
|
26
29
|
|
|
@@ -31,80 +34,25 @@ describe("renderStatus", () => {
|
|
|
31
34
|
tag,
|
|
32
35
|
),
|
|
33
36
|
).toBe(
|
|
34
|
-
`<dim>${
|
|
37
|
+
`<dim>${CV}</dim> <accent>${RK}</accent> <accent>${TN}</accent> <accent>${PT}</accent> `,
|
|
35
38
|
);
|
|
36
39
|
});
|
|
37
40
|
|
|
38
41
|
it("all dim when nothing enabled (cell never empty)", () => {
|
|
39
42
|
expect(renderStatus({}, tag)).toBe(
|
|
40
|
-
`<dim>${
|
|
43
|
+
`<dim>${CV}</dim> <dim>${RK}</dim> <dim>${TN}</dim> <dim>${PT}</dim> `,
|
|
41
44
|
);
|
|
42
45
|
});
|
|
43
46
|
|
|
44
47
|
it("preserves fixed order regardless of insertion order", () => {
|
|
45
48
|
expect(renderStatus({ toon: true, caveman: true }, tag)).toBe(
|
|
46
|
-
`<accent>${
|
|
47
|
-
);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe("icon modes", () => {
|
|
52
|
-
it("nerd is the default mode used by renderStatus", () => {
|
|
53
|
-
expect(renderStatus({ rtk: true }, tag)).toContain(TOOL_ICONS.rtk);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("renders the unicode (outline-suit) set when mode=unicode", () => {
|
|
57
|
-
const icons = getIcons("unicode");
|
|
58
|
-
const out = renderStatus({ caveman: true }, tag, "unicode");
|
|
59
|
-
expect(out).toContain(icons.caveman);
|
|
60
|
-
// outline white spade + text variation selector
|
|
61
|
-
expect(icons.caveman).toBe("\u2664\uFE0E");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("renders the ascii set when mode=ascii", () => {
|
|
65
|
-
const out = renderStatus(
|
|
66
|
-
{ caveman: true, rtk: false, toon: true, ponytail: false },
|
|
67
|
-
tag,
|
|
68
|
-
"ascii",
|
|
69
|
-
);
|
|
70
|
-
expect(out).toBe(
|
|
71
|
-
`<accent>Cv</accent> <dim>Rk</dim> <accent>Tn</accent> <dim>Pt</dim> `,
|
|
49
|
+
`<accent>${CV}</accent> <dim>${RK}</dim> <accent>${TN}</accent> <dim>${PT}</dim> `,
|
|
72
50
|
);
|
|
73
51
|
});
|
|
74
52
|
|
|
75
|
-
it("
|
|
76
|
-
//
|
|
77
|
-
expect(
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("exposes exactly nerd/unicode/ascii in cycle order", () => {
|
|
81
|
-
expect([...ICON_MODES]).toEqual(["nerd", "unicode", "ascii"]);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe("envIconMode", () => {
|
|
86
|
-
const saved = { ...process.env };
|
|
87
|
-
afterEach(() => {
|
|
88
|
-
process.env.OPTIMIZER_ICONS = saved.OPTIMIZER_ICONS;
|
|
89
|
-
process.env.PRETTY_ICONS = saved.PRETTY_ICONS;
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("defaults to nerd when nothing set", () => {
|
|
93
|
-
process.env.OPTIMIZER_ICONS = undefined;
|
|
94
|
-
process.env.PRETTY_ICONS = undefined;
|
|
95
|
-
expect(envIconMode()).toBe("nerd");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("OPTIMIZER_ICONS wins over PRETTY_ICONS", () => {
|
|
99
|
-
process.env.OPTIMIZER_ICONS = "unicode";
|
|
100
|
-
process.env.PRETTY_ICONS = "none";
|
|
101
|
-
expect(envIconMode()).toBe("unicode");
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("PRETTY_ICONS=none maps to ascii", () => {
|
|
105
|
-
process.env.OPTIMIZER_ICONS = undefined;
|
|
106
|
-
process.env.PRETTY_ICONS = "none";
|
|
107
|
-
expect(envIconMode()).toBe("ascii");
|
|
53
|
+
it("resolves icons via the shared catalog (non-empty glyphs)", () => {
|
|
54
|
+
// toolIcon delegates to pix-pretty's icon(); every tool has a glyph.
|
|
55
|
+
expect([CV, RK, TN, PT].every((g) => g.length > 0)).toBe(true);
|
|
108
56
|
});
|
|
109
57
|
});
|
|
110
58
|
|
|
@@ -130,7 +78,7 @@ describe("OptimizerStatus", () => {
|
|
|
130
78
|
expect(last.key).toBe(STATUS_KEY);
|
|
131
79
|
// caveman + toon + ponytail still unset (dim), rtk accent.
|
|
132
80
|
expect(last.text).toBe(
|
|
133
|
-
`<dim>${
|
|
81
|
+
`<dim>${CV}</dim> <accent>${RK}</accent> <dim>${TN}</dim> <dim>${PT}</dim> `,
|
|
134
82
|
);
|
|
135
83
|
});
|
|
136
84
|
|
|
@@ -141,7 +89,7 @@ describe("OptimizerStatus", () => {
|
|
|
141
89
|
status.set("toon", true, ctx as never);
|
|
142
90
|
const last = ctx.calls.at(-1)!;
|
|
143
91
|
expect(last.text).toBe(
|
|
144
|
-
`<accent>${
|
|
92
|
+
`<accent>${CV}</accent> <dim>${RK}</dim> <accent>${TN}</accent> <dim>${PT}</dim> `,
|
|
145
93
|
);
|
|
146
94
|
});
|
|
147
95
|
|
|
@@ -152,27 +100,7 @@ describe("OptimizerStatus", () => {
|
|
|
152
100
|
status.set("rtk", false, ctx as never);
|
|
153
101
|
const last = ctx.calls.at(-1)!;
|
|
154
102
|
expect(last.text).toBe(
|
|
155
|
-
`<dim>${
|
|
103
|
+
`<dim>${CV}</dim> <dim>${RK}</dim> <dim>${TN}</dim> <dim>${PT}</dim> `,
|
|
156
104
|
);
|
|
157
105
|
});
|
|
158
|
-
|
|
159
|
-
it("seeds icon mode from the constructor", () => {
|
|
160
|
-
const status = new OptimizerStatus("ascii");
|
|
161
|
-
expect(status.mode).toBe("ascii");
|
|
162
|
-
const ctx = fakeCtx();
|
|
163
|
-
status.set("caveman", true, ctx as never);
|
|
164
|
-
expect(ctx.calls.at(-1)!.text).toContain("Cv");
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("setMode switches the rendered glyph set and repaints", () => {
|
|
168
|
-
const status = new OptimizerStatus("nerd");
|
|
169
|
-
const ctx = fakeCtx();
|
|
170
|
-
status.set("rtk", true, ctx as never);
|
|
171
|
-
expect(ctx.calls.at(-1)!.text).toContain(TOOL_ICONS.rtk);
|
|
172
|
-
status.setMode("ascii", ctx as never);
|
|
173
|
-
const last = ctx.calls.at(-1)!;
|
|
174
|
-
expect(last.key).toBe(STATUS_KEY);
|
|
175
|
-
expect(last.text).toContain("Rk");
|
|
176
|
-
expect(last.text).not.toContain(TOOL_ICONS.rtk);
|
|
177
|
-
});
|
|
178
106
|
});
|
package/src/status.ts
CHANGED
|
@@ -17,6 +17,11 @@ import type {
|
|
|
17
17
|
ExtensionContext,
|
|
18
18
|
ThemeColor,
|
|
19
19
|
} from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import {
|
|
21
|
+
type IconKey,
|
|
22
|
+
icon,
|
|
23
|
+
onIconModeChange,
|
|
24
|
+
} from "@xynogen/pix-pretty/icon-catalog";
|
|
20
25
|
|
|
21
26
|
/**
|
|
22
27
|
* Each optimizer tool (caveman/rtk/toon/ponytail) exposes a handle so the
|
|
@@ -43,71 +48,23 @@ export const STATUS_KEY = "pix-optimizer";
|
|
|
43
48
|
export type OptimizerTool = "caveman" | "rtk" | "toon" | "ponytail";
|
|
44
49
|
|
|
45
50
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* every monospace font) + U+FE0E to force text (non-emoji) presentation.
|
|
51
|
-
* - `ascii`: two-letter labels, renders on literally any terminal.
|
|
52
|
-
* Mode is chosen via the /optimizer menu (persisted) or the OPTIMIZER_ICONS /
|
|
53
|
-
* PRETTY_ICONS env vars.
|
|
51
|
+
* Icons come from the shared pix-pretty catalog and follow the ONE global icon
|
|
52
|
+
* mode (set via /pretty). The optimizer no longer owns a mode: each tool maps
|
|
53
|
+
* to a semantic catalog key, and the cell renders whatever the active mode
|
|
54
|
+
* resolves — nerd / unicode / ascii — with no per-package knob.
|
|
54
55
|
*/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
/** Force text (non-emoji) presentation for symbols that default to emoji. */
|
|
61
|
-
const VS_TEXT = "\uFE0E";
|
|
62
|
-
|
|
63
|
-
/** Per-mode glyph table for each tool. */
|
|
64
|
-
const ICON_SETS: Record<IconMode, Record<OptimizerTool, string>> = {
|
|
65
|
-
nerd: {
|
|
66
|
-
caveman: "",
|
|
67
|
-
rtk: "",
|
|
68
|
-
toon: "",
|
|
69
|
-
ponytail: "",
|
|
70
|
-
},
|
|
71
|
-
unicode: {
|
|
72
|
-
caveman: `\u2664${VS_TEXT}`, // ♤ white spade
|
|
73
|
-
rtk: `\u2661${VS_TEXT}`, // ♡ white heart
|
|
74
|
-
toon: `\u2662${VS_TEXT}`, // ♢ white diamond
|
|
75
|
-
ponytail: `\u2667${VS_TEXT}`, // ♧ white club
|
|
76
|
-
},
|
|
77
|
-
ascii: {
|
|
78
|
-
caveman: "Cv",
|
|
79
|
-
rtk: "Rk",
|
|
80
|
-
toon: "Tn",
|
|
81
|
-
ponytail: "Pt",
|
|
82
|
-
},
|
|
56
|
+
const TOOL_ICON_KEY: Record<OptimizerTool, IconKey> = {
|
|
57
|
+
caveman: "opt.caveman",
|
|
58
|
+
rtk: "opt.rtk",
|
|
59
|
+
toon: "opt.toon",
|
|
60
|
+
ponytail: "opt.ponytail",
|
|
83
61
|
};
|
|
84
62
|
|
|
85
|
-
/**
|
|
86
|
-
export function
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Default icon mode from the environment. OPTIMIZER_ICONS takes precedence;
|
|
92
|
-
* PRETTY_ICONS=none|off maps to ascii so one stack-wide var disables glyphs.
|
|
93
|
-
* Anything unrecognized (or unset) falls back to `nerd`. A persisted menu
|
|
94
|
-
* choice overrides this at load time (see persist.loadIconMode).
|
|
95
|
-
*/
|
|
96
|
-
export function envIconMode(): IconMode {
|
|
97
|
-
const raw = (process.env.OPTIMIZER_ICONS ?? "").toLowerCase();
|
|
98
|
-
if (raw === "nerd" || raw === "unicode" || raw === "ascii") return raw;
|
|
99
|
-
const pretty = (process.env.PRETTY_ICONS ?? "").toLowerCase();
|
|
100
|
-
if (pretty === "none" || pretty === "off") return "ascii";
|
|
101
|
-
return "nerd";
|
|
63
|
+
/** Current glyph for a tool, resolved against the active global icon mode. */
|
|
64
|
+
export function toolIcon(tool: OptimizerTool): string {
|
|
65
|
+
return icon(TOOL_ICON_KEY[tool]);
|
|
102
66
|
}
|
|
103
67
|
|
|
104
|
-
/**
|
|
105
|
-
* Default Nerd Font glyph table. Kept as a named export for back-compat with
|
|
106
|
-
* callers/tests that reference the nerd set directly; equivalent to
|
|
107
|
-
* getIcons("nerd").
|
|
108
|
-
*/
|
|
109
|
-
export const TOOL_ICONS: Record<OptimizerTool, string> = ICON_SETS.nerd;
|
|
110
|
-
|
|
111
68
|
/** Fixed left-to-right order of icons in the cell. */
|
|
112
69
|
const TOOL_ORDER: readonly OptimizerTool[] = [
|
|
113
70
|
"caveman",
|
|
@@ -127,8 +84,8 @@ export type Colorize = (color: ThemeColor, text: string) => string;
|
|
|
127
84
|
/**
|
|
128
85
|
* Build the colored status string for a set of tool states. ALL tool icons are
|
|
129
86
|
* always shown, in TOOL_ORDER; each is accent-colored when its tool is enabled
|
|
130
|
-
* and dim when disabled.
|
|
131
|
-
* `
|
|
87
|
+
* and dim when disabled. Glyphs resolve against the active global icon mode
|
|
88
|
+
* (see /pretty). `color` applies the theme color (e.g. theme.fg).
|
|
132
89
|
*
|
|
133
90
|
* Pure + exported for tests (pass a tagging colorizer to assert per-icon color).
|
|
134
91
|
* A trailing space separates the cell from the next status segment.
|
|
@@ -136,11 +93,9 @@ export type Colorize = (color: ThemeColor, text: string) => string;
|
|
|
136
93
|
export function renderStatus(
|
|
137
94
|
states: Partial<Record<OptimizerTool, boolean>>,
|
|
138
95
|
color: Colorize,
|
|
139
|
-
mode: IconMode = "nerd",
|
|
140
96
|
): string {
|
|
141
|
-
const icons = getIcons(mode);
|
|
142
97
|
return `${TOOL_ORDER.map((t) =>
|
|
143
|
-
color(states[t] === true ? ENABLED_COLOR : DISABLED_COLOR,
|
|
98
|
+
color(states[t] === true ? ENABLED_COLOR : DISABLED_COLOR, toolIcon(t)),
|
|
144
99
|
).join(" ")} `;
|
|
145
100
|
}
|
|
146
101
|
|
|
@@ -151,11 +106,16 @@ export function renderStatus(
|
|
|
151
106
|
*/
|
|
152
107
|
export class OptimizerStatus {
|
|
153
108
|
private states: Partial<Record<OptimizerTool, boolean>> = {};
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
constructor(
|
|
158
|
-
|
|
109
|
+
/** Latest ui ctx seen, so an icon-mode change can repaint without a toggle. */
|
|
110
|
+
private lastCtx: Pick<ExtensionContext, "ui"> | undefined;
|
|
111
|
+
|
|
112
|
+
constructor() {
|
|
113
|
+
// The cell is a pushed status (setStatus), so it does NOT auto-refresh
|
|
114
|
+
// when /pretty flips the global icon mode. Repaint it on mode change
|
|
115
|
+
// using the last-seen ctx (no-op until the cell has painted once).
|
|
116
|
+
onIconModeChange(() => {
|
|
117
|
+
if (this.lastCtx) this.paint(this.lastCtx);
|
|
118
|
+
});
|
|
159
119
|
}
|
|
160
120
|
|
|
161
121
|
/** Current enabled state for one tool (undefined until first set). */
|
|
@@ -163,17 +123,6 @@ export class OptimizerStatus {
|
|
|
163
123
|
return this.states[tool];
|
|
164
124
|
}
|
|
165
125
|
|
|
166
|
-
/** Current icon presentation mode. */
|
|
167
|
-
get mode(): IconMode {
|
|
168
|
-
return this.iconMode;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Switch icon mode and repaint the shared cell. */
|
|
172
|
-
setMode(mode: IconMode, ctx: Pick<ExtensionContext, "ui">): void {
|
|
173
|
-
this.iconMode = mode;
|
|
174
|
-
this.paint(ctx);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
126
|
/** Update one tool's enabled state and repaint the shared cell. */
|
|
178
127
|
set(
|
|
179
128
|
tool: OptimizerTool,
|
|
@@ -186,11 +135,8 @@ export class OptimizerStatus {
|
|
|
186
135
|
|
|
187
136
|
/** Repaint the shared cell from current states. */
|
|
188
137
|
paint(ctx: Pick<ExtensionContext, "ui">): void {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
(c, t) => ctx.ui.theme.fg(c, t),
|
|
192
|
-
this.iconMode,
|
|
193
|
-
);
|
|
138
|
+
this.lastCtx = ctx;
|
|
139
|
+
const text = renderStatus(this.states, (c, t) => ctx.ui.theme.fg(c, t));
|
|
194
140
|
ctx.ui.setStatus(STATUS_KEY, text);
|
|
195
141
|
}
|
|
196
142
|
}
|