@xynogen/pix-runtime 0.3.0 → 0.4.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/README.md +10 -1
- package/package.json +2 -1
- package/src/index.ts +3 -0
- package/src/io.ts +11 -0
- package/src/pix-command.ts +138 -33
- package/src/sections/index.ts +3 -0
- package/src/sections/io.ts +21 -0
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ See `DESIGN.md` for the full contract.
|
|
|
10
10
|
## What it does
|
|
11
11
|
|
|
12
12
|
- Versioned, sparse `pix.json` (`$version: 1`) — defaults resolve in code.
|
|
13
|
-
- Typed sections: `collapse`, `pretty`, `optimizer`, `gate`.
|
|
13
|
+
- Typed sections: `collapse`, `pretty`, `io`, `optimizer`, `gate`.
|
|
14
14
|
- Atomic writes behind a serialized in-process queue and a short-lived
|
|
15
15
|
cross-process lock. A failed write leaves the old file intact.
|
|
16
16
|
- Immutable, deeply frozen config snapshots with a monotonic revision.
|
|
@@ -38,8 +38,17 @@ import { prettySection } from "@xynogen/pix-runtime/sections";
|
|
|
38
38
|
const icons = config(prettySection).icons; // synchronous read
|
|
39
39
|
await updateConfig(prettySection, { icons: "ascii" });
|
|
40
40
|
const off = onConfigChange((c) => render(), { paths: ["pretty.icons"] });
|
|
41
|
+
|
|
42
|
+
import { ioTimeoutMs, ioTimeoutSignal } from "@xynogen/pix-runtime/io";
|
|
43
|
+
const timeoutMs = ioTimeoutMs(); // shared network timeout
|
|
44
|
+
const signal = ioTimeoutSignal(toolSignal); // timeout + cancellation
|
|
41
45
|
```
|
|
42
46
|
|
|
47
|
+
Set `io.timeoutSec` in `~/.pi/agent/pix.json`, or change **Network → timeout (sec)**
|
|
48
|
+
with `/pix`. The default is 30 seconds. It applies to Pix network operations,
|
|
49
|
+
including remote skills, web fetch/search/transcription, MCP requests and
|
|
50
|
+
connection bootstrap, background model-data refreshes, and update downloads.
|
|
51
|
+
|
|
43
52
|
Collapse policy helpers:
|
|
44
53
|
|
|
45
54
|
```ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"./config": "./src/runtime.ts",
|
|
25
25
|
"./sections": "./src/sections/index.ts",
|
|
26
26
|
"./collapse": "./src/collapse.ts",
|
|
27
|
+
"./io": "./src/io.ts",
|
|
27
28
|
"./once": "./src/once.ts",
|
|
28
29
|
"./testing": "./src/testing.ts"
|
|
29
30
|
},
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type {
|
|
|
7
7
|
SubscribeOptions,
|
|
8
8
|
} from "./events.ts";
|
|
9
9
|
export { default } from "./extension.ts";
|
|
10
|
+
export { ioTimeoutMs, ioTimeoutSignal } from "./io.ts";
|
|
10
11
|
export {
|
|
11
12
|
config,
|
|
12
13
|
createRuntime,
|
|
@@ -34,6 +35,8 @@ export {
|
|
|
34
35
|
type GateConfig,
|
|
35
36
|
type GateRuleConfig,
|
|
36
37
|
gateSection,
|
|
38
|
+
type IoConfig,
|
|
39
|
+
ioSection,
|
|
37
40
|
type OptimizerConfig,
|
|
38
41
|
optimizerSection,
|
|
39
42
|
type PrettyConfig,
|
package/src/io.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { config } from "./runtime.ts";
|
|
2
|
+
import { ioSection } from "./sections/io.ts";
|
|
3
|
+
|
|
4
|
+
export function ioTimeoutMs(): number {
|
|
5
|
+
return config(ioSection).timeoutSec * 1000;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function ioTimeoutSignal(signal?: AbortSignal): AbortSignal {
|
|
9
|
+
const timeout = AbortSignal.timeout(ioTimeoutMs());
|
|
10
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
11
|
+
}
|
package/src/pix-command.ts
CHANGED
|
@@ -7,11 +7,19 @@
|
|
|
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";
|
|
22
|
+
import { ioSection } from "./sections/io.ts";
|
|
15
23
|
import { prettySection } from "./sections/pretty.ts";
|
|
16
24
|
|
|
17
25
|
interface SettingRow<T> {
|
|
@@ -61,6 +69,14 @@ const SETTINGS: SettingRow<unknown>[] = [
|
|
|
61
69
|
read: (v) => String(v.delaySec),
|
|
62
70
|
patch: (value) => ({ delaySec: Number(value) }),
|
|
63
71
|
}),
|
|
72
|
+
row({
|
|
73
|
+
section: "Network",
|
|
74
|
+
label: "timeout (sec)",
|
|
75
|
+
handle: ioSection,
|
|
76
|
+
values: ["30", "10", "60", "120", "300"],
|
|
77
|
+
read: (v) => String(v.timeoutSec),
|
|
78
|
+
patch: (value) => ({ timeoutSec: Number(value) }),
|
|
79
|
+
}),
|
|
64
80
|
row({
|
|
65
81
|
section: "Gate",
|
|
66
82
|
label: "Guardrails",
|
|
@@ -104,16 +120,20 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
104
120
|
const boxW = 52;
|
|
105
121
|
await ui.custom(
|
|
106
122
|
(
|
|
107
|
-
tui: { requestRender(): void },
|
|
123
|
+
tui: { requestRender(): void; terminal?: { rows?: number } },
|
|
108
124
|
theme: {
|
|
109
125
|
fg(c: string, t: string): string;
|
|
110
126
|
bg(c: string, t: string): string;
|
|
111
127
|
bold(t: string): string;
|
|
112
128
|
},
|
|
113
|
-
|
|
129
|
+
kb: KeybindingsManager,
|
|
114
130
|
done: (v: null) => void,
|
|
115
131
|
) => {
|
|
116
132
|
let selected = 0;
|
|
133
|
+
let bodyOffset = 0;
|
|
134
|
+
let visibleBodyLines = 1;
|
|
135
|
+
let maxBodyOffset = 0;
|
|
136
|
+
let inspectingPage = false;
|
|
117
137
|
|
|
118
138
|
const cycle = (direction: -1 | 1) => {
|
|
119
139
|
const row = SETTINGS[selected];
|
|
@@ -126,18 +146,20 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
126
146
|
};
|
|
127
147
|
const move = (direction: -1 | 1) => {
|
|
128
148
|
selected = (selected + direction + SETTINGS.length) % SETTINGS.length;
|
|
149
|
+
inspectingPage = false;
|
|
129
150
|
};
|
|
130
151
|
|
|
131
152
|
return {
|
|
132
153
|
render: () => {
|
|
133
154
|
const labelW = Math.max(...SETTINGS.map((r) => r.label.length));
|
|
134
|
-
const
|
|
155
|
+
const body: string[] = [];
|
|
156
|
+
const settingBodyLines: number[] = [];
|
|
135
157
|
let lastSection = "";
|
|
136
158
|
for (let i = 0; i < SETTINGS.length; i++) {
|
|
137
159
|
const row = SETTINGS[i]!;
|
|
138
160
|
if (row.section !== lastSection) {
|
|
139
|
-
if (lastSection)
|
|
140
|
-
|
|
161
|
+
if (lastSection) body.push("");
|
|
162
|
+
body.push(theme.fg("dim", ` ${row.section}`));
|
|
141
163
|
lastSection = row.section;
|
|
142
164
|
}
|
|
143
165
|
const sel = i === selected;
|
|
@@ -145,19 +167,40 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
145
167
|
const label = theme.fg(sel ? "accent" : "text", row.label.padEnd(labelW));
|
|
146
168
|
const value = row.read(runtime.get(row.handle));
|
|
147
169
|
const isDefault = value === row.values[0];
|
|
148
|
-
|
|
170
|
+
settingBodyLines[i] = body.length;
|
|
171
|
+
body.push(`${cursor} ${label} ${theme.fg(isDefault ? "dim" : "success", value)}`);
|
|
149
172
|
}
|
|
150
|
-
|
|
151
|
-
lines.push(theme.fg("dim", "←→ change · ↑↓ move · esc close"));
|
|
152
|
-
return frameLines({
|
|
173
|
+
const result = frameModal({
|
|
153
174
|
width: boxW,
|
|
154
|
-
|
|
175
|
+
maxHeight: modalHeight(tui.terminal?.rows),
|
|
176
|
+
header: [theme.fg("accent", theme.bold(" pix settings")), ""],
|
|
177
|
+
body,
|
|
178
|
+
footer: [
|
|
179
|
+
"",
|
|
180
|
+
theme.fg("dim", "←→ change · ↑↓ move · PgUp/PgDn inspect · esc close"),
|
|
181
|
+
],
|
|
182
|
+
bodyOffset,
|
|
183
|
+
selectedBodyLine: inspectingPage ? undefined : settingBodyLines[selected],
|
|
155
184
|
color: (s: string) => theme.fg("accent", s),
|
|
156
185
|
bg: (s: string) => theme.bg("customMessageBg", s),
|
|
157
186
|
});
|
|
187
|
+
bodyOffset = result.bodyOffset;
|
|
188
|
+
visibleBodyLines = Math.max(1, result.visibleBodyLines);
|
|
189
|
+
maxBodyOffset = result.maxBodyOffset;
|
|
190
|
+
return result.lines;
|
|
158
191
|
},
|
|
159
192
|
invalidate: () => {},
|
|
160
193
|
handleInput: (data: string) => {
|
|
194
|
+
const pageUp = kb?.matches(data, "tui.select.pageUp") || matchesKey(data, Key.pageUp);
|
|
195
|
+
const pageDown =
|
|
196
|
+
kb?.matches(data, "tui.select.pageDown") || matchesKey(data, Key.pageDown);
|
|
197
|
+
if (pageUp || pageDown) {
|
|
198
|
+
const direction = pageUp ? -1 : 1;
|
|
199
|
+
bodyOffset = pageBodyOffset(bodyOffset, visibleBodyLines, maxBodyOffset, direction);
|
|
200
|
+
inspectingPage = true;
|
|
201
|
+
tui.requestRender();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
161
204
|
// matchesKey handles both legacy bytes and Kitty CSI-u encodings
|
|
162
205
|
// for letters and special keys alike — raw string compares like
|
|
163
206
|
// `data === "k"` silently fail under the Kitty keyboard protocol.
|
|
@@ -185,29 +228,96 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
185
228
|
});
|
|
186
229
|
}
|
|
187
230
|
|
|
188
|
-
// ── Inline
|
|
231
|
+
// ── Inline modal engine (runtime cannot depend on pix-pretty: pretty depends on runtime) ─
|
|
189
232
|
|
|
190
|
-
interface
|
|
233
|
+
interface RuntimeModalOptions {
|
|
191
234
|
width: number;
|
|
192
|
-
|
|
235
|
+
maxHeight: number;
|
|
236
|
+
header: string[];
|
|
237
|
+
body: string[];
|
|
238
|
+
footer: string[];
|
|
239
|
+
bodyOffset: number;
|
|
240
|
+
selectedBodyLine?: number;
|
|
193
241
|
color: (s: string) => string;
|
|
194
242
|
bg?: (s: string) => string;
|
|
195
243
|
}
|
|
196
244
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
245
|
+
interface RuntimeModalResult {
|
|
246
|
+
lines: string[];
|
|
247
|
+
bodyOffset: number;
|
|
248
|
+
visibleBodyLines: number;
|
|
249
|
+
maxBodyOffset: number;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function modalHeight(rows = 24): number {
|
|
253
|
+
const safe = Number.isFinite(rows) ? Math.max(1, Math.floor(rows)) : 24;
|
|
254
|
+
return Math.max(1, Math.min(safe, Math.floor(safe * 0.8)));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function pageBodyOffset(offset: number, size: number, max: number, direction: -1 | 1): number {
|
|
258
|
+
const s = Math.max(1, size);
|
|
259
|
+
const step = Math.max(1, Math.floor(s / 2));
|
|
260
|
+
return Math.min(max, Math.max(0, offset + direction * step));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function fitLines(lines: string[], width: number): string[] {
|
|
264
|
+
const rows: string[] = [];
|
|
265
|
+
for (const line of lines) {
|
|
266
|
+
if (line === "") rows.push("");
|
|
267
|
+
else rows.push(...wrapTextWithAnsi(line, Math.max(1, width)));
|
|
268
|
+
}
|
|
269
|
+
return rows;
|
|
200
270
|
}
|
|
201
271
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
272
|
+
function frameModal(opts: RuntimeModalOptions): RuntimeModalResult {
|
|
273
|
+
const inner = Math.max(1, opts.width - 4);
|
|
274
|
+
const header = fitLines(opts.header, inner);
|
|
275
|
+
const footer = fitLines(opts.footer, inner);
|
|
276
|
+
const body = fitLines(opts.body, inner);
|
|
277
|
+
const cap = Math.max(1, opts.maxHeight);
|
|
278
|
+
const bodyBudget = cap - 2 - header.length - footer.length;
|
|
279
|
+
const overflows = body.length > Math.max(0, bodyBudget);
|
|
280
|
+
const visibleBodyLines = overflows ? Math.max(0, bodyBudget - 1) : Math.max(0, bodyBudget);
|
|
281
|
+
if (visibleBodyLines === 0 && body.length > 0) {
|
|
282
|
+
return {
|
|
283
|
+
lines: frameRows(opts.width, ["Terminal too short — resize or press esc to cancel"], opts),
|
|
284
|
+
bodyOffset: 0,
|
|
285
|
+
visibleBodyLines: 0,
|
|
286
|
+
maxBodyOffset: 0,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const maxBodyOffset = Math.max(0, body.length - visibleBodyLines);
|
|
290
|
+
let offset = Math.min(maxBodyOffset, Math.max(0, opts.bodyOffset));
|
|
291
|
+
if (opts.selectedBodyLine !== undefined) {
|
|
292
|
+
const selected = Math.min(body.length - 1, Math.max(0, opts.selectedBodyLine));
|
|
293
|
+
if (selected < offset) offset = selected;
|
|
294
|
+
else if (selected >= offset + visibleBodyLines) offset = selected - visibleBodyLines + 1;
|
|
295
|
+
}
|
|
296
|
+
const end = Math.min(body.length, offset + visibleBodyLines);
|
|
297
|
+
const lines = [...header];
|
|
298
|
+
if (overflows) {
|
|
299
|
+
const step = Math.max(1, Math.floor(visibleBodyLines / 2));
|
|
300
|
+
const totalPages = Math.max(1, Math.ceil(maxBodyOffset / step) + 1);
|
|
301
|
+
const page = offset >= maxBodyOffset ? totalPages : Math.floor(offset / step) + 1;
|
|
302
|
+
lines.push(`PgUp/PgDn inspect • ${page}/${totalPages}`);
|
|
303
|
+
}
|
|
304
|
+
lines.push(...body.slice(offset, end), ...footer);
|
|
305
|
+
return {
|
|
306
|
+
lines: frameRows(opts.width, lines, opts),
|
|
307
|
+
bodyOffset: offset,
|
|
308
|
+
visibleBodyLines,
|
|
309
|
+
maxBodyOffset,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function frameRows(
|
|
314
|
+
width: number,
|
|
315
|
+
lines: string[],
|
|
316
|
+
opts: Pick<RuntimeModalOptions, "color" | "bg">,
|
|
317
|
+
): string[] {
|
|
205
318
|
const bg = opts.bg ?? ((s: string) => s);
|
|
206
|
-
const inner = Math.max(1, width - 4);
|
|
319
|
+
const inner = Math.max(1, width - 4);
|
|
207
320
|
const dashes = "─".repeat(width - 2);
|
|
208
|
-
|
|
209
|
-
// Re-assert the bg OPEN sequence after any full reset (\x1b[0m) or bg reset
|
|
210
|
-
// (\x1b[49m) embedded in themed content, so the fill has no transparent holes.
|
|
211
321
|
const SENTINEL = "\x00";
|
|
212
322
|
const bgOpen = bg(SENTINEL).split(SENTINEL)[0] ?? "";
|
|
213
323
|
const reassert = (s: string): string =>
|
|
@@ -216,15 +326,10 @@ function frameLines(opts: FrameOptions): string[] {
|
|
|
216
326
|
p === "0" || p.split(";").includes("49") ? `${seq}${bgOpen}` : seq,
|
|
217
327
|
)
|
|
218
328
|
: s;
|
|
219
|
-
|
|
220
329
|
const row = (content: string): string => {
|
|
221
|
-
const
|
|
222
|
-
const padded =
|
|
223
|
-
return bg(`${color("│")} ${reassert(padded)} ${color("│")}`);
|
|
330
|
+
const fitted = truncateToWidth(content, inner, "…");
|
|
331
|
+
const padded = fitted + " ".repeat(Math.max(0, inner - visibleWidth(fitted)));
|
|
332
|
+
return bg(`${opts.color("│")} ${reassert(padded)} ${opts.color("│")}`);
|
|
224
333
|
};
|
|
225
|
-
|
|
226
|
-
const out: string[] = [bg(color(`╭${dashes}╮`))];
|
|
227
|
-
for (const line of lines) out.push(row(line));
|
|
228
|
-
out.push(bg(color(`╰${dashes}╯`)));
|
|
229
|
-
return out;
|
|
334
|
+
return [bg(opts.color(`╭${dashes}╮`)), ...lines.map(row), bg(opts.color(`╰${dashes}╯`))];
|
|
230
335
|
}
|
package/src/sections/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export {
|
|
|
5
5
|
type GateSeverity,
|
|
6
6
|
gateSection,
|
|
7
7
|
} from "./gate.ts";
|
|
8
|
+
export { type IoConfig, ioSection } from "./io.ts";
|
|
8
9
|
export {
|
|
9
10
|
type CavemanLevel,
|
|
10
11
|
type OptimizerConfig,
|
|
@@ -22,6 +23,7 @@ export {
|
|
|
22
23
|
|
|
23
24
|
import { collapseSection } from "./collapse.ts";
|
|
24
25
|
import { gateSection } from "./gate.ts";
|
|
26
|
+
import { ioSection } from "./io.ts";
|
|
25
27
|
import { optimizerSection } from "./optimizer.ts";
|
|
26
28
|
import { prettySection } from "./pretty.ts";
|
|
27
29
|
|
|
@@ -29,6 +31,7 @@ import { prettySection } from "./pretty.ts";
|
|
|
29
31
|
export const builtinSections = [
|
|
30
32
|
collapseSection,
|
|
31
33
|
prettySection,
|
|
34
|
+
ioSection,
|
|
32
35
|
optimizerSection,
|
|
33
36
|
gateSection,
|
|
34
37
|
] as const;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineSection, isObj, posNumOr } from "../schema.ts";
|
|
2
|
+
|
|
3
|
+
export interface IoConfig {
|
|
4
|
+
/** Default timeout in seconds for Pix network requests. */
|
|
5
|
+
timeoutSec: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const DEFAULTS: Readonly<IoConfig> = {
|
|
9
|
+
timeoutSec: 30,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const ioSection = defineSection<"io", IoConfig>({
|
|
13
|
+
key: "io",
|
|
14
|
+
defaults: DEFAULTS,
|
|
15
|
+
parse(raw) {
|
|
16
|
+
if (!isObj(raw)) return { ...DEFAULTS };
|
|
17
|
+
return {
|
|
18
|
+
timeoutSec: posNumOr(raw.timeoutSec, DEFAULTS.timeoutSec),
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
});
|