@xynogen/pix-optimizer 1.1.9 → 1.1.12

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 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** (`󰜐`) — terse-output system prompt
8
- - **RTK** (`󰓥`) — prefixes shell commands with `rtk` + injects RTK prompt
9
- - **TOON** (`󰗀`) — jq + TOON guidance for dense JSON (skill lives in pix-skills)
10
- - **Ponytail** (`󰆐`) — lazy-senior-dev system prompt (minimal code, YAGNI)
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 (`󰜐 󰓥 󰗀 󰆐`) are **Nerd Font** symbols and require a patched
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) | `󰜐 󰓥 󰗀 󰆐` | yes |
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
- 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
- ```
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 + `OptimizerHandle` contract |
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.9",
3
+ "version": "1.1.12",
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",
@@ -48,7 +48,7 @@
48
48
  "access": "public"
49
49
  },
50
50
  "dependencies": {
51
- "@xynogen/pix-pretty": "*"
51
+ "@xynogen/pix-pretty": "^1.7.9"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@earendil-works/pi-coding-agent": "*",
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
- // Persisted menu choice wins; otherwise fall back to the env default.
33
- const status = new OptimizerStatus(loadIconMode() ?? envIconMode());
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/json.ts CHANGED
@@ -182,7 +182,8 @@ export function isUniformObjectArray(arr: unknown[]): boolean {
182
182
  for (let i = 0; i < keys.length; i++) {
183
183
  if (elKeys[i] !== keys[i]) return false;
184
184
  // values must be primitive for a clean tabular row
185
- if (isObjectLike((el as Record<string, unknown>)[keys[i]!])) return false;
185
+ if (isObjectLike((el as Record<string, unknown>)[keys[i] as string]))
186
+ return false;
186
187
  }
187
188
  return true;
188
189
  });
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 { saveIconMode } from "./persist.ts";
20
- import {
21
- getIcons,
22
- ICON_MODES,
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
- status: OptimizerStatus,
79
+ _status: OptimizerStatus,
90
80
  ): void {
91
81
  pi.registerCommand("optimizer", {
92
82
  description: "pix-optimizer: caveman / rtk / toon / ponytail tools",
@@ -139,45 +129,28 @@ 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
- const tool = TOOL_ORDER[selected]!;
135
+ const tool = TOOL_ORDER[selected] as OptimizerTool;
163
136
  const values = handles[tool].values;
164
137
  const cur = values.indexOf(handles[tool].current());
165
138
  const next = (cur + direction + values.length) % values.length;
166
- handles[tool].run(values[next]!, ctx);
139
+ handles[tool].run(values[next] as string, ctx);
167
140
  };
168
141
 
169
142
  const move = (direction: -1 | 1) => {
170
- selected = (selected + direction + ROW_COUNT) % ROW_COUNT;
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 icon = theme.fg(on ? "accent" : "dim", icons[tool]);
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,19 @@ export function registerOptCommand(
191
164
  on ? "accent" : "dim",
192
165
  levelBar(cur, handles[tool].values),
193
166
  );
194
- return `${cursor} ${icon} ${name} ${value} ${bar}`;
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("dim", helpText),
174
+ theme.fg(
175
+ "dim",
176
+ helpSummary(
177
+ handles[TOOL_ORDER[selected] as OptimizerTool].help,
178
+ ),
179
+ ),
219
180
  "",
220
181
  theme.fg("dim", "←→ change · ↑↓ move · esc close"),
221
182
  ];
@@ -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 { 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";
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/rtk.ts CHANGED
@@ -156,7 +156,7 @@ export function splitChain(command: string): string[] | null {
156
156
  let quote: "'" | '"' | null = null;
157
157
 
158
158
  for (let i = 0; i < command.length; i++) {
159
- const c = command[i]!;
159
+ const c = command[i] as string;
160
160
  const next = command[i + 1];
161
161
 
162
162
  if (quote) {
@@ -213,7 +213,7 @@ export function rewriteChain(command: string): string {
213
213
  const body = part.slice(leading.length);
214
214
  if (!body) return part;
215
215
 
216
- const firstWord = body.split(/\s+/)[0]!;
216
+ const firstWord = body.split(/\s+/)[0] ?? "";
217
217
  if (firstWord === "rtk") return part;
218
218
  if (!RTK_COMMANDS.has(firstWord)) return part;
219
219
 
@@ -1,17 +1,20 @@
1
- import { afterEach, describe, expect, it } from "bun:test";
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
- TOOL_ICONS,
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>${TOOL_ICONS.caveman}</accent> <accent>${TOOL_ICONS.rtk}</accent> <accent>${TOOL_ICONS.toon}</accent> <accent>${TOOL_ICONS.ponytail}</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>${TOOL_ICONS.caveman}</dim> <accent>${TOOL_ICONS.rtk}</accent> <accent>${TOOL_ICONS.toon}</accent> <accent>${TOOL_ICONS.ponytail}</accent> `,
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>${TOOL_ICONS.caveman}</dim> <dim>${TOOL_ICONS.rtk}</dim> <dim>${TOOL_ICONS.toon}</dim> <dim>${TOOL_ICONS.ponytail}</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>${TOOL_ICONS.caveman}</accent> <dim>${TOOL_ICONS.rtk}</dim> <accent>${TOOL_ICONS.toon}</accent> <dim>${TOOL_ICONS.ponytail}</dim> `,
49
+ `<accent>${CV}</accent> <dim>${RK}</dim> <accent>${TN}</accent> <dim>${PT}</dim> `,
47
50
  );
48
51
  });
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> `,
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
52
 
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
 
