@xynogen/pix-data 0.3.1 → 0.3.2
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 +2 -1
- package/package.json +1 -1
- package/src/index.ts +3 -1
- package/src/pix-command.ts +298 -0
- package/src/pix-config.ts +67 -3
package/README.md
CHANGED
|
@@ -154,8 +154,9 @@ pix-data hosts the **single shared config file** consumed by every `pix-*` packa
|
|
|
154
154
|
|
|
155
155
|
// Rendering options (pix-pretty)
|
|
156
156
|
"pretty": {
|
|
157
|
-
"
|
|
157
|
+
"syntaxTheme": "monokai", // syntax-highlight theme (overrides PRETTY_THEME)
|
|
158
158
|
"icons": "nerd", // icon mode: nerd | unicode | ascii (overrides PRETTY_ICONS)
|
|
159
|
+
"lsStyle": "grid", // ls output layout: "grid" (horizontal) | "tree" (vertical)
|
|
159
160
|
"maxPreviewLines": 50, // overrides PRETTY_MAX_PREVIEW_LINES
|
|
160
161
|
"diffColors": true // colored diff output
|
|
161
162
|
},
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { benchlm, modelgrep } from "./data.ts";
|
|
13
|
+
import { registerPixCommand } from "./pix-command.ts";
|
|
13
14
|
|
|
14
15
|
export type {
|
|
15
16
|
BenchmarkEntry,
|
|
@@ -33,7 +34,8 @@ export {
|
|
|
33
34
|
modelgrep,
|
|
34
35
|
} from "./data.ts";
|
|
35
36
|
|
|
36
|
-
export default function (
|
|
37
|
+
export default function (pi: ExtensionAPI): void {
|
|
37
38
|
void modelgrep.get();
|
|
38
39
|
void benchlm.get();
|
|
40
|
+
registerPixCommand(pi);
|
|
39
41
|
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pix-command.ts — the `/pix` command: unified settings overlay for pix.json.
|
|
3
|
+
*
|
|
4
|
+
* Opens an interactive overlay that surfaces every section of
|
|
5
|
+
* `~/.pi/agent/pix.json` as a browsable, editable settings panel. Each setting
|
|
6
|
+
* is a row with ←→ to cycle its value. Sections are separated by headers.
|
|
7
|
+
*
|
|
8
|
+
* Headless hosts get a notify summary instead.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { type PixConfig, pixConfig, savePixConfig } from "./pix-config.js";
|
|
13
|
+
|
|
14
|
+
// ── Setting descriptors ──────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
interface SettingRow {
|
|
17
|
+
/** Section header — only the first row per section renders a header. */
|
|
18
|
+
section: string;
|
|
19
|
+
/** Display label. */
|
|
20
|
+
label: string;
|
|
21
|
+
/** The pix.json path: top-level key. */
|
|
22
|
+
configSection: keyof PixConfig;
|
|
23
|
+
/** The field name within the section. */
|
|
24
|
+
configKey: string;
|
|
25
|
+
/** Allowed values to cycle through. */
|
|
26
|
+
values: readonly string[];
|
|
27
|
+
/** Read the current value from config. */
|
|
28
|
+
read: (cfg: PixConfig) => string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SETTINGS: SettingRow[] = [
|
|
32
|
+
// ── Pretty ──────────────────────────────────────────────────────────────
|
|
33
|
+
{
|
|
34
|
+
section: "Pretty",
|
|
35
|
+
label: "icons",
|
|
36
|
+
configSection: "pretty",
|
|
37
|
+
configKey: "icons",
|
|
38
|
+
values: ["nerd", "unicode", "ascii"],
|
|
39
|
+
read: (c) => c.pretty.icons,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
section: "Pretty",
|
|
43
|
+
label: "ls style",
|
|
44
|
+
configSection: "pretty",
|
|
45
|
+
configKey: "lsStyle",
|
|
46
|
+
values: ["grid", "tree"],
|
|
47
|
+
read: (c) => c.pretty.lsStyle,
|
|
48
|
+
},
|
|
49
|
+
// ── Collapse ─────────────────────────────────────────────────────────────
|
|
50
|
+
{
|
|
51
|
+
section: "Collapse",
|
|
52
|
+
label: "enabled",
|
|
53
|
+
configSection: "collapse",
|
|
54
|
+
configKey: "enabled",
|
|
55
|
+
values: ["true", "false"],
|
|
56
|
+
read: (c) => String(c.collapse.enabled),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
section: "Collapse",
|
|
60
|
+
label: "delay (sec)",
|
|
61
|
+
configSection: "collapse",
|
|
62
|
+
configKey: "delaySec",
|
|
63
|
+
values: ["5", "10", "15", "20", "30", "60"],
|
|
64
|
+
read: (c) => String(c.collapse.delaySec),
|
|
65
|
+
},
|
|
66
|
+
// ── Optimizer ─────────────────────────────────────────────────────────────
|
|
67
|
+
{
|
|
68
|
+
section: "Optimizer",
|
|
69
|
+
label: "caveman",
|
|
70
|
+
configSection: "optimizer",
|
|
71
|
+
configKey: "caveman",
|
|
72
|
+
values: ["off", "lite", "full", "ultra", "micro"],
|
|
73
|
+
read: (c) => c.optimizer.caveman,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
section: "Optimizer",
|
|
77
|
+
label: "rtk",
|
|
78
|
+
configSection: "optimizer",
|
|
79
|
+
configKey: "rtk",
|
|
80
|
+
values: ["off", "on"],
|
|
81
|
+
read: (c) => c.optimizer.rtk,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
section: "Optimizer",
|
|
85
|
+
label: "toon",
|
|
86
|
+
configSection: "optimizer",
|
|
87
|
+
configKey: "toon",
|
|
88
|
+
values: ["off", "on"],
|
|
89
|
+
read: (c) => c.optimizer.toon,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
section: "Optimizer",
|
|
93
|
+
label: "ponytail",
|
|
94
|
+
configSection: "optimizer",
|
|
95
|
+
configKey: "ponytail",
|
|
96
|
+
values: ["off", "lite", "full", "ultra"],
|
|
97
|
+
read: (c) => c.optimizer.ponytail,
|
|
98
|
+
},
|
|
99
|
+
// ── Gate ──────────────────────────────────────────────────────────────────
|
|
100
|
+
{
|
|
101
|
+
section: "Gate",
|
|
102
|
+
label: "disable defaults",
|
|
103
|
+
configSection: "gate",
|
|
104
|
+
configKey: "disableDefaults",
|
|
105
|
+
values: ["false", "true"],
|
|
106
|
+
read: (c) => String(c.gate.disableDefaults),
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/** Coerce string values back to proper JSON types for saving. */
|
|
113
|
+
function coerce(value: string): string | number | boolean {
|
|
114
|
+
if (value === "true") return true as unknown as string;
|
|
115
|
+
if (value === "false") return false as unknown as string;
|
|
116
|
+
const n = Number(value);
|
|
117
|
+
if (Number.isFinite(n) && String(n) === value) return n as unknown as string;
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Build a plain text summary for headless hosts. */
|
|
122
|
+
function buildSummary(): string {
|
|
123
|
+
const cfg = pixConfig();
|
|
124
|
+
const lines = ["pix settings (~/.pi/agent/pix.json)", ""];
|
|
125
|
+
let lastSection = "";
|
|
126
|
+
for (const row of SETTINGS) {
|
|
127
|
+
if (row.section !== lastSection) {
|
|
128
|
+
if (lastSection) lines.push("");
|
|
129
|
+
lines.push(`[${row.section}]`);
|
|
130
|
+
lastSection = row.section;
|
|
131
|
+
}
|
|
132
|
+
lines.push(` ${row.label}: ${row.read(cfg)}`);
|
|
133
|
+
}
|
|
134
|
+
return lines.join("\n");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Command registration ─────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
export function registerPixCommand(pi: ExtensionAPI): void {
|
|
140
|
+
pi.registerCommand("pix", {
|
|
141
|
+
description: "pix: open settings (edit ~/.pi/agent/pix.json)",
|
|
142
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
143
|
+
const ui = ctx.ui as unknown as {
|
|
144
|
+
theme: {
|
|
145
|
+
fg(c: string, t: string): string;
|
|
146
|
+
bg(c: string, t: string): string;
|
|
147
|
+
bold(t: string): string;
|
|
148
|
+
};
|
|
149
|
+
custom?: <T>(
|
|
150
|
+
f: unknown,
|
|
151
|
+
opts?: {
|
|
152
|
+
overlay?: boolean;
|
|
153
|
+
overlayOptions?: {
|
|
154
|
+
anchor?: string;
|
|
155
|
+
width?: number;
|
|
156
|
+
maxHeight?: string;
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
) => Promise<T>;
|
|
160
|
+
notify(m: string, t?: "info" | "warning" | "error"): void;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Headless fallback.
|
|
164
|
+
if (typeof ui.custom !== "function") {
|
|
165
|
+
ui.notify(buildSummary(), "info");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const boxW = 52;
|
|
170
|
+
|
|
171
|
+
await ui.custom<null>(
|
|
172
|
+
(
|
|
173
|
+
tui: { requestRender(): void },
|
|
174
|
+
theme: typeof ui.theme,
|
|
175
|
+
_kb: unknown,
|
|
176
|
+
done: (v: null) => void,
|
|
177
|
+
) => {
|
|
178
|
+
let selected = 0;
|
|
179
|
+
let cfg = pixConfig();
|
|
180
|
+
|
|
181
|
+
/** Cycle the selected setting's value. */
|
|
182
|
+
const cycle = (direction: -1 | 1) => {
|
|
183
|
+
const row = SETTINGS[selected];
|
|
184
|
+
if (!row) return;
|
|
185
|
+
const vals = row.values;
|
|
186
|
+
const cur = vals.indexOf(row.read(cfg));
|
|
187
|
+
const next = (cur + direction + vals.length) % vals.length;
|
|
188
|
+
const val = vals[next];
|
|
189
|
+
if (val === undefined) return;
|
|
190
|
+
|
|
191
|
+
// Persist to pix.json.
|
|
192
|
+
cfg = savePixConfig({
|
|
193
|
+
[row.configSection]: { [row.configKey]: coerce(val) },
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const move = (direction: -1 | 1) => {
|
|
198
|
+
selected = (selected + direction + SETTINGS.length) % SETTINGS.length;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
render: () => {
|
|
203
|
+
const labelW = Math.max(...SETTINGS.map((r) => r.label.length));
|
|
204
|
+
const lines: string[] = [theme.fg("accent", theme.bold(" pix settings")), ""];
|
|
205
|
+
|
|
206
|
+
let lastSection = "";
|
|
207
|
+
for (let i = 0; i < SETTINGS.length; i++) {
|
|
208
|
+
const row = SETTINGS[i]!;
|
|
209
|
+
// Section header.
|
|
210
|
+
if (row.section !== lastSection) {
|
|
211
|
+
if (lastSection) lines.push("");
|
|
212
|
+
lines.push(theme.fg("dim", ` ${row.section}`));
|
|
213
|
+
lastSection = row.section;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const sel = i === selected;
|
|
217
|
+
const cursor = sel ? theme.fg("accent", "→") : " ";
|
|
218
|
+
const label = theme.fg(sel ? "accent" : "text", row.label.padEnd(labelW));
|
|
219
|
+
const val = row.read(cfg);
|
|
220
|
+
const isDefault = val === row.values[0];
|
|
221
|
+
const value = theme.fg(isDefault ? "dim" : "success", val);
|
|
222
|
+
lines.push(`${cursor} ${label} ${value}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
lines.push("");
|
|
226
|
+
lines.push(theme.fg("dim", "←→ change · ↑↓ move · esc close"));
|
|
227
|
+
|
|
228
|
+
return frameLines({
|
|
229
|
+
width: boxW,
|
|
230
|
+
lines,
|
|
231
|
+
color: (s: string) => theme.fg("accent", s),
|
|
232
|
+
bg: (s: string) => theme.bg("customMessageBg", s),
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
invalidate: () => {},
|
|
236
|
+
handleInput: (data: string) => {
|
|
237
|
+
if (data === "k" || data === "\u001b[A") move(-1);
|
|
238
|
+
else if (data === "j" || data === "\u001b[B") move(1);
|
|
239
|
+
else if (data === "h" || data === "\u001b[D") cycle(-1);
|
|
240
|
+
else if (data === "l" || data === "\u001b[C" || data === " " || data === "\r")
|
|
241
|
+
cycle(1);
|
|
242
|
+
else if (data === "\u001b" || data === "q") {
|
|
243
|
+
done(null);
|
|
244
|
+
return;
|
|
245
|
+
} else return;
|
|
246
|
+
tui.requestRender();
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
overlay: true,
|
|
252
|
+
overlayOptions: { anchor: "center", width: boxW, maxHeight: "80%" },
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── Inline frameLines (avoid cross-package dep on pix-pretty) ────────────────
|
|
260
|
+
|
|
261
|
+
interface FrameOptions {
|
|
262
|
+
width: number;
|
|
263
|
+
lines: string[];
|
|
264
|
+
color: (s: string) => string;
|
|
265
|
+
bg?: (s: string) => string;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function visibleWidth(s: string): number {
|
|
269
|
+
// Strip ANSI escape sequences for width calculation.
|
|
270
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function frameLines(opts: FrameOptions): string[] {
|
|
274
|
+
const { width, lines, color } = opts;
|
|
275
|
+
const bg = opts.bg ?? ((s: string) => s);
|
|
276
|
+
const inner = Math.max(1, width - 4); // 2 border + 2 padding
|
|
277
|
+
const dashes = "─".repeat(width - 2);
|
|
278
|
+
|
|
279
|
+
const SENTINEL = "\x00";
|
|
280
|
+
const bgOpen = bg(SENTINEL).split(SENTINEL)[0] ?? "";
|
|
281
|
+
const reassert = (s: string): string =>
|
|
282
|
+
bgOpen
|
|
283
|
+
? s.replace(/\x1b\[([0-9;]*)m/g, (seq, p: string) =>
|
|
284
|
+
p === "0" || p.split(";").includes("49") ? `${seq}${bgOpen}` : seq,
|
|
285
|
+
)
|
|
286
|
+
: s;
|
|
287
|
+
|
|
288
|
+
const row = (content: string): string => {
|
|
289
|
+
const pad = inner - visibleWidth(content);
|
|
290
|
+
const padded = pad > 0 ? content + " ".repeat(pad) : content.slice(0, inner);
|
|
291
|
+
return bg(`${color("│")} ${reassert(padded)} ${color("│")}`);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const out: string[] = [bg(color(`╭${dashes}╮`))];
|
|
295
|
+
for (const line of lines) out.push(row(line));
|
|
296
|
+
out.push(bg(color(`╰${dashes}╯`)));
|
|
297
|
+
return out;
|
|
298
|
+
}
|
package/src/pix-config.ts
CHANGED
|
@@ -41,9 +41,13 @@ export interface DiffColors {
|
|
|
41
41
|
fgDel: string;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** How `ls` output is rendered: `"grid"` (horizontal columns) or `"tree"` (vertical tree view). */
|
|
45
|
+
export type LsStyle = "grid" | "tree";
|
|
46
|
+
|
|
44
47
|
export interface PrettyConfig {
|
|
45
|
-
theme: string;
|
|
46
48
|
icons: string;
|
|
49
|
+
/** `"grid"` = horizontal columns (default), `"tree"` = vertical tree view. */
|
|
50
|
+
lsStyle: LsStyle;
|
|
47
51
|
maxPreviewLines: number;
|
|
48
52
|
maxRenderLines: number;
|
|
49
53
|
maxHighlightChars: number;
|
|
@@ -100,8 +104,8 @@ const DEFAULT_COLLAPSE: CollapseConfig = {
|
|
|
100
104
|
};
|
|
101
105
|
|
|
102
106
|
const DEFAULT_PRETTY: PrettyConfig = {
|
|
103
|
-
theme: "github-dark",
|
|
104
107
|
icons: "nerd",
|
|
108
|
+
lsStyle: "grid",
|
|
105
109
|
maxPreviewLines: 80,
|
|
106
110
|
maxRenderLines: 150,
|
|
107
111
|
maxHighlightChars: 80_000,
|
|
@@ -220,11 +224,16 @@ function mergeDiff(raw: unknown): DiffColors {
|
|
|
220
224
|
};
|
|
221
225
|
}
|
|
222
226
|
|
|
227
|
+
function lsStyle(v: unknown): LsStyle {
|
|
228
|
+
if (v === "grid" || v === "tree") return v;
|
|
229
|
+
return DEFAULT_PRETTY.lsStyle;
|
|
230
|
+
}
|
|
231
|
+
|
|
223
232
|
function mergePretty(raw: unknown): PrettyConfig {
|
|
224
233
|
if (!isObj(raw)) return { ...DEFAULT_PRETTY };
|
|
225
234
|
return {
|
|
226
|
-
theme: str(raw.theme, DEFAULT_PRETTY.theme),
|
|
227
235
|
icons: str(raw.icons, DEFAULT_PRETTY.icons),
|
|
236
|
+
lsStyle: lsStyle(raw.lsStyle),
|
|
228
237
|
maxPreviewLines: num(raw.maxPreviewLines, DEFAULT_PRETTY.maxPreviewLines),
|
|
229
238
|
maxRenderLines: num(raw.maxRenderLines, DEFAULT_PRETTY.maxRenderLines),
|
|
230
239
|
maxHighlightChars: num(raw.maxHighlightChars, DEFAULT_PRETTY.maxHighlightChars),
|
|
@@ -269,6 +278,20 @@ function buildConfig(raw: Record<string, unknown>): PixConfig {
|
|
|
269
278
|
};
|
|
270
279
|
}
|
|
271
280
|
|
|
281
|
+
// ── Change listeners ─────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
type ConfigListener = (cfg: PixConfig) => void;
|
|
284
|
+
const configListeners = new Set<ConfigListener>();
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Subscribe to config changes (fired by `savePixConfig`). Returns unsubscribe.
|
|
288
|
+
* Listeners receive the freshly-reloaded PixConfig.
|
|
289
|
+
*/
|
|
290
|
+
export function onPixConfigChange(cb: ConfigListener): () => void {
|
|
291
|
+
configListeners.add(cb);
|
|
292
|
+
return () => configListeners.delete(cb);
|
|
293
|
+
}
|
|
294
|
+
|
|
272
295
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
273
296
|
|
|
274
297
|
/** Get the resolved pix config. Loads from disk on first call, cached after. */
|
|
@@ -297,3 +320,44 @@ export function shouldCollapse(toolName: string): boolean {
|
|
|
297
320
|
export function collapseDelayMs(): number {
|
|
298
321
|
return pixConfig().collapse.delaySec * 1000;
|
|
299
322
|
}
|
|
323
|
+
|
|
324
|
+
/** Get the ls rendering style: `"grid"` (horizontal) or `"tree"` (vertical). */
|
|
325
|
+
export function getLsStyle(): LsStyle {
|
|
326
|
+
return pixConfig().pretty.lsStyle;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Merge a partial update into pix.json and reload the in-process cache.
|
|
331
|
+
* Only the keys present in `patch` are overwritten; the rest of the file is
|
|
332
|
+
* preserved. Nested objects (e.g. `pretty`, `collapse`) are shallow-merged
|
|
333
|
+
* one level deep so callers can update a single field without wiping siblings.
|
|
334
|
+
*/
|
|
335
|
+
export function savePixConfig(patch: Record<string, unknown>): PixConfig {
|
|
336
|
+
const p = configPath();
|
|
337
|
+
if (!p) return pixConfig();
|
|
338
|
+
try {
|
|
339
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
340
|
+
let existing: Record<string, unknown> = {};
|
|
341
|
+
if (existsSync(p)) {
|
|
342
|
+
try {
|
|
343
|
+
existing = JSON.parse(readFileSync(p, "utf-8")) as Record<string, unknown>;
|
|
344
|
+
} catch {
|
|
345
|
+
existing = {};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Shallow-merge each top-level section so partial updates don't wipe siblings.
|
|
349
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
350
|
+
if (isObj(value) && isObj(existing[key])) {
|
|
351
|
+
existing[key] = { ...(existing[key] as Record<string, unknown>), ...value };
|
|
352
|
+
} else {
|
|
353
|
+
existing[key] = value;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
writeFileSync(p, `${JSON.stringify(existing, null, 2)}\n`);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
console.warn("pix-config: save failed:", err);
|
|
359
|
+
}
|
|
360
|
+
const cfg = reloadPixConfig();
|
|
361
|
+
for (const cb of configListeners) cb(cfg);
|
|
362
|
+
return cfg;
|
|
363
|
+
}
|