@xynogen/pix-runtime 0.15.2 → 0.16.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-runtime",
3
- "version": "0.15.2",
3
+ "version": "0.16.1",
4
4
  "description": "Pix shared runtime — versioned pix.json config, atomic persistence, typed change events",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/exec.ts CHANGED
@@ -97,7 +97,7 @@ export interface ToolExecOptions {
97
97
  onStatus?: (status: ToolStatus) => void;
98
98
  /** Test seam for the host OS. */
99
99
  host?: HostPlatform;
100
- /** Cap on captured stdout/stderr bytes each (default 50 MB). */
100
+ /** Cap on captured stdout/stderr bytes each (default 50 MB). Past it the child is stopped. */
101
101
  maxBuffer?: number;
102
102
  /** Args are already quoted for the target (cmd.exe `/c` lines); Node must not re-quote. */
103
103
  verbatim?: boolean;
@@ -111,7 +111,7 @@ export interface ToolRunResult {
111
111
  /** Raw stdout for binary output (clipboard images, archives). */
112
112
  stdoutBytes: Buffer;
113
113
  timedOut: boolean;
114
- /** Output passed `maxBuffer` and was cut. A zero exit code is then not a complete result. */
114
+ /** Output passed `maxBuffer`; the child was stopped and output cut. Never a complete result. */
115
115
  truncated: boolean;
116
116
  /** The binary that ran and where it was found. */
117
117
  tool: ResolvedTool;
@@ -220,7 +220,11 @@ function collect(
220
220
  let truncated = false;
221
221
  const push = (list: Buffer[], chunk: Buffer, len: number): number => {
222
222
  const room = cap - len;
223
- if (chunk.length > room) truncated = true;
223
+ // Like pi.exec: stop a runaway child instead of draining output nobody keeps.
224
+ if (chunk.length > room) {
225
+ truncated = true;
226
+ stop();
227
+ }
224
228
  if (room <= 0) return len;
225
229
  list.push(chunk.length > room ? chunk.subarray(0, room) : chunk);
226
230
  return len + Math.min(chunk.length, room);
@@ -135,11 +135,15 @@ function envMode(): IconMode {
135
135
  return "nerd";
136
136
  }
137
137
 
138
- let activeMode: IconMode = envMode();
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 activeMode;
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 = new Set<ModeListener>();
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 === activeMode) return;
167
- activeMode = mode;
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[activeMode] : "";
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
@@ -32,6 +32,7 @@ export {
32
32
  parseLfid,
33
33
  uniqueLfid,
34
34
  } from "./lfid.ts";
35
+ export { type PackageSettings, registerPackageSettings } from "./pix-command.ts";
35
36
  export {
36
37
  config,
37
38
  createRuntime,
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
- export function once(pi: object, key: string, fn: () => void): void {
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 {
@@ -32,7 +46,7 @@ export function openCommand(
32
46
  target: string,
33
47
  host: HostPlatform,
34
48
  app?: string,
35
- ): { name: string; args: string[]; verbatim?: boolean } {
49
+ ): { name: string; args: string[]; verbatim?: boolean; cwd?: string } {
36
50
  if (host.os === "darwin") return { name: "open", args: app ? ["-a", app, target] : [target] };
37
51
  if (host.os === "win32") {
38
52
  // `start` is a cmd.exe builtin; "" is its window-title argument.
@@ -40,8 +54,9 @@ export function openCommand(
40
54
  return { name: "cmd", args: ["/d", "/s", "/c", `"${parts.join(" ")}"`], verbatim: true };
41
55
  }
42
56
  if (host.wsl && !app) {
43
- // WSL: hand the target to Windows so the user's default browser opens.
44
- return { name: "wslview", args: [target] };
57
+ // WSL: hand the target to Windows so the user's default browser opens. Windows programs
58
+ // cannot start from a long `\\wsl.localhost\...` cwd, so start from `/`.
59
+ return { name: "wslview", args: [target], cwd: "/" };
45
60
  }
46
61
  return { name: app ?? "xdg-open", args: [target] };
47
62
  }
@@ -57,6 +72,7 @@ export async function openTarget(target: string, opts: OpenOptions = {}): Promis
57
72
  if (cmd.name === "wslview" && !resolveTool("wslview", opts))
58
73
  cmd = openCommand(target, { ...host, wsl: false });
59
74
  const result = await runTool(cmd.name, cmd.args, {
75
+ cwd: cmd.cwd,
60
76
  env: opts.env,
61
77
  host,
62
78
  timeoutMs: opts.timeoutMs ?? 15_000,
@@ -126,10 +142,16 @@ export function pickPreferredMime(types: readonly string[]): string | null {
126
142
  }
127
143
 
128
144
  /** Output bytes when the tool exists and exits 0; null otherwise (probing, never throws). */
129
- function probe(name: string, args: string[], timeoutMs: number, opts: OsOptions): Buffer | null {
145
+ function probe(
146
+ name: string,
147
+ args: string[],
148
+ timeoutMs: number,
149
+ opts: OsOptions,
150
+ cwd?: string,
151
+ ): Buffer | null {
130
152
  if (!resolveTool(name, opts)) return null;
131
153
  try {
132
- const r = runToolSync(name, args, { env: opts.env, host: opts.host, timeoutMs });
154
+ const r = runToolSync(name, args, { env: opts.env, host: opts.host, timeoutMs, cwd });
133
155
  return r.code === 0 ? r.stdoutBytes : null;
134
156
  } catch {
135
157
  return null;
@@ -189,7 +211,8 @@ function viaPowerShell(host: HostPlatform, opts: OsOptions): ClipboardImage | nu
189
211
  "if ($img) { $img.Save($path, [System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'ok' } else { Write-Output 'empty' }",
190
212
  ].join("; ");
191
213
  const args = ["-NoProfile", "-STA", "-Command", script];
192
- const out = probe("powershell", args, POWERSHELL_TIMEOUT_MS, opts);
214
+ // From WSL, Windows programs cannot start from a long `\\wsl.localhost\...` cwd.
215
+ const out = probe("powershell", args, POWERSHELL_TIMEOUT_MS, opts, host.wsl ? "/" : undefined);
193
216
  if (out?.toString("utf-8").trim() !== "ok") return null;
194
217
  const bytes = readFileSync(tmpFile);
195
218
  return bytes.length ? { bytes, mimeType: "image/png" } : null;
@@ -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 type { PixRuntime } from "./runtime.ts";
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";
@@ -41,6 +42,21 @@ function row<T>(r: SettingRow<T>): SettingRow<unknown> {
41
42
  return r as SettingRow<unknown>;
42
43
  }
43
44
 
45
+ /** Pi-owned theme row: not stored in pix.json, applied through `ctx.ui.setTheme`. */
46
+ interface ThemeRow {
47
+ kind: "theme";
48
+ section: string;
49
+ label: string;
50
+ values: readonly string[];
51
+ current: () => string;
52
+ }
53
+
54
+ type Row = SettingRow<unknown> | ThemeRow;
55
+
56
+ function isThemeRow(row: Row): row is ThemeRow {
57
+ return "kind" in row;
58
+ }
59
+
44
60
  /** Render a token count as a compact label: 1_000_000 → "1M", 150_000 → "150k". */
45
61
  function formatTokens(tokens: number): string {
46
62
  return tokens % 1_000_000 === 0 ? `${tokens / 1_000_000}M` : `${tokens / 1000}k`;
@@ -213,11 +229,34 @@ function binariesEnv(runtime: PixRuntime): NodeJS.ProcessEnv {
213
229
  }
214
230
 
215
231
  const TABS = ["Settings", "Binaries", "Footer"] as const;
232
+ /** Most frequently changed tabs sit leftmost; unknown package tabs go before rare ones. */
233
+ const TAB_ORDER = ["Settings", "Tools", "MCP", "Optimizer", "Web", "Voice"];
234
+ const RARE_TABS = ["Footer", "Binaries"];
235
+ function tabRank(label: string): number {
236
+ const common = TAB_ORDER.indexOf(label);
237
+ if (common >= 0) return common;
238
+ const rare = RARE_TABS.indexOf(label);
239
+ return rare >= 0 ? 100 + rare : 50;
240
+ }
241
+ const SECTION_PACKAGES: Record<string, string> = {
242
+ Compaction: "pix-core",
243
+ Edit: "pix-diagnostics",
244
+ Gate: "pix-gate",
245
+ Footer: "pix-footer",
246
+ };
247
+
248
+ function visibleSettings(pi: ExtensionAPI): SettingRow<unknown>[] {
249
+ return SETTINGS.filter(
250
+ (row) =>
251
+ !SECTION_PACKAGES[row.section] ||
252
+ isPackageLoaded(pi, SECTION_PACKAGES[row.section] as string),
253
+ );
254
+ }
216
255
 
217
- function buildSummary(runtime: PixRuntime): string {
256
+ function buildSummary(runtime: PixRuntime, settings: SettingRow<unknown>[]): string {
218
257
  const lines = [`Pix Settings (${runtime.path})`, ""];
219
258
  let lastSection = "";
220
- for (const row of SETTINGS) {
259
+ for (const row of settings) {
221
260
  if (row.section !== lastSection) {
222
261
  if (lastSection) lines.push("");
223
262
  lines.push(`[${row.section}]`);
@@ -236,12 +275,63 @@ function buildSummary(runtime: PixRuntime): string {
236
275
  return lines.join("\n");
237
276
  }
238
277
 
278
+ export type PackageSettings = {
279
+ id: string;
280
+ label: string;
281
+ open(ctx: ExtensionCommandContext): Promise<void>;
282
+ };
283
+
284
+ const SETTINGS_QUERY = "pix:settings";
285
+ const COMMAND_QUERY = "pix:settings-command";
286
+
287
+ /** Register only from an active package factory; Pi clears subscriptions on reload. */
288
+ export function registerPackageSettings(pi: ExtensionAPI, settings: PackageSettings): void {
289
+ pi.events?.on(SETTINGS_QUERY, (data: unknown) => {
290
+ if (data instanceof Map) data.set(settings.id, settings);
291
+ });
292
+ registerPixCommand(pi, pixRuntime());
293
+ }
294
+
295
+ function packageSettings(pi: ExtensionAPI): PackageSettings[] {
296
+ const entries = new Map<string, PackageSettings>();
297
+ pi.events?.emit(SETTINGS_QUERY, entries);
298
+ return [...entries.values()].sort((a, b) => a.label.localeCompare(b.label));
299
+ }
300
+
239
301
  export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void {
302
+ const claim = { registered: false };
303
+ pi.events?.emit(COMMAND_QUERY, claim);
304
+ if (claim.registered) return;
305
+ pi.events?.on(COMMAND_QUERY, (data: unknown) => {
306
+ if (data && typeof data === "object" && "registered" in data) data.registered = true;
307
+ });
240
308
  pi.registerCommand("pix", {
241
- description: "pix: open shared settings (edit pix.json)",
242
- handler: async (_args: string, ctx: ExtensionCommandContext) => {
309
+ description: "pix: open shared settings (edit pix.json); /pix <tab> opens that tab",
310
+ getArgumentCompletions: (prefix: string) => {
311
+ const p = prefix.trim().toLowerCase();
312
+ const items = [...TABS, ...packageSettings(pi).map((entry) => entry.label)]
313
+ .map((label) => label.toLowerCase())
314
+ .filter((name) => name.startsWith(p))
315
+ .map((name) => ({ value: name, label: name }));
316
+ return items.length ? items : null;
317
+ },
318
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
243
319
  // Re-parse pix.json so section parsers remain the single validation source.
244
320
  await runtime.reload({ origin: "command", source: "pix" });
321
+ const panels = packageSettings(pi);
322
+ const settings = visibleSettings(pi);
323
+ // Pi owns the active theme (settings.json); `/pix` only cycles it through `ctx.ui.setTheme`.
324
+ const themeNames = (ctx.ui.getAllThemes?.() ?? []).map((t) => t.name).sort();
325
+ const themeRow: ThemeRow | undefined =
326
+ themeNames.length > 1
327
+ ? {
328
+ kind: "theme",
329
+ section: "Pretty",
330
+ label: "theme",
331
+ values: themeNames,
332
+ current: () => ctx.ui.theme.name ?? "",
333
+ }
334
+ : undefined;
245
335
  // SAFETY: Runtime UI supports custom overlays beyond the published command context type.
246
336
  const ui = ctx.ui as unknown as {
247
337
  custom?: (f: unknown, opts?: unknown) => Promise<unknown>;
@@ -249,7 +339,12 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
249
339
  };
250
340
 
251
341
  if (typeof ui.custom !== "function") {
252
- ui.notify(buildSummary(runtime), "info");
342
+ ui.notify(
343
+ [buildSummary(runtime, settings), ...panels.map((panel) => `[${panel.label}]`)].join(
344
+ "\n",
345
+ ),
346
+ "info",
347
+ );
253
348
  return;
254
349
  }
255
350
 
@@ -272,7 +367,77 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
272
367
  let visibleBodyLines = 1;
273
368
  let maxBodyOffset = 0;
274
369
  let inspectingPage = false;
275
- let tab: (typeof TABS)[number] = "Settings";
370
+ const tabs = [
371
+ ...TABS.filter(
372
+ (name) => name !== "Footer" || settings.some((row) => row.section === "Footer"),
373
+ ),
374
+ ...panels.map((entry) => entry.label),
375
+ ].sort((a, b) => tabRank(a) - tabRank(b));
376
+ const wanted = (args ?? "").trim().toLowerCase();
377
+ let tab = tabs.find((name) => name.toLowerCase() === wanted) ?? "Settings";
378
+ let embedded:
379
+ | { render(width: number): string[]; handleInput(data: string): void; dispose?(): void }
380
+ | undefined;
381
+ let leavePanel: (() => void) | undefined;
382
+ let panelGeneration = 0;
383
+ let panelShrink = 1;
384
+ const mountPanel = () => {
385
+ const generation = ++panelGeneration;
386
+ leavePanel?.();
387
+ embedded?.dispose?.();
388
+ embedded = undefined;
389
+ leavePanel = undefined;
390
+ const panel = panels.find((entry) => entry.label === tab);
391
+ if (!panel) return;
392
+ const panelContext = {
393
+ ...ctx,
394
+ ui: {
395
+ ...ctx.ui,
396
+ custom: async (
397
+ factory: (
398
+ tui: unknown,
399
+ theme: unknown,
400
+ kb: unknown,
401
+ done: (value: unknown) => void,
402
+ ) => NonNullable<typeof embedded>,
403
+ ) => {
404
+ if (generation !== panelGeneration) return null;
405
+ return new Promise<unknown>((resolve) => {
406
+ leavePanel = () => resolve(null);
407
+ // The /pix tab row takes one line; render() shrinks the panel's terminal until both fit.
408
+ const panelTui = {
409
+ ...tui,
410
+ terminal: {
411
+ ...tui.terminal,
412
+ get rows() {
413
+ return Math.max(
414
+ 1,
415
+ (tui.terminal?.rows ?? process.stdout.rows ?? 24) - panelShrink,
416
+ );
417
+ },
418
+ },
419
+ };
420
+ embedded = factory(panelTui, theme, kb, (value: unknown) => {
421
+ if (generation !== panelGeneration) return resolve(null);
422
+ if (value == null) {
423
+ embedded = undefined;
424
+ done(null);
425
+ }
426
+ resolve(value);
427
+ });
428
+ tui.requestRender();
429
+ });
430
+ },
431
+ },
432
+ } as unknown as ExtensionCommandContext;
433
+ void panel.open(panelContext).catch((error) => {
434
+ if (generation === panelGeneration)
435
+ ui.notify(
436
+ `pix: ${panel.label}: ${error instanceof Error ? error.message : String(error)}`,
437
+ "error",
438
+ );
439
+ });
440
+ };
276
441
  let binaries: ReturnType<typeof createBinariesTab> | undefined;
277
442
  const binariesTab = () => {
278
443
  binaries ??= createBinariesTab({
@@ -283,25 +448,44 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
283
448
  return binaries;
284
449
  };
285
450
  const tabBar = () =>
286
- TABS.map((name) =>
287
- name === tab
288
- ? theme.fg("accent", theme.bold(` ${name} `))
289
- : theme.fg("muted", ` ${name} `),
290
- ).join(theme.fg("muted", "│"));
451
+ tabs
452
+ .map((name) =>
453
+ name === tab
454
+ ? theme.fg("accent", theme.bold(` ${name} `))
455
+ : theme.fg("muted", ` ${name} `),
456
+ )
457
+ .join(theme.fg("muted", "│"));
291
458
  const switchTab = (direction: -1 | 1) => {
292
- const next = (TABS.indexOf(tab) + direction + TABS.length) % TABS.length;
293
- tab = TABS[next] ?? "Settings";
459
+ const next = (tabs.indexOf(tab) + direction + tabs.length) % tabs.length;
460
+ tab = tabs[next] ?? "Settings";
294
461
  selected = 0;
295
462
  bodyOffset = 0;
296
463
  inspectingPage = false;
297
464
  if (tab === "Binaries") binariesTab().refresh();
465
+ mountPanel();
298
466
  };
299
467
 
300
- const rows = () =>
301
- SETTINGS.filter((r) => (r.section === "Footer") === (tab === "Footer"));
468
+ // `/pix <tab>` opens straight into that tab.
469
+ if (tab === "Binaries") binariesTab().refresh();
470
+ mountPanel();
471
+
472
+ const rows = (): Row[] => {
473
+ const list = settings.filter((r) => (r.section === "Footer") === (tab === "Footer"));
474
+ return tab === "Settings" && themeRow ? [themeRow, ...list] : list;
475
+ };
302
476
  const cycle = (direction: -1 | 1) => {
303
477
  const row = rows()[selected];
304
478
  if (!row) return;
479
+ if (isThemeRow(row)) {
480
+ const cur = row.values.indexOf(row.current());
481
+ const start = cur === -1 && direction === -1 ? 0 : cur;
482
+ const next = row.values[(start + direction + row.values.length) % row.values.length];
483
+ const result = next ? ctx.ui.setTheme(next) : undefined;
484
+ if (result && !result.success)
485
+ ui.notify(`pix: theme ${next}: ${result.error ?? "failed"}`, "error");
486
+ tui.requestRender();
487
+ return;
488
+ }
305
489
  // Functional updater: the runtime queue serializes writes, and the
306
490
  // callback sees the latest committed value — so rapid presses each
307
491
  // advance exactly one step instead of collapsing on a stale snapshot.
@@ -370,6 +554,26 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
370
554
  maxBodyOffset = result.maxBodyOffset;
371
555
  return result.lines;
372
556
  }
557
+ if (!TABS.some((name) => name === tab)) {
558
+ if (embedded) {
559
+ let lines = embedded.render(width);
560
+ // Keep the composite inside the same budget as built-in tabs.
561
+ for (let tries = 0; lines.length + 1 > frame.maxHeight && tries < 8; tries++) {
562
+ panelShrink += lines.length + 1 - frame.maxHeight;
563
+ lines = embedded.render(width);
564
+ }
565
+ const panelWidth = visibleWidth(lines[0] ?? "");
566
+ lines.splice(1, 0, ...frameRows(panelWidth, [tabBar()], frame).slice(1, -1));
567
+ return lines;
568
+ }
569
+ return frameModal({
570
+ ...frame,
571
+ header: [tabBar(), ""],
572
+ body: [theme.fg("dim", ` Loading ${tab} settings…`)],
573
+ footer: ["", tabGuide + guideSep + guide("esc", "close")],
574
+ bodyOffset: 0,
575
+ }).lines;
576
+ }
373
577
  const settings = rows();
374
578
  const labelW = Math.max(...settings.map((r) => r.label.length));
375
579
  const body: string[] = [];
@@ -386,8 +590,8 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
386
590
  const sel = i === selected;
387
591
  const cursor = sel ? theme.fg("accent", "→") : " ";
388
592
  const label = theme.fg(sel ? "accent" : "text", row.label.padEnd(labelW));
389
- const value = row.read(runtime.get(row.handle));
390
- const isDefault = value === row.read(row.handle.defaults);
593
+ const value = isThemeRow(row) ? row.current() : row.read(runtime.get(row.handle));
594
+ const isDefault = isThemeRow(row) || value === row.read(row.handle.defaults);
391
595
  settingBodyLines[i] = body.length;
392
596
  body.push(`${cursor} ${label} ${theme.fg(isDefault ? "dim" : "success", value)}`);
393
597
  }
@@ -415,6 +619,11 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
415
619
  maxBodyOffset = result.maxBodyOffset;
416
620
  return result.lines;
417
621
  },
622
+ dispose: () => {
623
+ panelGeneration++;
624
+ leavePanel?.();
625
+ embedded?.dispose?.();
626
+ },
418
627
  invalidate: () => {},
419
628
  handleInput: (data: string) => {
420
629
  if (tab === "Binaries" && binariesTab().editing) {
@@ -435,6 +644,10 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
435
644
  tui.requestRender();
436
645
  return;
437
646
  }
647
+ if (!TABS.some((name) => name === tab) && embedded) {
648
+ embedded.handleInput(data);
649
+ return;
650
+ }
438
651
  const pageUp = kb?.matches(data, "tui.select.pageUp") || matchesKey(data, Key.pageUp);
439
652
  const pageDown =
440
653
  kb?.matches(data, "tui.select.pageDown") || matchesKey(data, Key.pageDown);