@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 +1 -1
- package/src/pix-command.ts +160 -31
- package/src/sections/gate.ts +6 -5
package/package.json
CHANGED
package/src/pix-command.ts
CHANGED
|
@@ -7,12 +7,18 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import {
|
|
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: "
|
|
73
|
+
label: "Guardrails",
|
|
84
74
|
handle: gateSection,
|
|
85
|
-
values: ["
|
|
86
|
-
read: (v) =>
|
|
87
|
-
patch: (
|
|
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
|
-
|
|
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
|
|
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)
|
|
156
|
-
|
|
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
|
-
|
|
161
|
+
settingBodyLines[i] = body.length;
|
|
162
|
+
body.push(`${cursor} ${label} ${theme.fg(isDefault ? "dim" : "success", value)}`);
|
|
165
163
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
}
|
package/src/sections/gate.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
67
|
+
guardrails: enumOr(raw.guardrails, ["on", "off"], DEFAULTS.guardrails),
|
|
67
68
|
autoApprove: strArr(raw.autoApprove),
|
|
68
69
|
extraRules,
|
|
69
70
|
};
|