@xynogen/pix-runtime 0.15.2 → 0.16.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/package.json +1 -1
- package/src/icon-catalog.ts +10 -6
- package/src/index.ts +1 -0
- package/src/once.ts +25 -1
- package/src/os.ts +14 -0
- package/src/pix-command.ts +174 -13
package/package.json
CHANGED
package/src/icon-catalog.ts
CHANGED
|
@@ -135,11 +135,15 @@ function envMode(): IconMode {
|
|
|
135
135
|
return "nerd";
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
const shared = globalThis as {
|
|
139
|
+
__pixIconState?: { mode: IconMode; listeners: Set<(mode: IconMode) => void> };
|
|
140
|
+
};
|
|
141
|
+
shared.__pixIconState ??= { mode: envMode(), listeners: new Set() };
|
|
142
|
+
const state = shared.__pixIconState;
|
|
139
143
|
|
|
140
144
|
/** Current global icon mode. */
|
|
141
145
|
export function getIconMode(): IconMode {
|
|
142
|
-
return
|
|
146
|
+
return state.mode;
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
/**
|
|
@@ -149,7 +153,7 @@ export function getIconMode(): IconMode {
|
|
|
149
153
|
* here; setIconMode fires every callback on an actual change.
|
|
150
154
|
*/
|
|
151
155
|
type ModeListener = (mode: IconMode) => void;
|
|
152
|
-
const listeners =
|
|
156
|
+
const listeners = state.listeners;
|
|
153
157
|
|
|
154
158
|
/** Subscribe to global icon-mode changes. Returns an unsubscribe fn. */
|
|
155
159
|
export function onIconModeChange(cb: ModeListener): () => void {
|
|
@@ -163,8 +167,8 @@ export function onIconModeChange(cb: ModeListener): () => void {
|
|
|
163
167
|
* subscribers only on an actual change (no-op re-sets are ignored).
|
|
164
168
|
*/
|
|
165
169
|
export function setIconMode(mode: IconMode): void {
|
|
166
|
-
if (!ICON_MODES.includes(mode) || mode ===
|
|
167
|
-
|
|
170
|
+
if (!ICON_MODES.includes(mode) || mode === state.mode) return;
|
|
171
|
+
state.mode = mode;
|
|
168
172
|
for (const cb of listeners) cb(mode);
|
|
169
173
|
}
|
|
170
174
|
|
|
@@ -174,7 +178,7 @@ export function setIconMode(mode: IconMode): void {
|
|
|
174
178
|
*/
|
|
175
179
|
export function icon(key: IconKey): string {
|
|
176
180
|
const entry = CATALOG[key];
|
|
177
|
-
return entry ? entry[
|
|
181
|
+
return entry ? entry[state.mode] : "";
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
/** Resolve a key for an explicit mode (used by /pix previews + tests). */
|
package/src/index.ts
CHANGED
package/src/once.ts
CHANGED
|
@@ -10,7 +10,25 @@
|
|
|
10
10
|
* skip, new instance => run. The registry lives on globalThis because jiti
|
|
11
11
|
* (`moduleCache: false`) re-evaluates this module on every load pass.
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
type PackageHost = {
|
|
14
|
+
events?: {
|
|
15
|
+
emit(channel: string, data: unknown): void;
|
|
16
|
+
on(channel: string, handler: (data: unknown) => void): unknown;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const PACKAGE_QUERY = "pix:package-loaded";
|
|
21
|
+
|
|
22
|
+
/** Check after extension loading (for example, session_start), not factory order. */
|
|
23
|
+
export function isPackageLoaded(pi: PackageHost, key: string): boolean {
|
|
24
|
+
const loaded = (globalThis as { __pixOnce?: WeakMap<object, Set<string>> }).__pixOnce;
|
|
25
|
+
if (loaded?.get(pi)?.has(key)) return true;
|
|
26
|
+
const query = { key, loaded: false };
|
|
27
|
+
pi.events?.emit(PACKAGE_QUERY, query);
|
|
28
|
+
return query.loaded;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function once(pi: PackageHost, key: string, fn: () => void): void {
|
|
14
32
|
const g = globalThis as { __pixOnce?: WeakMap<object, Set<string>> };
|
|
15
33
|
if (!g.__pixOnce) g.__pixOnce = new WeakMap<object, Set<string>>();
|
|
16
34
|
const registry = g.__pixOnce;
|
|
@@ -18,6 +36,12 @@ export function once(pi: object, key: string, fn: () => void): void {
|
|
|
18
36
|
if (!loaded) {
|
|
19
37
|
loaded = new Set<string>();
|
|
20
38
|
registry.set(pi, loaded);
|
|
39
|
+
pi.events?.on(PACKAGE_QUERY, (data) => {
|
|
40
|
+
if (!data || typeof data !== "object" || !("key" in data) || !("loaded" in data)) return;
|
|
41
|
+
if (typeof data.key === "string" && registry.get(pi)?.has(data.key)) {
|
|
42
|
+
data.loaded = true;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
21
45
|
}
|
|
22
46
|
if (loaded.has(key)) return;
|
|
23
47
|
loaded.add(key);
|
package/src/os.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
10
|
import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { userInfo } from "node:os";
|
|
11
12
|
import { join } from "node:path";
|
|
12
13
|
import { resolveTool } from "./binaries/resolve.ts";
|
|
13
14
|
import { quoteForCmd, runTool, runToolSync } from "./exec.ts";
|
|
@@ -19,6 +20,19 @@ export interface OsOptions {
|
|
|
19
20
|
host?: HostPlatform;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/** Current OS account name; unavailable accounts do not prevent startup. */
|
|
24
|
+
export function username(): string | undefined {
|
|
25
|
+
try {
|
|
26
|
+
return (
|
|
27
|
+
userInfo()
|
|
28
|
+
.username.replace(/[\x00-\x1f\x7f-\x9f]/g, "")
|
|
29
|
+
.trim() || undefined
|
|
30
|
+
);
|
|
31
|
+
} catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
// ── Open a URL or path ──────────────────────────────────────────────────────
|
|
23
37
|
|
|
24
38
|
export interface OpenOptions extends OsOptions {
|
package/src/pix-command.ts
CHANGED
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
} from "@earendil-works/pi-tui";
|
|
19
19
|
import { createBinariesTab } from "./binaries-tab.ts";
|
|
20
20
|
import { icon } from "./icon-catalog.ts";
|
|
21
|
-
import
|
|
21
|
+
import { isPackageLoaded } from "./once.ts";
|
|
22
|
+
import { type PixRuntime, pixRuntime } from "./runtime.ts";
|
|
22
23
|
import { type DeepPartial, deepMerge, type SectionHandle } from "./schema.ts";
|
|
23
24
|
import { collapseSection } from "./sections/collapse.ts";
|
|
24
25
|
import { compactionSection } from "./sections/compaction.ts";
|
|
@@ -213,11 +214,34 @@ function binariesEnv(runtime: PixRuntime): NodeJS.ProcessEnv {
|
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
const TABS = ["Settings", "Binaries", "Footer"] as const;
|
|
217
|
+
/** Most frequently changed tabs sit leftmost; unknown package tabs go before rare ones. */
|
|
218
|
+
const TAB_ORDER = ["Settings", "Tools", "MCP", "Optimizer", "Web", "Voice"];
|
|
219
|
+
const RARE_TABS = ["Footer", "Binaries"];
|
|
220
|
+
function tabRank(label: string): number {
|
|
221
|
+
const common = TAB_ORDER.indexOf(label);
|
|
222
|
+
if (common >= 0) return common;
|
|
223
|
+
const rare = RARE_TABS.indexOf(label);
|
|
224
|
+
return rare >= 0 ? 100 + rare : 50;
|
|
225
|
+
}
|
|
226
|
+
const SECTION_PACKAGES: Record<string, string> = {
|
|
227
|
+
Compaction: "pix-core",
|
|
228
|
+
Edit: "pix-diagnostics",
|
|
229
|
+
Gate: "pix-gate",
|
|
230
|
+
Footer: "pix-footer",
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
function visibleSettings(pi: ExtensionAPI): SettingRow<unknown>[] {
|
|
234
|
+
return SETTINGS.filter(
|
|
235
|
+
(row) =>
|
|
236
|
+
!SECTION_PACKAGES[row.section] ||
|
|
237
|
+
isPackageLoaded(pi, SECTION_PACKAGES[row.section] as string),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
216
240
|
|
|
217
|
-
function buildSummary(runtime: PixRuntime): string {
|
|
241
|
+
function buildSummary(runtime: PixRuntime, settings: SettingRow<unknown>[]): string {
|
|
218
242
|
const lines = [`Pix Settings (${runtime.path})`, ""];
|
|
219
243
|
let lastSection = "";
|
|
220
|
-
for (const row of
|
|
244
|
+
for (const row of settings) {
|
|
221
245
|
if (row.section !== lastSection) {
|
|
222
246
|
if (lastSection) lines.push("");
|
|
223
247
|
lines.push(`[${row.section}]`);
|
|
@@ -236,12 +260,43 @@ function buildSummary(runtime: PixRuntime): string {
|
|
|
236
260
|
return lines.join("\n");
|
|
237
261
|
}
|
|
238
262
|
|
|
263
|
+
export type PackageSettings = {
|
|
264
|
+
id: string;
|
|
265
|
+
label: string;
|
|
266
|
+
open(ctx: ExtensionCommandContext): Promise<void>;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const SETTINGS_QUERY = "pix:settings";
|
|
270
|
+
const COMMAND_QUERY = "pix:settings-command";
|
|
271
|
+
|
|
272
|
+
/** Register only from an active package factory; Pi clears subscriptions on reload. */
|
|
273
|
+
export function registerPackageSettings(pi: ExtensionAPI, settings: PackageSettings): void {
|
|
274
|
+
pi.events?.on(SETTINGS_QUERY, (data: unknown) => {
|
|
275
|
+
if (data instanceof Map) data.set(settings.id, settings);
|
|
276
|
+
});
|
|
277
|
+
registerPixCommand(pi, pixRuntime());
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function packageSettings(pi: ExtensionAPI): PackageSettings[] {
|
|
281
|
+
const entries = new Map<string, PackageSettings>();
|
|
282
|
+
pi.events?.emit(SETTINGS_QUERY, entries);
|
|
283
|
+
return [...entries.values()].sort((a, b) => a.label.localeCompare(b.label));
|
|
284
|
+
}
|
|
285
|
+
|
|
239
286
|
export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void {
|
|
287
|
+
const claim = { registered: false };
|
|
288
|
+
pi.events?.emit(COMMAND_QUERY, claim);
|
|
289
|
+
if (claim.registered) return;
|
|
290
|
+
pi.events?.on(COMMAND_QUERY, (data: unknown) => {
|
|
291
|
+
if (data && typeof data === "object" && "registered" in data) data.registered = true;
|
|
292
|
+
});
|
|
240
293
|
pi.registerCommand("pix", {
|
|
241
294
|
description: "pix: open shared settings (edit pix.json)",
|
|
242
295
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
243
296
|
// Re-parse pix.json so section parsers remain the single validation source.
|
|
244
297
|
await runtime.reload({ origin: "command", source: "pix" });
|
|
298
|
+
const panels = packageSettings(pi);
|
|
299
|
+
const settings = visibleSettings(pi);
|
|
245
300
|
// SAFETY: Runtime UI supports custom overlays beyond the published command context type.
|
|
246
301
|
const ui = ctx.ui as unknown as {
|
|
247
302
|
custom?: (f: unknown, opts?: unknown) => Promise<unknown>;
|
|
@@ -249,7 +304,12 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
249
304
|
};
|
|
250
305
|
|
|
251
306
|
if (typeof ui.custom !== "function") {
|
|
252
|
-
ui.notify(
|
|
307
|
+
ui.notify(
|
|
308
|
+
[buildSummary(runtime, settings), ...panels.map((panel) => `[${panel.label}]`)].join(
|
|
309
|
+
"\n",
|
|
310
|
+
),
|
|
311
|
+
"info",
|
|
312
|
+
);
|
|
253
313
|
return;
|
|
254
314
|
}
|
|
255
315
|
|
|
@@ -272,7 +332,76 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
272
332
|
let visibleBodyLines = 1;
|
|
273
333
|
let maxBodyOffset = 0;
|
|
274
334
|
let inspectingPage = false;
|
|
275
|
-
|
|
335
|
+
const tabs = [
|
|
336
|
+
...TABS.filter(
|
|
337
|
+
(name) => name !== "Footer" || settings.some((row) => row.section === "Footer"),
|
|
338
|
+
),
|
|
339
|
+
...panels.map((entry) => entry.label),
|
|
340
|
+
].sort((a, b) => tabRank(a) - tabRank(b));
|
|
341
|
+
let tab = "Settings";
|
|
342
|
+
let embedded:
|
|
343
|
+
| { render(width: number): string[]; handleInput(data: string): void; dispose?(): void }
|
|
344
|
+
| undefined;
|
|
345
|
+
let leavePanel: (() => void) | undefined;
|
|
346
|
+
let panelGeneration = 0;
|
|
347
|
+
let panelShrink = 1;
|
|
348
|
+
const mountPanel = () => {
|
|
349
|
+
const generation = ++panelGeneration;
|
|
350
|
+
leavePanel?.();
|
|
351
|
+
embedded?.dispose?.();
|
|
352
|
+
embedded = undefined;
|
|
353
|
+
leavePanel = undefined;
|
|
354
|
+
const panel = panels.find((entry) => entry.label === tab);
|
|
355
|
+
if (!panel) return;
|
|
356
|
+
const panelContext = {
|
|
357
|
+
...ctx,
|
|
358
|
+
ui: {
|
|
359
|
+
...ctx.ui,
|
|
360
|
+
custom: async (
|
|
361
|
+
factory: (
|
|
362
|
+
tui: unknown,
|
|
363
|
+
theme: unknown,
|
|
364
|
+
kb: unknown,
|
|
365
|
+
done: (value: unknown) => void,
|
|
366
|
+
) => NonNullable<typeof embedded>,
|
|
367
|
+
) => {
|
|
368
|
+
if (generation !== panelGeneration) return null;
|
|
369
|
+
return new Promise<unknown>((resolve) => {
|
|
370
|
+
leavePanel = () => resolve(null);
|
|
371
|
+
// The /pix tab row takes one line; render() shrinks the panel's terminal until both fit.
|
|
372
|
+
const panelTui = {
|
|
373
|
+
...tui,
|
|
374
|
+
terminal: {
|
|
375
|
+
...tui.terminal,
|
|
376
|
+
get rows() {
|
|
377
|
+
return Math.max(
|
|
378
|
+
1,
|
|
379
|
+
(tui.terminal?.rows ?? process.stdout.rows ?? 24) - panelShrink,
|
|
380
|
+
);
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
embedded = factory(panelTui, theme, kb, (value: unknown) => {
|
|
385
|
+
if (generation !== panelGeneration) return resolve(null);
|
|
386
|
+
if (value == null) {
|
|
387
|
+
embedded = undefined;
|
|
388
|
+
done(null);
|
|
389
|
+
}
|
|
390
|
+
resolve(value);
|
|
391
|
+
});
|
|
392
|
+
tui.requestRender();
|
|
393
|
+
});
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
} as unknown as ExtensionCommandContext;
|
|
397
|
+
void panel.open(panelContext).catch((error) => {
|
|
398
|
+
if (generation === panelGeneration)
|
|
399
|
+
ui.notify(
|
|
400
|
+
`pix: ${panel.label}: ${error instanceof Error ? error.message : String(error)}`,
|
|
401
|
+
"error",
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
};
|
|
276
405
|
let binaries: ReturnType<typeof createBinariesTab> | undefined;
|
|
277
406
|
const binariesTab = () => {
|
|
278
407
|
binaries ??= createBinariesTab({
|
|
@@ -283,22 +412,25 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
283
412
|
return binaries;
|
|
284
413
|
};
|
|
285
414
|
const tabBar = () =>
|
|
286
|
-
|
|
287
|
-
name
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
415
|
+
tabs
|
|
416
|
+
.map((name) =>
|
|
417
|
+
name === tab
|
|
418
|
+
? theme.fg("accent", theme.bold(` ${name} `))
|
|
419
|
+
: theme.fg("muted", ` ${name} `),
|
|
420
|
+
)
|
|
421
|
+
.join(theme.fg("muted", "│"));
|
|
291
422
|
const switchTab = (direction: -1 | 1) => {
|
|
292
|
-
const next = (
|
|
293
|
-
tab =
|
|
423
|
+
const next = (tabs.indexOf(tab) + direction + tabs.length) % tabs.length;
|
|
424
|
+
tab = tabs[next] ?? "Settings";
|
|
294
425
|
selected = 0;
|
|
295
426
|
bodyOffset = 0;
|
|
296
427
|
inspectingPage = false;
|
|
297
428
|
if (tab === "Binaries") binariesTab().refresh();
|
|
429
|
+
mountPanel();
|
|
298
430
|
};
|
|
299
431
|
|
|
300
432
|
const rows = () =>
|
|
301
|
-
|
|
433
|
+
settings.filter((r) => (r.section === "Footer") === (tab === "Footer"));
|
|
302
434
|
const cycle = (direction: -1 | 1) => {
|
|
303
435
|
const row = rows()[selected];
|
|
304
436
|
if (!row) return;
|
|
@@ -370,6 +502,26 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
370
502
|
maxBodyOffset = result.maxBodyOffset;
|
|
371
503
|
return result.lines;
|
|
372
504
|
}
|
|
505
|
+
if (!TABS.some((name) => name === tab)) {
|
|
506
|
+
if (embedded) {
|
|
507
|
+
let lines = embedded.render(width);
|
|
508
|
+
// Keep the composite inside the same budget as built-in tabs.
|
|
509
|
+
for (let tries = 0; lines.length + 1 > frame.maxHeight && tries < 8; tries++) {
|
|
510
|
+
panelShrink += lines.length + 1 - frame.maxHeight;
|
|
511
|
+
lines = embedded.render(width);
|
|
512
|
+
}
|
|
513
|
+
const panelWidth = visibleWidth(lines[0] ?? "");
|
|
514
|
+
lines.splice(1, 0, ...frameRows(panelWidth, [tabBar()], frame).slice(1, -1));
|
|
515
|
+
return lines;
|
|
516
|
+
}
|
|
517
|
+
return frameModal({
|
|
518
|
+
...frame,
|
|
519
|
+
header: [tabBar(), ""],
|
|
520
|
+
body: [theme.fg("dim", ` Loading ${tab} settings…`)],
|
|
521
|
+
footer: ["", tabGuide + guideSep + guide("esc", "close")],
|
|
522
|
+
bodyOffset: 0,
|
|
523
|
+
}).lines;
|
|
524
|
+
}
|
|
373
525
|
const settings = rows();
|
|
374
526
|
const labelW = Math.max(...settings.map((r) => r.label.length));
|
|
375
527
|
const body: string[] = [];
|
|
@@ -415,6 +567,11 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
415
567
|
maxBodyOffset = result.maxBodyOffset;
|
|
416
568
|
return result.lines;
|
|
417
569
|
},
|
|
570
|
+
dispose: () => {
|
|
571
|
+
panelGeneration++;
|
|
572
|
+
leavePanel?.();
|
|
573
|
+
embedded?.dispose?.();
|
|
574
|
+
},
|
|
418
575
|
invalidate: () => {},
|
|
419
576
|
handleInput: (data: string) => {
|
|
420
577
|
if (tab === "Binaries" && binariesTab().editing) {
|
|
@@ -435,6 +592,10 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
435
592
|
tui.requestRender();
|
|
436
593
|
return;
|
|
437
594
|
}
|
|
595
|
+
if (!TABS.some((name) => name === tab) && embedded) {
|
|
596
|
+
embedded.handleInput(data);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
438
599
|
const pageUp = kb?.matches(data, "tui.select.pageUp") || matchesKey(data, Key.pageUp);
|
|
439
600
|
const pageDown =
|
|
440
601
|
kb?.matches(data, "tui.select.pageDown") || matchesKey(data, Key.pageDown);
|