@@ -126,11 +74,12 @@ describe("OptimizerStatus", () => {
126
74
  const status = new OptimizerStatus();
127
75
  const ctx = fakeCtx();
128
76
  status.set("rtk", true, ctx as never);
129
- const last = ctx.calls.at(-1)!;
77
+ const last = ctx.calls.at(-1);
78
+ if (!last) throw new Error("no calls");
130
79
  expect(last.key).toBe(STATUS_KEY);
131
80
  // caveman + toon + ponytail still unset (dim), rtk accent.
132
81
  expect(last.text).toBe(
133
- `<dim>${TOOL_ICONS.caveman}</dim> <accent>${TOOL_ICONS.rtk}</accent> <dim>${TOOL_ICONS.toon}</dim> <dim>${TOOL_ICONS.ponytail}</dim> `,
82
+ `<dim>${CV}</dim> <accent>${RK}</accent> <dim>${TN}</dim> <dim>${PT}</dim> `,
134
83
  );
135
84
  });
136
85
 
@@ -139,9 +88,10 @@ describe("OptimizerStatus", () => {
139
88
  const ctx = fakeCtx();
140
89
  status.set("caveman", true, ctx as never);
141
90
  status.set("toon", true, ctx as never);
142
- const last = ctx.calls.at(-1)!;
91
+ const last = ctx.calls.at(-1);
92
+ if (!last) throw new Error("no calls");
143
93
  expect(last.text).toBe(
144
- `<accent>${TOOL_ICONS.caveman}</accent> <dim>${TOOL_ICONS.rtk}</dim> <accent>${TOOL_ICONS.toon}</accent> <dim>${TOOL_ICONS.ponytail}</dim> `,
94
+ `<accent>${CV}</accent> <dim>${RK}</dim> <accent>${TN}</accent> <dim>${PT}</dim> `,
145
95
  );
146
96
  });
147
97
 
@@ -150,29 +100,10 @@ describe("OptimizerStatus", () => {
150
100
  const ctx = fakeCtx();
151
101
  status.set("rtk", true, ctx as never);
152
102
  status.set("rtk", false, ctx as never);
153
- const last = ctx.calls.at(-1)!;
103
+ const last = ctx.calls.at(-1);
104
+ if (!last) throw new Error("no calls");
154
105
  expect(last.text).toBe(
155
- `<dim>${TOOL_ICONS.caveman}</dim> <dim>${TOOL_ICONS.rtk}</dim> <dim>${TOOL_ICONS.toon}</dim> <dim>${TOOL_ICONS.ponytail}</dim> `,
106
+ `<dim>${CV}</dim> <dim>${RK}</dim> <dim>${TN}</dim> <dim>${PT}</dim> `,
156
107
  );
157
108
  });
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
109
  });
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
- * 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.
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
- 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
- },
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
- /** 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";
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. `color` applies the theme color (e.g. theme.fg).
131
- * `mode` selects the glyph table (nerd / unicode / ascii); defaults to nerd.
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, icons[t]),
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
- private iconMode: IconMode;
155
-
156
- /** `mode` seeds the icon set (persisted choice or env default). */
157
- constructor(mode: IconMode = "nerd") {
158
- this.iconMode = mode;
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
- const text = renderStatus(
190
- this.states,
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
  }