@xynogen/pix-runtime 0.2.0 → 0.3.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.2.0",
3
+ "version": "0.3.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",
@@ -7,12 +7,18 @@
7
7
  */
8
8
 
9
9
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
10
- import { matchesKey } from "@earendil-works/pi-tui";
10
+ import {
11
+ Key,
12
+ type KeybindingsManager,
13
+ matchesKey,
14
+ truncateToWidth,
15
+ visibleWidth,
16
+ wrapTextWithAnsi,
17
+ } from "@earendil-works/pi-tui";
11
18
  import type { PixRuntime } from "./runtime.ts";
12
19
  import type { DeepPartial, SectionHandle } from "./schema.ts";
13
20
  import { collapseSection } from "./sections/collapse.ts";
14
21
  import { gateSection } from "./sections/gate.ts";
15
- import { optimizerSection } from "./sections/optimizer.ts";
16
22
  import { prettySection } from "./sections/pretty.ts";
17
23
 
18
24
  interface SettingRow<T> {
@@ -62,29 +68,13 @@ const SETTINGS: SettingRow<unknown>[] = [
62
68
  read: (v) => String(v.delaySec),
63
69
  patch: (value) => ({ delaySec: Number(value) }),
64
70
  }),
65
- row({
66
- section: "Optimizer",
67
- label: "caveman",
68
- handle: optimizerSection,
69
- values: ["off", "lite", "full", "ultra", "micro"],
70
- read: (v) => v.caveman,
71
- patch: (value) => ({ caveman: value as "off" | "lite" | "full" | "ultra" | "micro" }),
72
- }),
73
- row({
74
- section: "Optimizer",
75
- label: "rtk",
76
- handle: optimizerSection,
77
- values: ["on", "off"],
78
- read: (v) => v.rtk,
79
- patch: (value) => ({ rtk: value as "on" | "off" }),
80
- }),
81
71
  row({
82
72
  section: "Gate",
83
- label: "disable defaults",
73
+ label: "Guardrails",
84
74
  handle: gateSection,
85
- values: ["false", "true"],
86
- read: (v) => String(v.disableDefaults),
87
- patch: (value) => ({ disableDefaults: value === "true" }),
75
+ values: ["on", "off"],
76
+ read: (v) => v.guardrails,
77
+ patch: (guardrails) => ({ guardrails: guardrails as "on" | "off" }),
88
78
  }),
89
79
  ];
90
80
 
@@ -121,15 +111,20 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
121
111
  const boxW = 52;
122
112
  await ui.custom(
123
113
  (
124
- tui: { requestRender(): void },
114
+ tui: { requestRender(): void; terminal?: { rows?: number } },
125
115
  theme: {
126
116
  fg(c: string, t: string): string;
117
+ bg(c: string, t: string): string;
127
118
  bold(t: string): string;
128
119
  },
129
- _kb: unknown,
120
+ kb: KeybindingsManager,
130
121
  done: (v: null) => void,
131
122
  ) => {
132
123
  let selected = 0;
124
+ let bodyOffset = 0;
125
+ let visibleBodyLines = 1;
126
+ let maxBodyOffset = 0;
127
+ let inspectingPage = false;
133
128
 
134
129
  const cycle = (direction: -1 | 1) => {
135
130
  const row = SETTINGS[selected];
@@ -142,18 +137,20 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
142
137
  };
143
138
  const move = (direction: -1 | 1) => {
144
139
  selected = (selected + direction + SETTINGS.length) % SETTINGS.length;
140
+ inspectingPage = false;
145
141
  };
146
142
 
147
143
  return {
148
144
  render: () => {
149
145
  const labelW = Math.max(...SETTINGS.map((r) => r.label.length));
150
- const lines: string[] = [theme.fg("accent", theme.bold(" pix settings")), ""];
146
+ const body: string[] = [];
147
+ const settingBodyLines: number[] = [];
151
148
  let lastSection = "";
152
149
  for (let i = 0; i < SETTINGS.length; i++) {
153
150
  const row = SETTINGS[i]!;
154
151
  if (row.section !== lastSection) {
155
- if (lastSection) lines.push("");
156
- lines.push(theme.fg("dim", ` ${row.section}`));
152
+ if (lastSection) body.push("");
153
+ body.push(theme.fg("dim", ` ${row.section}`));
157
154
  lastSection = row.section;
158
155
  }
159
156
  const sel = i === selected;
@@ -161,14 +158,40 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
161
158
  const label = theme.fg(sel ? "accent" : "text", row.label.padEnd(labelW));
162
159
  const value = row.read(runtime.get(row.handle));
163
160
  const isDefault = value === row.values[0];
164
- lines.push(`${cursor} ${label} ${theme.fg(isDefault ? "dim" : "success", value)}`);
161
+ settingBodyLines[i] = body.length;
162
+ body.push(`${cursor} ${label} ${theme.fg(isDefault ? "dim" : "success", value)}`);
165
163
  }
166
- lines.push("");
167
- lines.push(theme.fg("dim", "←→ change · ↑↓ move · esc close"));
168
- return lines;
164
+ const result = frameModal({
165
+ width: boxW,
166
+ maxHeight: modalHeight(tui.terminal?.rows),
167
+ header: [theme.fg("accent", theme.bold(" pix settings")), ""],
168
+ body,
169
+ footer: [
170
+ "",
171
+ theme.fg("dim", "←→ change · ↑↓ move · PgUp/PgDn inspect · esc close"),
172
+ ],
173
+ bodyOffset,
174
+ selectedBodyLine: inspectingPage ? undefined : settingBodyLines[selected],
175
+ color: (s: string) => theme.fg("accent", s),
176
+ bg: (s: string) => theme.bg("customMessageBg", s),
177
+ });
178
+ bodyOffset = result.bodyOffset;
179
+ visibleBodyLines = Math.max(1, result.visibleBodyLines);
180
+ maxBodyOffset = result.maxBodyOffset;
181
+ return result.lines;
169
182
  },
170
183
  invalidate: () => {},
171
184
  handleInput: (data: string) => {
185
+ const pageUp = kb?.matches(data, "tui.select.pageUp") || matchesKey(data, Key.pageUp);
186
+ const pageDown =
187
+ kb?.matches(data, "tui.select.pageDown") || matchesKey(data, Key.pageDown);
188
+ if (pageUp || pageDown) {
189
+ const direction = pageUp ? -1 : 1;
190
+ bodyOffset = pageBodyOffset(bodyOffset, visibleBodyLines, maxBodyOffset, direction);
191
+ inspectingPage = true;
192
+ tui.requestRender();
193
+ return;
194
+ }
172
195
  // matchesKey handles both legacy bytes and Kitty CSI-u encodings
173
196
  // for letters and special keys alike — raw string compares like
174
197
  // `data === "k"` silently fail under the Kitty keyboard protocol.
@@ -195,3 +218,109 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
195
218
  },
196
219
  });
197
220
  }
