@xynogen/pix-optimizer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/rtk.ts ADDED
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Simple RTK Integration
3
+ *
4
+ * 1. Injects RTK system prompt (tells model to prefix commands with rtk)
5
+ * 2. Rewrites bash commands to add rtk prefix when model forgets
6
+ * 3. Falls back gracefully if rtk binary is missing
7
+ */
8
+
9
+ import type {
10
+ ExtensionAPI,
11
+ ExtensionCommandContext,
12
+ ExtensionContext,
13
+ } from "@earendil-works/pi-coding-agent";
14
+ import type { OptimizerHandle, OptimizerStatus } from "./status.ts";
15
+
16
+ /**
17
+ * Minimal structural shape of the SDK `tool_call` event we care about.
18
+ * Mirrors `BashToolCallEvent` from the SDK without importing it, so the
19
+ * rewrite logic stays unit-testable with plain objects.
20
+ */
21
+ export interface BashCallEvent {
22
+ toolName: string;
23
+ input: { command?: unknown; [k: string]: unknown };
24
+ }
25
+
26
+ /**
27
+ * Pure decision + mutation step for the `tool_call` hook.
28
+ *
29
+ * Given a tool-call event and whether RTK is available, mutate `event.input`
30
+ * in place (the SDK's only supported way to patch tool args) when the command
31
+ * is a rewritable bash command. Returns true if the command was rewritten.
32
+ *
33
+ * Extracted from the hook closure so the integration is directly testable
34
+ * without a live ExtensionAPI.
35
+ */
36
+ export function applyRtkRewrite(
37
+ event: BashCallEvent,
38
+ opts: { enabled: boolean; rtkAvailable: boolean },
39
+ ): boolean {
40
+ if (!opts.enabled) return false;
41
+ if (!opts.rtkAvailable) return false;
42
+ if (event.toolName !== "bash") return false;
43
+
44
+ const command = event.input?.command;
45
+ if (typeof command !== "string" || !command) return false;
46
+
47
+ const rewritten = rewriteChain(command);
48
+ if (rewritten === command) return false;
49
+
50
+ event.input.command = rewritten;
51
+ return true;
52
+ }
53
+
54
+ const RTK_SYSTEM_PROMPT = `# RTK — token-optimized command wrapper
55
+
56
+ Prefix shell commands with \`rtk\` (e.g. \`rtk git status\`). RTK compacts output for git, gh, cargo, npm/pnpm/yarn/bun, tsc, lint, vitest/jest/playwright, docker, kubectl, ls, grep, find, prisma — and passes anything else through unchanged, so it's always safe.
57
+
58
+ Prefix EVERY segment in a chain, not just the first:
59
+ \`rtk git add . && rtk git commit -m "msg" && rtk git push\`
60
+
61
+ RTK also has filtering subcommands the auto-rewriter won't add — reach for these yourself when useful: \`rtk err <cmd>\` (errors only), \`rtk summary <cmd>\`, \`rtk log <file>\` (dedup), \`rtk json <file>\` (structure), \`rtk test <cmd>\` (failures only), \`rtk gain\` (savings stats).`;
62
+
63
+ // Commands that should be prefixed with rtk
64
+ const RTK_COMMANDS = new Set([
65
+ "git", "gh", "ls", "tree", "find", "grep", "cat", "head", "tail",
66
+ "tsc", "lint", "eslint", "prettier", "next", "cargo", "rustc",
67
+ "vitest", "playwright", "jest", "test",
68
+ "pnpm", "npm", "npx", "yarn", "bun",
69
+ "docker", "kubectl", "aws", "psql",
70
+ "curl", "wget", "wc",
71
+ "prisma", "dotnet"
72
+ ]);
73
+
74
+ interface RtkStatus {
75
+ available: boolean;
76
+ checkedAt: number;
77
+ path?: string;
78
+ }
79
+
80
+ /**
81
+ * Split a command line into segments at top-level shell operators
82
+ * (&&, ||, ;, |), keeping the operators as their own tokens. Operators
83
+ * inside single/double quotes are ignored.
84
+ *
85
+ * Returns null if the parser hits something it can't safely reason about
86
+ * (unbalanced quotes), so the caller can skip rewriting.
87
+ */
88
+ export function splitChain(command: string): string[] | null {
89
+ const out: string[] = [];
90
+ let buf = "";
91
+ let quote: "'" | '"' | null = null;
92
+
93
+ for (let i = 0; i < command.length; i++) {
94
+ const c = command[i]!;
95
+ const next = command[i + 1];
96
+
97
+ if (quote) {
98
+ buf += c;
99
+ if (c === quote) quote = null;
100
+ continue;
101
+ }
102
+
103
+ if (c === "'" || c === '"') {
104
+ quote = c;
105
+ buf += c;
106
+ continue;
107
+ }
108
+
109
+ // two-char operators
110
+ if ((c === "&" && next === "&") || (c === "|" && next === "|")) {
111
+ out.push(buf, c + c);
112
+ buf = "";
113
+ i++;
114
+ continue;
115
+ }
116
+
117
+ // single-char operators
118
+ if (c === ";" || c === "|") {
119
+ out.push(buf, c);
120
+ buf = "";
121
+ continue;
122
+ }
123
+
124
+ buf += c;
125
+ }
126
+
127
+ if (quote) return null; // unbalanced quote — bail out
128
+ out.push(buf);
129
+ return out;
130
+ }
131
+
132
+ const CHAIN_OPERATORS = new Set(["&&", "||", ";", "|"]);
133
+
134
+ /**
135
+ * Prefix each command segment with `rtk` when its first word is a known
136
+ * RTK command and it is not already prefixed. Operators are preserved.
137
+ * Returns the rewritten command, or the original if nothing changed.
138
+ */
139
+ export function rewriteChain(command: string): string {
140
+ const parts = splitChain(command);
141
+ if (!parts) return command; // unparseable — leave untouched
142
+
143
+ let changed = false;
144
+ const rewritten = parts.map((part) => {
145
+ if (CHAIN_OPERATORS.has(part.trim())) return part;
146
+
147
+ const leading = part.match(/^\s*/)?.[0] ?? "";
148
+ const body = part.slice(leading.length);
149
+ if (!body) return part;
150
+
151
+ const firstWord = body.split(/\s+/)[0]!;
152
+ if (firstWord === "rtk") return part;
153
+ if (!RTK_COMMANDS.has(firstWord)) return part;
154
+
155
+ changed = true;
156
+ return `${leading}rtk ${body}`;
157
+ });
158
+
159
+ return changed ? rewritten.join("") : command;
160
+ }
161
+
162
+ export function rtk(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle {
163
+ let rtkStatus: RtkStatus | null = null;
164
+ let warnedMissing = false;
165
+ let enabled = true;
166
+
167
+ // Report into the shared optimizer indicator. RTK counts as "on" only when
168
+ // enabled AND the binary is actually available.
169
+ function syncStatus(ctx: Pick<ExtensionContext, "ui">) {
170
+ status.set("rtk", enabled && rtkStatus?.available === true, ctx);
171
+ }
172
+
173
+ // Check if rtk binary is available
174
+ const checkRtkAvailability = async (): Promise<RtkStatus> => {
175
+ // Cache for 60 seconds
176
+ if (rtkStatus && Date.now() - rtkStatus.checkedAt < 60000) {
177
+ return rtkStatus;
178
+ }
179
+
180
+ try {
181
+ const result = await pi.exec("which", ["rtk"], { timeout: 1000 });
182
+ if (result.code === 0 && result.stdout?.trim()) {
183
+ rtkStatus = {
184
+ available: true,
185
+ checkedAt: Date.now(),
186
+ path: result.stdout.trim()
187
+ };
188
+ warnedMissing = false;
189
+ return rtkStatus;
190
+ }
191
+ } catch (error) {
192
+ // which command failed
193
+ }
194
+
195
+ rtkStatus = {
196
+ available: false,
197
+ checkedAt: Date.now()
198
+ };
199
+ return rtkStatus;
200
+ };
201
+
202
+ // Inject RTK system prompt
203
+ pi.on("before_agent_start", async (event) => {
204
+ if (!enabled) return undefined;
205
+ const existing = event.systemPrompt ?? "";
206
+ return { systemPrompt: RTK_SYSTEM_PROMPT + "\n\n" + existing };
207
+ });
208
+
209
+ // Keep the status indicator in sync across the agent lifecycle. Probe
210
+ // availability on session start so the icon reflects reality immediately.
211
+ pi.on("session_start", async (_event, ctx) => {
212
+ await checkRtkAvailability();
213
+ syncStatus(ctx);
214
+ });
215
+ pi.on("agent_start", async (_event, ctx) => { syncStatus(ctx); });
216
+ pi.on("agent_end", async (_event, ctx) => { syncStatus(ctx); });
217
+
218
+ // -- Subcommand handler (dispatched by the merged /opt router) --
219
+
220
+ async function run(args: string, ctx: ExtensionCommandContext): Promise<void> {
221
+ const arg = args.trim().toLowerCase();
222
+ if (arg === "on") enabled = true;
223
+ else if (arg === "off") enabled = false;
224
+ else enabled = !enabled; // bare toggles
225
+
226
+ await checkRtkAvailability();
227
+ syncStatus(ctx);
228
+ ctx.ui.notify(`RTK rewriting ${enabled ? "on" : "off"}.`, "info");
229
+ }
230
+
231
+ function complete(prefix: string) {
232
+ const items = [
233
+ { value: "on", label: "on", description: "Force RTK rewriting on" },
234
+ { value: "off", label: "off", description: "Force RTK rewriting off" },
235
+ ];
236
+ const n = prefix.trim().toLowerCase();
237
+ const filtered = items.filter((i) => i.value.startsWith(n));
238
+ return filtered.length > 0 ? filtered : null;
239
+ }
240
+
241
+ // Rewrite bash commands to add rtk prefix.
242
+ //
243
+ // The SDK fires a single `tool_call` event for every tool. The bash variant
244
+ // carries `event.toolName === "bash"` and a mutable `event.input` of shape
245
+ // `{ command: string; timeout?: number }`. Arguments are patched by mutating
246
+ // `event.input` IN PLACE — returning `{ toolInput: ... }` does nothing.
247
+ pi.on("tool_call", async (event, ctx) => {
248
+ if (!enabled) {
249
+ return undefined;
250
+ }
251
+
252
+ if (event.toolName !== "bash") {
253
+ return undefined;
254
+ }
255
+
256
+ const probe = await checkRtkAvailability();
257
+
258
+ if (!probe.available) {
259
+ if (!warnedMissing) {
260
+ ctx.ui.notify(
261
+ "RTK binary not found. Install: cargo install rtk-ai",
262
+ "warning"
263
+ );
264
+ warnedMissing = true;
265
+ }
266
+ return undefined; // Don't rewrite if rtk not available
267
+ }
268
+
269
+ // First confirmed-available probe may have flipped state — refresh icon.
270
+ syncStatus(ctx);
271
+
272
+ // Rewrite every segment in the command chain that uses a known RTK
273
+ // command (e.g. `git add . && git push` -> `rtk git add . && rtk git push`).
274
+ // Mutates `event.input.command` in place — the SDK's supported patch path.
275
+ applyRtkRewrite(event, { enabled, rtkAvailable: probe.available });
276
+ return undefined;
277
+ });
278
+
279
+ return {
280
+ name: "rtk",
281
+ help: "rtk [on|off] — prefix shell commands with rtk (token-optimized)",
282
+ run,
283
+ complete,
284
+ };
285
+ }
@@ -0,0 +1,101 @@
1
+ ---
2
+ name: toon-json
3
+ description: Manipulate information-dense JSON efficiently with jq + TOON. Use when fetching/reading large or repetitive JSON (LLM schemas, OpenAPI specs, API responses, datasets, config dumps) into context. Query/reshape with jq, compress to TOON to cut tokens, decode back to JSON only when a strict contract needs it.
4
+ ---
5
+
6
+ # TOON + jq: Dense JSON Workflow
7
+
8
+ ## Goal
9
+ Carry only the JSON slice you need, in the cheapest encoding. Query with `jq`,
10
+ compress with `toon`, and round-trip back to JSON **only** when a contract
11
+ requires strict JSON.
12
+
13
+ TOON (Token-Oriented Object Notation, https://github.com/toon-format/spec) is a
14
+ line-oriented encoding of the JSON data model. Uniform arrays of objects declare
15
+ their keys once and stream bare rows, so token cost drops sharply on tabular and
16
+ dense data.
17
+
18
+ ## The pipeline
19
+
20
+ ```bash
21
+ # Fetch → reshape → compress (most common)
22
+ curl -s https://api.example.com/models | jq '.data' | toon
23
+
24
+ # Local file, show token savings
25
+ cat openapi.json | jq '.paths' | toon --stats
26
+
27
+ # Just compress, no query
28
+ cat data.json | toon
29
+
30
+ # Convert TOON back to JSON (strict contract / downstream parser)
31
+ echo "$TOON_BLOB" | toon -d
32
+ cat data.toon | toon --decode
33
+ ```
34
+
35
+ `toon` auto-detects direction from input. Force it with `-e` (encode JSON→TOON)
36
+ or `-d` (decode TOON→JSON).
37
+
38
+ ### Useful flags
39
+ - `--stats` — print token/byte statistics for the conversion
40
+ - `--delimiter=,|\t|"|"` — array delimiter (comma default; tab/pipe can tokenize better)
41
+ - `--keyFolding=safe` — collapse single-key nesting chains
42
+ - `--no-strict` — lenient decode
43
+
44
+ ## Decide before you compress
45
+
46
+ TOON is not always smaller. Pick based on shape:
47
+
48
+ | Shape | Action | Why |
49
+ |---|---|---|
50
+ | Uniform array of objects (same keys, primitive values) | **TOON** | Sweet spot — keys declared once, rows streamed; savings scale with rows × fields |
51
+ | Flat object / primitive array | **TOON** | Drops quotes and braces |
52
+ | Shallow nesting | **TOON** | Indentation cheaper than braces at low depth |
53
+ | Deeply nested / non-uniform | **JSON** | Indentation cost grows; compact JSON can win |
54
+ | Array of arrays | **JSON** | TOON's one structurally-worse case (explicit list markers + inner headers) |
55
+ | API contract / payload to send or store | **JSON** | Must stay valid JSON for the consumer |
56
+
57
+ Rule of thumb: **TOON for reading dense data into context. JSON for contracts.**
58
+
59
+ ## When NOT to use TOON
60
+ - Anything sent to or stored by an API that expects JSON.
61
+ - Data a downstream tool/parser must consume as strict JSON.
62
+ - Deeply nested config trees or highly irregular structures.
63
+ - Tiny payloads where the conversion overhead isn't worth it.
64
+
65
+ In those cases, still use `jq` to slice down to what you need — just skip the
66
+ `| toon` step.
67
+
68
+ ## Worked examples
69
+
70
+ LLM model list (uniform array → great TOON candidate):
71
+
72
+ ```bash
73
+ curl -s https://api.example.com/v1/models | jq '.data | map({id, owned_by, context})' | toon
74
+ ```
75
+ ```
76
+ [3]{id,owned_by,context}:
77
+ gpt-x,acme,128000
78
+ gpt-y,acme,200000
79
+ gpt-z,acme,1000000
80
+ ```
81
+
82
+ OpenAPI paths summary (reshape first, then compress):
83
+
84
+ ```bash
85
+ cat openapi.json \
86
+ | jq '[.paths | to_entries[] | {path: .key, methods: (.value | keys)}]' \
87
+ | toon --stats
88
+ ```
89
+
90
+ Round-trip back to JSON for an API call:
91
+
92
+ ```bash
93
+ RESHAPED=$(cat payload.toon | toon -d)
94
+ curl -s -X POST https://api.example.com/ingest -d "$RESHAPED"
95
+ ```
96
+
97
+ ## Checklist
98
+ 1. **Query** — narrow with `jq` to the exact slice needed.
99
+ 2. **Decide** — uniform/tabular/shallow → TOON; nested/array-of-arrays/contract → JSON.
100
+ 3. **Compress** — `| toon` (add `--stats` to confirm savings).
101
+ 4. **Round-trip** — `toon -d` only when strict JSON is required downstream.
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import {
3
+ OptimizerStatus,
4
+ STATUS_KEY,
5
+ TOOL_ICONS,
6
+ renderStatus,
7
+ } from "./status.ts";
8
+
9
+ /** Tagging colorizer: <accent>X</accent> / <dim>X</dim> for assertions. */
10
+ const tag = (c: string, t: string) => `<${c}>${t}</${c}>`;
11
+
12
+ describe("renderStatus", () => {
13
+ it("shows ALL icons in order, accent when enabled", () => {
14
+ expect(renderStatus({ caveman: true, rtk: true, toon: true }, tag)).toBe(
15
+ `<accent>${TOOL_ICONS.caveman}</accent> <accent>${TOOL_ICONS.rtk}</accent> <accent>${TOOL_ICONS.toon}</accent> `,
16
+ );
17
+ });
18
+
19
+ it("dims disabled tools but still shows them", () => {
20
+ expect(renderStatus({ caveman: false, rtk: true, toon: true }, tag)).toBe(
21
+ `<dim>${TOOL_ICONS.caveman}</dim> <accent>${TOOL_ICONS.rtk}</accent> <accent>${TOOL_ICONS.toon}</accent> `,
22
+ );
23
+ });
24
+
25
+ it("all dim when nothing enabled (cell never empty)", () => {
26
+ expect(renderStatus({}, tag)).toBe(
27
+ `<dim>${TOOL_ICONS.caveman}</dim> <dim>${TOOL_ICONS.rtk}</dim> <dim>${TOOL_ICONS.toon}</dim> `,
28
+ );
29
+ });
30
+
31
+ it("preserves fixed order regardless of insertion order", () => {
32
+ expect(renderStatus({ toon: true, caveman: true }, tag)).toBe(
33
+ `<accent>${TOOL_ICONS.caveman}</accent> <dim>${TOOL_ICONS.rtk}</dim> <accent>${TOOL_ICONS.toon}</accent> `,
34
+ );
35
+ });
36
+ });
37
+
38
+ describe("OptimizerStatus", () => {
39
+ /** Minimal ui stub capturing setStatus calls. */
40
+ function fakeCtx() {
41
+ const calls: { key: string; text: string }[] = [];
42
+ return {
43
+ calls,
44
+ ui: {
45
+ setStatus: (key: string, text: string | undefined) =>
46
+ calls.push({ key, text: text ?? "" }),
47
+ theme: { fg: (c: string, t: string) => `<${c}>${t}</${c}>` },
48
+ },
49
+ } as const;
50
+ }
51
+
52
+ it("paints the shared key with per-icon accent/dim", () => {
53
+ const status = new OptimizerStatus();
54
+ const ctx = fakeCtx();
55
+ status.set("rtk", true, ctx as never);
56
+ const last = ctx.calls.at(-1)!;
57
+ expect(last.key).toBe(STATUS_KEY);
58
+ // caveman + toon still unset (dim), rtk accent.
59
+ expect(last.text).toBe(
60
+ `<dim>${TOOL_ICONS.caveman}</dim> <accent>${TOOL_ICONS.rtk}</accent> <dim>${TOOL_ICONS.toon}</dim> `,
61
+ );
62
+ });
63
+
64
+ it("accumulates state across tools", () => {
65
+ const status = new OptimizerStatus();
66
+ const ctx = fakeCtx();
67
+ status.set("caveman", true, ctx as never);
68
+ status.set("toon", true, ctx as never);
69
+ const last = ctx.calls.at(-1)!;
70
+ expect(last.text).toBe(
71
+ `<accent>${TOOL_ICONS.caveman}</accent> <dim>${TOOL_ICONS.rtk}</dim> <accent>${TOOL_ICONS.toon}</accent> `,
72
+ );
73
+ });
74
+
75
+ it("dims an icon when its tool toggles off (cell stays populated)", () => {
76
+ const status = new OptimizerStatus();
77
+ const ctx = fakeCtx();
78
+ status.set("rtk", true, ctx as never);
79
+ status.set("rtk", false, ctx as never);
80
+ const last = ctx.calls.at(-1)!;
81
+ expect(last.text).toBe(
82
+ `<dim>${TOOL_ICONS.caveman}</dim> <dim>${TOOL_ICONS.rtk}</dim> <dim>${TOOL_ICONS.toon}</dim> `,
83
+ );
84
+ });
85
+ });
package/src/status.ts ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * status.ts — shared status-bar indicator for the optimizer suite.
3
+ *
4
+ * caveman / rtk / json each toggle independently, but they're all the same
5
+ * class of thing (token-optimization tools), so they share ONE status cell
6
+ * instead of three. Each tool reports its on/off state into a single registry;
7
+ * the cell renders only the icons whose tool is currently enabled, in a fixed
8
+ * order, accent-colored. When everything is off the cell is empty.
9
+ *
10
+ * all on: ⛏ ⚔ ✂
11
+ * caveman off: ⚔ ✂
12
+ * all off: (empty)
13
+ */
14
+
15
+ import type {
16
+ ExtensionCommandContext,
17
+ ExtensionContext,
18
+ ThemeColor,
19
+ } from "@earendil-works/pi-coding-agent";
20
+
21
+ /** Argument-completion item shape used by the merged /opt command. */
22
+ export interface CompletionItem {
23
+ value: string;
24
+ label: string;
25
+ description: string;
26
+ }
27
+
28
+ /**
29
+ * Each optimizer tool (caveman/rtk/json) exposes a handle so the single `/opt`
30
+ * router can dispatch subcommands to it without knowing its internals.
31
+ */
32
+ export interface OptimizerHandle {
33
+ /** Subcommand name, e.g. "caveman" / "rtk" / "toon". */
34
+ name: OptimizerTool;
35
+ /** One-line help shown by `/opt` with no args. */
36
+ help: string;
37
+ /** Handle `/opt <name> <args>`. `args` is everything after the name. */
38
+ run(args: string, ctx: ExtensionCommandContext): Promise<void> | void;
39
+ /** Completions for the second token (`/opt <name> <prefix>`). */
40
+ complete(prefix: string): CompletionItem[] | null;
41
+ }
42
+
43
+ /** Stable status-bar key shared by every optimizer tool. */
44
+ export const STATUS_KEY = "pix-optimizer";
45
+
46
+ /** Tools that participate in the shared indicator, in render order. */
47
+ export type OptimizerTool = "caveman" | "rtk" | "toon";
48
+
49
+ export const TOOL_ICONS: Record<OptimizerTool, string> = {
50
+ caveman: "⛏",
51
+ rtk: "⚔",
52
+ toon: "✂",
53
+ };
54
+
55
+ /** Fixed left-to-right order of icons in the cell. */
56
+ export const TOOL_ORDER: readonly OptimizerTool[] = ["caveman", "rtk", "toon"];
57
+
58
+ /** Theme color for enabled icons. */
59
+ export const ENABLED_COLOR: ThemeColor = "accent";
60
+ /** Theme color for disabled icons. */
61
+ export const DISABLED_COLOR: ThemeColor = "dim";
62
+
63
+ /** Colorizer: maps a (theme color, text) pair to a rendered string. */
64
+ export type Colorize = (color: ThemeColor, text: string) => string;
65
+
66
+ /**
67
+ * Build the colored status string for a set of tool states. ALL tool icons are
68
+ * always shown, in TOOL_ORDER; each is accent-colored when its tool is enabled
69
+ * and dim when disabled. `color` applies the theme color (e.g. theme.fg).
70
+ *
71
+ * Pure + exported for tests (pass a tagging colorizer to assert per-icon color).
72
+ * A trailing space separates the cell from the next status segment.
73
+ */
74
+ export function renderStatus(
75
+ states: Partial<Record<OptimizerTool, boolean>>,
76
+ color: Colorize,
77
+ ): string {
78
+ return `${TOOL_ORDER.map((t) =>
79
+ color(states[t] === true ? ENABLED_COLOR : DISABLED_COLOR, TOOL_ICONS[t]),
80
+ ).join(" ")} `;
81
+ }
82
+
83
+ /**
84
+ * Shared registry: each tool calls `set(tool, enabled)` whenever its state
85
+ * changes, then the combined cell is re-rendered. A single registry instance
86
+ * is created per extension load and passed to caveman/rtk/json.
87
+ */
88
+ export class OptimizerStatus {
89
+ private states: Partial<Record<OptimizerTool, boolean>> = {};
90
+
91
+ /** Update one tool's enabled state and repaint the shared cell. */
92
+ set(tool: OptimizerTool, enabled: boolean, ctx: Pick<ExtensionContext, "ui">): void {
93
+ this.states[tool] = enabled;
94
+ this.paint(ctx);
95
+ }
96
+
97
+ /** Repaint the shared cell from current states. */
98
+ paint(ctx: Pick<ExtensionContext, "ui">): void {
99
+ const text = renderStatus(this.states, (c, t) => ctx.ui.theme.fg(c, t));
100
+ ctx.ui.setStatus(STATUS_KEY, text);
101
+ }
102
+ }