@xynogen/pix-optimizer 1.1.7 → 1.1.9
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 +65 -41
- package/package.json +1 -1
- package/src/index.ts +9 -5
- package/src/opt.ts +50 -9
- package/src/persist.test.ts +23 -1
- package/src/persist.ts +27 -2
- package/src/status.test.ts +84 -1
- package/src/status.ts +95 -12
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pix-optimizer
|
|
2
2
|
|
|
3
|
-
Token-optimization suite for Pi Coding Agent.
|
|
4
|
-
extension via `src/index.ts`, fronted by a single `/
|
|
3
|
+
Token-optimization suite for Pi Coding Agent. Four tools wired into one
|
|
4
|
+
extension via `src/index.ts`, fronted by a single `/optimizer` command and one
|
|
5
5
|
shared status-bar cell:
|
|
6
6
|
|
|
7
7
|
- **Caveman** (``) — terse-output system prompt
|
|
@@ -11,20 +11,45 @@ shared status-bar cell:
|
|
|
11
11
|
|
|
12
12
|
## Command
|
|
13
13
|
|
|
14
|
-
One command
|
|
14
|
+
One command opens an interactive overlay that fronts every tool:
|
|
15
15
|
|
|
16
16
|
```text
|
|
17
|
-
/
|
|
18
|
-
/opt caveman <level> → set caveman level (1/2/3/lite/full/ultra/micro/off/config)
|
|
19
|
-
/opt rtk [on|off] → toggle RTK rewriting
|
|
20
|
-
/opt toon [on|off] → toggle jq+TOON guidance
|
|
21
|
-
/opt ponytail <level> → set ponytail level (1/2/3/lite/full/ultra/off/config)
|
|
17
|
+
/optimizer → open the overlay (←→ cycle value, ↑↓ move, esc close)
|
|
22
18
|
```
|
|
23
19
|
|
|
20
|
+
There is no text-arg form: the overlay is the only UI. Selecting a value calls
|
|
21
|
+
the tool's `run()` handler, which persists the new value and repaints the
|
|
22
|
+
shared status cell. Headless/test fallbacks print a plain status summary.
|
|
23
|
+
|
|
24
|
+
State persists to `~/.pi/agent/optimizer.json` (caveman/rtk/toon/ponytail).
|
|
25
|
+
|
|
24
26
|
## Status bar
|
|
25
27
|
|
|
26
|
-
A single cell always shows
|
|
27
|
-
|
|
28
|
+
A single cell always shows all four tool icons in a fixed order, color-coded by
|
|
29
|
+
state: **accent** when the tool is enabled, **dim** when disabled.
|
|
30
|
+
|
|
31
|
+
### Icon style (Nerd Font, Unicode, or ASCII)
|
|
32
|
+
|
|
33
|
+
The default glyphs (` `) are **Nerd Font** symbols and require a patched
|
|
34
|
+
font (e.g. MesloLGS NF). Terminals without one render them as missing-glyph
|
|
35
|
+
“tofu” boxes. Two font-independent fallbacks are available:
|
|
36
|
+
|
|
37
|
+
| Mode | Glyphs | Needs Nerd Font? |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `nerd` (default) | ` ` | yes |
|
|
40
|
+
| `unicode` | `♤ ♡ ♢ ♧` (outline card suits) | no |
|
|
41
|
+
| `ascii` | `Cv Rk Tn Pt` | no |
|
|
42
|
+
|
|
43
|
+
Switch the style live from the **`/optimizer` overlay** — a fifth `icons` row
|
|
44
|
+
at the bottom cycles `nerd → unicode → ascii` with `←→`; the choice persists to
|
|
45
|
+
`~/.pi/agent/optimizer.json`.
|
|
46
|
+
|
|
47
|
+
Or set it via env var (a persisted menu choice takes precedence):
|
|
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
|
+
```
|
|
28
53
|
|
|
29
54
|
## Features
|
|
30
55
|
|
|
@@ -32,15 +57,15 @@ coded by state: **accent** when the tool is enabled, **dim** when disabled.
|
|
|
32
57
|
|
|
33
58
|
Cuts ~75% of output tokens while keeping full technical accuracy.
|
|
34
59
|
|
|
35
|
-
|
|
|
36
|
-
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
60
|
+
| Level | Description |
|
|
61
|
+
|-------|------------------------------|
|
|
62
|
+
| lite | Professional, no fluff |
|
|
63
|
+
| full | Classic caveman |
|
|
64
|
+
| ultra | Maximum compression |
|
|
65
|
+
| micro | Experimental prompt-minimized |
|
|
41
66
|
|
|
42
|
-
`/
|
|
43
|
-
|
|
67
|
+
The `/optimizer` overlay opens a settings dialog when needed. Default level
|
|
68
|
+
for new sessions is restored from `~/.pi/agent/optimizer.json`.
|
|
44
69
|
|
|
45
70
|
### RTK Tool Rewriting (``)
|
|
46
71
|
|
|
@@ -88,14 +113,11 @@ writing code the agent stops at the first rung that holds: does this need to
|
|
|
88
113
|
exist → stdlib → native platform → installed dep → one line → minimum that
|
|
89
114
|
works. Validation, error handling, security, and accessibility are never cut.
|
|
90
115
|
|
|
91
|
-
|
|
|
92
|
-
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
96
|
-
|
|
97
|
-
`/opt ponytail config` opens a settings dialog. Default level for new sessions
|
|
98
|
-
and status-bar visibility are saved to `~/.pi/agent/ponytail.json`.
|
|
116
|
+
| Level | Description |
|
|
117
|
+
|-------|--------------------------------------|
|
|
118
|
+
| lite | Name the lazier alternative, you pick |
|
|
119
|
+
| full | The ladder enforced (default) |
|
|
120
|
+
| ultra | YAGNI extremist |
|
|
99
121
|
|
|
100
122
|
**No install required** — pure prompt injection, no external binary or PATH
|
|
101
123
|
dependency (unlike RTK and TOON).
|
|
@@ -116,16 +138,18 @@ pi install npm:@xynogen/pix-optimizer
|
|
|
116
138
|
|
|
117
139
|
| File | Role |
|
|
118
140
|
|-------------------|-----------------------------------------------------------|
|
|
119
|
-
| `src/index.ts` | Wires the four tools + shared status, registers `/
|
|
120
|
-
| `src/opt.ts` | The `/
|
|
141
|
+
| `src/index.ts` | Wires the four tools + shared status, registers `/optimizer` |
|
|
142
|
+
| `src/opt.ts` | The `/optimizer` overlay UI (keyboard nav + cycling) |
|
|
121
143
|
| `src/status.ts` | Shared status-bar cell + `OptimizerHandle` contract |
|
|
122
|
-
| `src/caveman.ts` | Caveman logic, levels, prompt
|
|
144
|
+
| `src/caveman.ts` | Caveman logic, levels, prompt |
|
|
123
145
|
| `src/rtk.ts` | RTK prompt + bash command rewriting |
|
|
124
146
|
| `src/json.ts` | jq+TOON guidance, heuristics, system-prompt injection |
|
|
125
|
-
| `src/ponytail.ts` | Ponytail logic, levels, prompt
|
|
147
|
+
| `src/ponytail.ts` | Ponytail logic, levels, prompt |
|
|
148
|
+
| `src/persist.ts` | Disk-backed `~/.pi/agent/optimizer.json` persistence |
|
|
149
|
+
| `src/tool-result-filter.ts` | Strips model-guidance warnings from tool_result |
|
|
126
150
|
|
|
127
151
|
Each tool registers its own lifecycle hooks and exposes an `OptimizerHandle`
|
|
128
|
-
that `/
|
|
152
|
+
that `/optimizer` dispatches to. All four share one `OptimizerStatus`.
|
|
129
153
|
|
|
130
154
|
## Development
|
|
131
155
|
|
|
@@ -137,22 +161,22 @@ bun test
|
|
|
137
161
|
|
|
138
162
|
This package was built by merging two upstream Pi community packages:
|
|
139
163
|
|
|
140
|
-
- **Caveman mode** — merged from
|
|
141
|
-
|
|
142
|
-
|
|
164
|
+
- **Caveman mode** — merged from `npm:pi-caveman`. Reimplemented here with
|
|
165
|
+
multiple compression levels and integration with the shared `/optimizer`
|
|
166
|
+
command.
|
|
143
167
|
|
|
144
|
-
- **RTK rewriting** — merged from `npm:pi-rtk-optimizer`. Reimplemented here
|
|
145
|
-
approach: prompt injection + live bash command rewriting
|
|
146
|
-
(`&&`, `||`, `;`, `|`).
|
|
168
|
+
- **RTK rewriting** — merged from `npm:pi-rtk-optimizer`. Reimplemented here
|
|
169
|
+
with a two-layer approach: prompt injection + live bash command rewriting
|
|
170
|
+
that handles chained commands (`&&`, `||`, `;`, `|`).
|
|
147
171
|
|
|
148
172
|
- **Ponytail mode** — ruleset adapted from [`git:github.com/DietrichGebert/ponytail`](https://github.com/DietrichGebert/ponytail),
|
|
149
|
-
the "lazy senior dev" skill. Reimplemented here as a native `/
|
|
150
|
-
|
|
173
|
+
the "lazy senior dev" skill. Reimplemented here as a native `/optimizer`
|
|
174
|
+
tool with three intensity levels — no external hooks or files. The
|
|
151
175
|
ruleset (the YAGNI ladder + safety carve-outs) is rewritten as a system-prompt fragment.
|
|
152
176
|
|
|
153
177
|
All upstreams are MIT licensed. No codebase was copied directly — the logic was
|
|
154
|
-
rewritten and combined into a single extension with a unified `/
|
|
155
|
-
This package does not sync back to any upstream.
|
|
178
|
+
rewritten and combined into a single extension with a unified `/optimizer`
|
|
179
|
+
command and shared status bar. This package does not sync back to any upstream.
|
|
156
180
|
|
|
157
181
|
## Full distro
|
|
158
182
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-optimizer",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.9",
|
|
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
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* pix-optimizer — token-optimization suite for Pi Coding Agent.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Four tools, combined into one extension + one command:
|
|
5
5
|
* - caveman: terse-output system prompt
|
|
6
6
|
* - rtk: prefixes shell commands with `rtk` + injects RTK prompt
|
|
7
7
|
* - toon: jq + TOON guidance for dense JSON (+ bundled skill)
|
|
8
8
|
* - ponytail: lazy-senior-dev system prompt (minimal code, YAGNI)
|
|
9
9
|
*
|
|
10
|
-
* They share ONE status-bar cell (
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* They share ONE status-bar cell (all four icons always shown — dimmed when
|
|
11
|
+
* off, accented when on; icon style is nerd/unicode/ascii, set via /optimizer)
|
|
12
|
+
* and ONE command (/optimizer — an interactive overlay). index.ts wires
|
|
13
|
+
* lifecycle hooks via each module, then registers the overlay command.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
17
|
import { caveman } from "./caveman.ts";
|
|
17
18
|
import { json } from "./json.ts";
|
|
18
19
|
import { registerOptCommand } from "./opt.ts";
|
|
20
|
+
import { loadIconMode } from "./persist.ts";
|
|
19
21
|
import { ponytail } from "./ponytail.ts";
|
|
20
22
|
import { rtk } from "./rtk.ts";
|
|
21
23
|
import {
|
|
24
|
+
envIconMode,
|
|
22
25
|
type OptimizerHandle,
|
|
23
26
|
OptimizerStatus,
|
|
24
27
|
type OptimizerTool,
|
|
@@ -26,7 +29,8 @@ import {
|
|
|
26
29
|
import { filterModelWarnings } from "./tool-result-filter.ts";
|
|
27
30
|
|
|
28
31
|
export default function optimizer(pi: ExtensionAPI) {
|
|
29
|
-
|
|
32
|
+
// Persisted menu choice wins; otherwise fall back to the env default.
|
|
33
|
+
const status = new OptimizerStatus(loadIconMode() ?? envIconMode());
|
|
30
34
|
|
|
31
35
|
// Each module registers its own lifecycle hooks and returns a handle the
|
|
32
36
|
// /optimizer overlay renders + drives.
|
package/src/opt.ts
CHANGED
|
@@ -16,13 +16,23 @@
|
|
|
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 { saveIconMode } from "./persist.ts";
|
|
19
20
|
import {
|
|
21
|
+
getIcons,
|
|
22
|
+
ICON_MODES,
|
|
23
|
+
type IconMode,
|
|
20
24
|
type OptimizerHandle,
|
|
21
25
|
type OptimizerStatus,
|
|
22
26
|
type OptimizerTool,
|
|
23
|
-
TOOL_ICONS,
|
|
24
27
|
} from "./status.ts";
|
|
25
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
|
+
};
|
|
35
|
+
|
|
26
36
|
/** Min/max content width for the overlay box (excludes 4 cols of chrome). */
|
|
27
37
|
const MIN_CONTENT = 28;
|
|
28
38
|
const MAX_CONTENT = 60;
|
|
@@ -76,7 +86,7 @@ export function buildOptHelp(
|
|
|
76
86
|
export function registerOptCommand(
|
|
77
87
|
pi: ExtensionAPI,
|
|
78
88
|
handles: Record<OptimizerTool, OptimizerHandle>,
|
|
79
|
-
|
|
89
|
+
status: OptimizerStatus,
|
|
80
90
|
): void {
|
|
81
91
|
pi.registerCommand("optimizer", {
|
|
82
92
|
description: "pix-optimizer: caveman / rtk / toon / ponytail tools",
|
|
@@ -129,9 +139,26 @@ export function registerOptCommand(
|
|
|
129
139
|
_kb: unknown,
|
|
130
140
|
done: (v: null) => void,
|
|
131
141
|
) => {
|
|
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)";
|
|
132
146
|
let selected = 0;
|
|
133
147
|
|
|
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
|
+
|
|
134
157
|
const cycle = (direction: -1 | 1) => {
|
|
158
|
+
if (selected === ICON_ROW) {
|
|
159
|
+
cycleIconMode(direction);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
135
162
|
const tool = TOOL_ORDER[selected]!;
|
|
136
163
|
const values = handles[tool].values;
|
|
137
164
|
const cur = values.indexOf(handles[tool].current());
|
|
@@ -140,17 +167,17 @@ export function registerOptCommand(
|
|
|
140
167
|
};
|
|
141
168
|
|
|
142
169
|
const move = (direction: -1 | 1) => {
|
|
143
|
-
selected =
|
|
144
|
-
(selected + direction + TOOL_ORDER.length) % TOOL_ORDER.length;
|
|
170
|
+
selected = (selected + direction + ROW_COUNT) % ROW_COUNT;
|
|
145
171
|
};
|
|
146
172
|
|
|
147
173
|
return {
|
|
148
174
|
render: () => {
|
|
175
|
+
const icons = getIcons(status.mode);
|
|
149
176
|
const rows = TOOL_ORDER.map((tool, i) => {
|
|
150
177
|
const on = handles[tool].current() !== "off";
|
|
151
178
|
const sel = i === selected;
|
|
152
179
|
const cursor = sel ? theme.fg("accent", "→") : " ";
|
|
153
|
-
const icon = theme.fg(on ? "accent" : "dim",
|
|
180
|
+
const icon = theme.fg(on ? "accent" : "dim", icons[tool]);
|
|
154
181
|
const name = theme.fg(
|
|
155
182
|
sel ? "accent" : on ? "text" : "muted",
|
|
156
183
|
tool.padEnd(nameWidth),
|
|
@@ -166,15 +193,29 @@ export function registerOptCommand(
|
|
|
166
193
|
);
|
|
167
194
|
return `${cursor} ${icon} ${name} ${value} ${bar}`;
|
|
168
195
|
});
|
|
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);
|
|
169
212
|
const lines = [
|
|
170
213
|
theme.fg("accent", theme.bold(" Optimizer")),
|
|
171
214
|
"",
|
|
172
215
|
...rows,
|
|
216
|
+
iconRow,
|
|
173
217
|
"",
|
|
174
|
-
theme.fg(
|
|
175
|
-
"dim",
|
|
176
|
-
helpSummary(handles[TOOL_ORDER[selected]!].help),
|
|
177
|
-
),
|
|
218
|
+
theme.fg("dim", helpText),
|
|
178
219
|
"",
|
|
179
220
|
theme.fg("dim", "←→ change · ↑↓ move · esc close"),
|
|
180
221
|
];
|
package/src/persist.test.ts
CHANGED
|
@@ -9,7 +9,12 @@ 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 {
|
|
12
|
+
import {
|
|
13
|
+
loadIconMode,
|
|
14
|
+
loadOptValue,
|
|
15
|
+
saveIconMode,
|
|
16
|
+
saveOptValue,
|
|
17
|
+
} from "./persist.ts";
|
|
13
18
|
|
|
14
19
|
let tmpAgentDir: string;
|
|
15
20
|
|
|
@@ -53,4 +58,21 @@ describe("optimizer persistence", () => {
|
|
|
53
58
|
expect(loadOptValue("caveman")).toBe("ultra");
|
|
54
59
|
expect(loadOptValue("ponytail")).toBe("full");
|
|
55
60
|
});
|
|
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
|
+
});
|
|
56
78
|
});
|
package/src/persist.ts
CHANGED
|
@@ -7,14 +7,21 @@
|
|
|
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.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
15
|
import { dirname, join } from "node:path";
|
|
14
16
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import type
|
|
17
|
+
import { ICON_MODES, type IconMode, type OptimizerTool } from "./status.ts";
|
|
18
|
+
|
|
19
|
+
/** Reserved (non-tool) key holding the icon presentation mode. */
|
|
20
|
+
const ICON_MODE_KEY = "iconMode";
|
|
16
21
|
|
|
17
|
-
type OptimizerConfig = Partial<Record<OptimizerTool, string
|
|
22
|
+
type OptimizerConfig = Partial<Record<OptimizerTool, string>> & {
|
|
23
|
+
[ICON_MODE_KEY]?: string;
|
|
24
|
+
};
|
|
18
25
|
|
|
19
26
|
function getStatePath(): string {
|
|
20
27
|
return join(getAgentDir(), "optimizer.json");
|
|
@@ -48,3 +55,21 @@ export function saveOptValue(tool: OptimizerTool, value: string): void {
|
|
|
48
55
|
console.warn(`optimizer: persist ${tool} failed:`, err);
|
|
49
56
|
}
|
|
50
57
|
}
|
|
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,5 +1,8 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
+
envIconMode,
|
|
4
|
+
getIcons,
|
|
5
|
+
ICON_MODES,
|
|
3
6
|
OptimizerStatus,
|
|
4
7
|
renderStatus,
|
|
5
8
|
STATUS_KEY,
|
|
@@ -45,6 +48,66 @@ describe("renderStatus", () => {
|
|
|
45
48
|
});
|
|
46
49
|
});
|
|
47
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> `,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("falls back to nerd for an unknown mode", () => {
|
|
76
|
+
// @ts-expect-error exercising the runtime guard in getIcons
|
|
77
|
+
expect(getIcons("bogus")).toBe(getIcons("nerd"));
|
|
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");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
48
111
|
describe("OptimizerStatus", () => {
|
|
49
112
|
/** Minimal ui stub capturing setStatus calls. */
|
|
50
113
|
function fakeCtx() {
|
|
@@ -92,4 +155,24 @@ describe("OptimizerStatus", () => {
|
|
|
92
155
|
`<dim>${TOOL_ICONS.caveman}</dim> <dim>${TOOL_ICONS.rtk}</dim> <dim>${TOOL_ICONS.toon}</dim> <dim>${TOOL_ICONS.ponytail}</dim> `,
|
|
93
156
|
);
|
|
94
157
|
});
|
|
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
|
+
});
|
|
95
178
|
});
|
package/src/status.ts
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
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
|
-
* the cell renders
|
|
8
|
-
*
|
|
7
|
+
* the cell renders ALL four icons in a fixed order — accent-colored when the
|
|
8
|
+
* tool is enabled, dim when disabled. The cell is never empty.
|
|
9
9
|
*
|
|
10
|
-
* all on:
|
|
11
|
-
* caveman off:
|
|
12
|
-
* all off:
|
|
10
|
+
* all on: all four accent
|
|
11
|
+
* caveman off: caveman dim, rest accent
|
|
12
|
+
* all off: all four dim
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type {
|
|
@@ -42,13 +42,72 @@ export const STATUS_KEY = "pix-optimizer";
|
|
|
42
42
|
/** Tools that participate in the shared indicator, in render order. */
|
|
43
43
|
export type OptimizerTool = "caveman" | "rtk" | "toon" | "ponytail";
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Icon presentation modes. The optimizer cell defaults to Nerd Font glyphs,
|
|
47
|
+
* which require a patched font (e.g. MesloLGS NF). Terminals without one show
|
|
48
|
+
* missing-glyph tofu, so two font-independent fallbacks are offered:
|
|
49
|
+
* - `unicode`: outline playing-card suits (standard BMP, ships with virtually
|
|
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.
|
|
54
|
+
*/
|
|
55
|
+
export type IconMode = "nerd" | "unicode" | "ascii";
|
|
56
|
+
|
|
57
|
+
/** All selectable icon modes, in /optimizer cycle order. */
|
|
58
|
+
export const ICON_MODES: readonly IconMode[] = ["nerd", "unicode", "ascii"];
|
|
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
|
+
},
|
|
50
83
|
};
|
|
51
84
|
|
|
85
|
+
/** Resolve the glyph table for a mode. */
|
|
86
|
+
export function getIcons(mode: IconMode): Record<OptimizerTool, string> {
|
|
87
|
+
return ICON_SETS[mode] ?? ICON_SETS.nerd;
|
|
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";
|
|
102
|
+
}
|
|
103
|
+
|
|
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
|
+
|
|
52
111
|
/** Fixed left-to-right order of icons in the cell. */
|
|
53
112
|
const TOOL_ORDER: readonly OptimizerTool[] = [
|
|
54
113
|
"caveman",
|
|
@@ -69,6 +128,7 @@ export type Colorize = (color: ThemeColor, text: string) => string;
|
|
|
69
128
|
* Build the colored status string for a set of tool states. ALL tool icons are
|
|
70
129
|
* always shown, in TOOL_ORDER; each is accent-colored when its tool is enabled
|
|
71
130
|
* and dim when disabled. `color` applies the theme color (e.g. theme.fg).
|
|
131
|
+
* `mode` selects the glyph table (nerd / unicode / ascii); defaults to nerd.
|
|
72
132
|
*
|
|
73
133
|
* Pure + exported for tests (pass a tagging colorizer to assert per-icon color).
|
|
74
134
|
* A trailing space separates the cell from the next status segment.
|
|
@@ -76,9 +136,11 @@ export type Colorize = (color: ThemeColor, text: string) => string;
|
|
|
76
136
|
export function renderStatus(
|
|
77
137
|
states: Partial<Record<OptimizerTool, boolean>>,
|
|
78
138
|
color: Colorize,
|
|
139
|
+
mode: IconMode = "nerd",
|
|
79
140
|
): string {
|
|
141
|
+
const icons = getIcons(mode);
|
|
80
142
|
return `${TOOL_ORDER.map((t) =>
|
|
81
|
-
color(states[t] === true ? ENABLED_COLOR : DISABLED_COLOR,
|
|
143
|
+
color(states[t] === true ? ENABLED_COLOR : DISABLED_COLOR, icons[t]),
|
|
82
144
|
).join(" ")} `;
|
|
83
145
|
}
|
|
84
146
|
|
|
@@ -89,12 +151,29 @@ export function renderStatus(
|
|
|
89
151
|
*/
|
|
90
152
|
export class OptimizerStatus {
|
|
91
153
|
private states: Partial<Record<OptimizerTool, boolean>> = {};
|
|
154
|
+
private iconMode: IconMode;
|
|
155
|
+
|
|
156
|
+
/** `mode` seeds the icon set (persisted choice or env default). */
|
|
157
|
+
constructor(mode: IconMode = "nerd") {
|
|
158
|
+
this.iconMode = mode;
|
|
159
|
+
}
|
|
92
160
|
|
|
93
161
|
/** Current enabled state for one tool (undefined until first set). */
|
|
94
162
|
get(tool: OptimizerTool): boolean | undefined {
|
|
95
163
|
return this.states[tool];
|
|
96
164
|
}
|
|
97
165
|
|
|
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
|
+
|
|
98
177
|
/** Update one tool's enabled state and repaint the shared cell. */
|
|
99
178
|
set(
|
|
100
179
|
tool: OptimizerTool,
|
|
@@ -107,7 +186,11 @@ export class OptimizerStatus {
|
|
|
107
186
|
|
|
108
187
|
/** Repaint the shared cell from current states. */
|
|
109
188
|
paint(ctx: Pick<ExtensionContext, "ui">): void {
|
|
110
|
-
const text = renderStatus(
|
|
189
|
+
const text = renderStatus(
|
|
190
|
+
this.states,
|
|
191
|
+
(c, t) => ctx.ui.theme.fg(c, t),
|
|
192
|
+
this.iconMode,
|
|
193
|
+
);
|
|
111
194
|
ctx.ui.setStatus(STATUS_KEY, text);
|
|
112
195
|
}
|
|
113
196
|
}
|