221
+
222
+ // ── Inline modal engine (runtime cannot depend on pix-pretty: pretty depends on runtime) ─
223
+
224
+ interface RuntimeModalOptions {
225
+ width: number;
226
+ maxHeight: number;
227
+ header: string[];
228
+ body: string[];
229
+ footer: string[];
230
+ bodyOffset: number;
231
+ selectedBodyLine?: number;
232
+ color: (s: string) => string;
233
+ bg?: (s: string) => string;
234
+ }
235
+
236
+ interface RuntimeModalResult {
237
+ lines: string[];
238
+ bodyOffset: number;
239
+ visibleBodyLines: number;
240
+ maxBodyOffset: number;
241
+ }
242
+
243
+ function modalHeight(rows = 24): number {
244
+ const safe = Number.isFinite(rows) ? Math.max(1, Math.floor(rows)) : 24;
245
+ return Math.max(1, Math.min(safe, Math.floor(safe * 0.8)));
246
+ }
247
+
248
+ function pageBodyOffset(offset: number, size: number, max: number, direction: -1 | 1): number {
249
+ const s = Math.max(1, size);
250
+ const step = Math.max(1, Math.floor(s / 2));
251
+ return Math.min(max, Math.max(0, offset + direction * step));
252
+ }
253
+
254
+ function fitLines(lines: string[], width: number): string[] {
255
+ const rows: string[] = [];
256
+ for (const line of lines) {
257
+ if (line === "") rows.push("");
258
+ else rows.push(...wrapTextWithAnsi(line, Math.max(1, width)));
259
+ }
260
+ return rows;
261
+ }
262
+
263
+ function frameModal(opts: RuntimeModalOptions): RuntimeModalResult {
264
+ const inner = Math.max(1, opts.width - 4);
265
+ const header = fitLines(opts.header, inner);
266
+ const footer = fitLines(opts.footer, inner);
267
+ const body = fitLines(opts.body, inner);
268
+ const cap = Math.max(1, opts.maxHeight);
269
+ const bodyBudget = cap - 2 - header.length - footer.length;
270
+ const overflows = body.length > Math.max(0, bodyBudget);
271
+ const visibleBodyLines = overflows ? Math.max(0, bodyBudget - 1) : Math.max(0, bodyBudget);
272
+ if (visibleBodyLines === 0 && body.length > 0) {
273
+ return {
274
+ lines: frameRows(opts.width, ["Terminal too short — resize or press esc to cancel"], opts),
275
+ bodyOffset: 0,
276
+ visibleBodyLines: 0,
277
+ maxBodyOffset: 0,
278
+ };
279
+ }
280
+ const maxBodyOffset = Math.max(0, body.length - visibleBodyLines);
281
+ let offset = Math.min(maxBodyOffset, Math.max(0, opts.bodyOffset));
282
+ if (opts.selectedBodyLine !== undefined) {
283
+ const selected = Math.min(body.length - 1, Math.max(0, opts.selectedBodyLine));
284
+ if (selected < offset) offset = selected;
285
+ else if (selected >= offset + visibleBodyLines) offset = selected - visibleBodyLines + 1;
286
+ }
287
+ const end = Math.min(body.length, offset + visibleBodyLines);
288
+ const lines = [...header];
289
+ if (overflows) {
290
+ const step = Math.max(1, Math.floor(visibleBodyLines / 2));
291
+ const totalPages = Math.max(1, Math.ceil(maxBodyOffset / step) + 1);
292
+ const page = offset >= maxBodyOffset ? totalPages : Math.floor(offset / step) + 1;
293
+ lines.push(`PgUp/PgDn inspect • ${page}/${totalPages}`);
294
+ }
295
+ lines.push(...body.slice(offset, end), ...footer);
296
+ return {
297
+ lines: frameRows(opts.width, lines, opts),
298
+ bodyOffset: offset,
299
+ visibleBodyLines,
300
+ maxBodyOffset,
301
+ };
302
+ }
303
+
304
+ function frameRows(
305
+ width: number,
306
+ lines: string[],
307
+ opts: Pick<RuntimeModalOptions, "color" | "bg">,
308
+ ): string[] {
309
+ const bg = opts.bg ?? ((s: string) => s);
310
+ const inner = Math.max(1, width - 4);
311
+ const dashes = "─".repeat(width - 2);
312
+ const SENTINEL = "\x00";
313
+ const bgOpen = bg(SENTINEL).split(SENTINEL)[0] ?? "";
314
+ const reassert = (s: string): string =>
315
+ bgOpen
316
+ ? s.replace(/\x1b\[([0-9;]*)m/g, (seq, p: string) =>
317
+ p === "0" || p.split(";").includes("49") ? `${seq}${bgOpen}` : seq,
318
+ )
319
+ : s;
320
+ const row = (content: string): string => {
321
+ const fitted = truncateToWidth(content, inner, "…");
322
+ const padded = fitted + " ".repeat(Math.max(0, inner - visibleWidth(fitted)));
323
+ return bg(`${opts.color("│")} ${reassert(padded)} ${opts.color("│")}`);
324
+ };
325
+ return [bg(opts.color(`╭${dashes}╮`)), ...lines.map(row), bg(opts.color(`╰${dashes}╯`))];
326
+ }
@@ -1,4 +1,4 @@
1
- import { boolOr, defineSection, isObj, strArr } from "../schema.ts";
1
+ import { defineSection, enumOr, isObj, strArr } from "../schema.ts";
2
2
 
3
3
  export type GateSeverity = "risky" | "dangerous" | "critical";
4
4
 
@@ -10,7 +10,8 @@ export interface GateRuleConfig {
10
10
  }
11
11
 
12
12
  export interface GateConfig {
13
- disableDefaults: boolean;
13
+ /** Whether Pix's built-in command protections are active. */
14
+ guardrails: "on" | "off";
14
15
  autoApprove: string[];
15
16
  extraRules: GateRuleConfig[];
16
17
  }
@@ -18,7 +19,7 @@ export interface GateConfig {
18
19
  const SEVERITIES: readonly GateSeverity[] = ["risky", "dangerous", "critical"];
19
20
 
20
21
  const DEFAULTS: Readonly<GateConfig> = {
21
- disableDefaults: false,
22
+ guardrails: "on",
22
23
  autoApprove: [],
23
24
  extraRules: [],
24
25
  };
@@ -37,7 +38,7 @@ export const gateSection = defineSection<"gate", GateConfig>({
37
38
  key: "gate",
38
39
  defaults: DEFAULTS,
39
40
  parse(raw, ctx) {
40
- if (!isObj(raw)) return { disableDefaults: false, autoApprove: [], extraRules: [] };
41
+ if (!isObj(raw)) return { guardrails: "on", autoApprove: [], extraRules: [] };
41
42
  const extraRules: GateRuleConfig[] = [];
42
43
  if (Array.isArray(raw.extraRules)) {
43
44
  raw.extraRules.forEach((r, i) => {
@@ -63,7 +64,7 @@ export const gateSection = defineSection<"gate", GateConfig>({
63
64
  });
64
65
  }
65
66
  return {
66
- disableDefaults: boolOr(raw.disableDefaults, DEFAULTS.disableDefaults),
67
+ guardrails: enumOr(raw.guardrails, ["on", "off"], DEFAULTS.guardrails),
67
68
  autoApprove: strArr(raw.autoApprove),
68
69
  extraRules,
69
70
  };