@xynogen/pix-runtime 0.5.1 → 0.5.3
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 +3 -2
- package/package.json +1 -1
- package/src/pix-command.ts +65 -30
- package/src/runtime.ts +14 -2
- package/src/sections/compaction.ts +23 -4
package/README.md
CHANGED
|
@@ -58,8 +58,9 @@ to calculate the trigger; the default is `60` and `0` disables the self-trigger
|
|
|
58
58
|
`compaction.minimumTokens` is the absolute floor for that calculation. The
|
|
59
59
|
effective threshold is `max(contextWindow × triggerPercent, minimumTokens)`, so
|
|
60
60
|
a 300K-context model at 10% waits for 100K tokens instead of compacting at 30K.
|
|
61
|
-
The default floor is 100K
|
|
62
|
-
600K
|
|
61
|
+
The default floor is 100K and the hard minimum is 25K (values below clamp up);
|
|
62
|
+
`/pix` offers 25K, 50K, 100K, 150K, 200K, 300K, 400K, 600K, 800K, and 1M.
|
|
63
|
+
pix-core consumes both settings.
|
|
63
64
|
|
|
64
65
|
Collapse policy helpers:
|
|
65
66
|
|
package/package.json
CHANGED
package/src/pix-command.ts
CHANGED
|
@@ -37,6 +37,23 @@ function row<T>(r: SettingRow<T>): SettingRow<unknown> {
|
|
|
37
37
|
return r as SettingRow<unknown>;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** Render a token count as a compact label: 1_000_000 → "1M", 150_000 → "150k". */
|
|
41
|
+
function formatTokens(tokens: number): string {
|
|
42
|
+
return tokens % 1_000_000 === 0 ? `${tokens / 1_000_000}M` : `${tokens / 1000}k`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Parse a compact token label ("1M", "150k") back to an absolute count. */
|
|
46
|
+
function parseTokens(label: string): number {
|
|
47
|
+
const raw = label.trim();
|
|
48
|
+
const m = raw.match(/^([0-9]*\.?[0-9]+)\s*([km])?$/i);
|
|
49
|
+
if (!m) return Number.NaN;
|
|
50
|
+
const numStr = m[1] ?? "";
|
|
51
|
+
const n = Number.parseFloat(numStr);
|
|
52
|
+
if (!Number.isFinite(n)) return Number.NaN;
|
|
53
|
+
const suf = (m[2] ?? "").toLowerCase();
|
|
54
|
+
return suf === "m" ? Math.round(n * 1_000_000) : Math.round(n * 1000);
|
|
55
|
+
}
|
|
56
|
+
|
|
40
57
|
const SETTINGS: SettingRow<unknown>[] = [
|
|
41
58
|
row({
|
|
42
59
|
section: "Pretty",
|
|
@@ -82,7 +99,8 @@ const SETTINGS: SettingRow<unknown>[] = [
|
|
|
82
99
|
section: "Compaction",
|
|
83
100
|
label: "Trigger (% ctx)",
|
|
84
101
|
handle: compactionSection,
|
|
85
|
-
|
|
102
|
+
// 0–100 in 5% steps.
|
|
103
|
+
values: Array.from({ length: 21 }, (_, i) => String(i * 5)),
|
|
86
104
|
read: (v) => String(v.triggerPercent),
|
|
87
105
|
patch: (value) => ({ triggerPercent: Number(value) }),
|
|
88
106
|
}),
|
|
@@ -90,9 +108,9 @@ const SETTINGS: SettingRow<unknown>[] = [
|
|
|
90
108
|
section: "Compaction",
|
|
91
109
|
label: "Minimum tokens",
|
|
92
110
|
handle: compactionSection,
|
|
93
|
-
values: ["100k", "150k", "200k", "300k", "400k", "
|
|
94
|
-
read: (v) =>
|
|
95
|
-
patch: (value) => ({ minimumTokens:
|
|
111
|
+
values: ["25k", "50k", "100k", "150k", "200k", "300k", "400k", "600k", "800k", "1M"],
|
|
112
|
+
read: (v) => formatTokens(v.minimumTokens),
|
|
113
|
+
patch: (value) => ({ minimumTokens: parseTokens(value) }),
|
|
96
114
|
}),
|
|
97
115
|
row({
|
|
98
116
|
section: "Gate",
|
|
@@ -114,7 +132,7 @@ function buildSummary(runtime: PixRuntime): string {
|
|
|
114
132
|
lastSection = row.section;
|
|
115
133
|
}
|
|
116
134
|
const value = row.read(runtime.get(row.handle));
|
|
117
|
-
const isDefault = value === row.
|
|
135
|
+
const isDefault = value === row.read(row.handle.defaults);
|
|
118
136
|
lines.push(` ${row.label}: ${value}${isDefault ? "" : " *"}`);
|
|
119
137
|
}
|
|
120
138
|
return lines.join("\n");
|
|
@@ -124,6 +142,8 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
124
142
|
pi.registerCommand("pix", {
|
|
125
143
|
description: "pix: open shared settings (edit pix.json)",
|
|
126
144
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
145
|
+
// Re-parse pix.json so section parsers remain the single validation source.
|
|
146
|
+
await runtime.reload({ origin: "command", source: "pix" });
|
|
127
147
|
const ui = ctx.ui as unknown as {
|
|
128
148
|
custom?: (f: unknown, opts?: unknown) => Promise<unknown>;
|
|
129
149
|
notify(m: string, t?: "info" | "warning" | "error"): void;
|
|
@@ -158,11 +178,29 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
158
178
|
const cycle = (direction: -1 | 1) => {
|
|
159
179
|
const row = SETTINGS[selected];
|
|
160
180
|
if (!row) return;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
181
|
+
// Functional updater: the runtime queue serializes writes, and the
|
|
182
|
+
// callback sees the latest committed value — so rapid presses each
|
|
183
|
+
// advance exactly one step instead of collapsing on a stale snapshot.
|
|
184
|
+
const step = (current: unknown): unknown => {
|
|
185
|
+
const curVal = row.read(current);
|
|
186
|
+
let cur = row.values.indexOf(curVal);
|
|
187
|
+
// ponytail: unsupported custom values snap to section default on first change.
|
|
188
|
+
if (cur === -1) {
|
|
189
|
+
const def = row.read(row.handle.defaults);
|
|
190
|
+
const defIdx = row.values.indexOf(def);
|
|
191
|
+
cur = defIdx !== -1 ? defIdx - direction : -1;
|
|
192
|
+
}
|
|
193
|
+
const next = (cur + direction + row.values.length) % row.values.length;
|
|
194
|
+
const val = row.values[next];
|
|
195
|
+
if (val === undefined) return current;
|
|
196
|
+
return { ...(current as object), ...(row.patch(val) as object) };
|
|
197
|
+
};
|
|
198
|
+
// Re-render AFTER the commit lands; the synchronous render in
|
|
199
|
+
// handleInput fires before the queued write and shows a stale value.
|
|
200
|
+
void runtime
|
|
201
|
+
.update(row.handle, step, { origin: "command", source: "pix" })
|
|
202
|
+
.then(() => tui.requestRender())
|
|
203
|
+
.catch(() => ui.notify(`pix: failed to update ${row.label}`, "error"));
|
|
166
204
|
};
|
|
167
205
|
const move = (direction: -1 | 1) => {
|
|
168
206
|
selected = (selected + direction + SETTINGS.length) % SETTINGS.length;
|
|
@@ -176,7 +214,8 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
176
214
|
const settingBodyLines: number[] = [];
|
|
177
215
|
let lastSection = "";
|
|
178
216
|
for (let i = 0; i < SETTINGS.length; i++) {
|
|
179
|
-
const row = SETTINGS[i]
|
|
217
|
+
const row = SETTINGS[i];
|
|
218
|
+
if (!row) continue;
|
|
180
219
|
if (row.section !== lastSection) {
|
|
181
220
|
if (lastSection) body.push("");
|
|
182
221
|
body.push(theme.fg("dim", ` ${row.section}`));
|
|
@@ -186,7 +225,7 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
186
225
|
const cursor = sel ? theme.fg("accent", "→") : " ";
|
|
187
226
|
const label = theme.fg(sel ? "accent" : "text", row.label.padEnd(labelW));
|
|
188
227
|
const value = row.read(runtime.get(row.handle));
|
|
189
|
-
const isDefault = value === row.
|
|
228
|
+
const isDefault = value === row.read(row.handle.defaults);
|
|
190
229
|
settingBodyLines[i] = body.length;
|
|
191
230
|
body.push(`${cursor} ${label} ${theme.fg(isDefault ? "dim" : "success", value)}`);
|
|
192
231
|
}
|
|
@@ -230,20 +269,15 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
|
|
|
230
269
|
// matchesKey handles both legacy bytes and Kitty CSI-u encodings
|
|
231
270
|
// for letters and special keys alike — raw string compares like
|
|
232
271
|
// `data === "k"` silently fail under the Kitty keyboard protocol.
|
|
233
|
-
if (
|
|
234
|
-
else if (matchesKey(data, "j") || matchesKey(data, "down")) move(1);
|
|
235
|
-
else if (matchesKey(data, "h") || matchesKey(data, "left")) cycle(-1);
|
|
236
|
-
else if (
|
|
237
|
-
matchesKey(data, "l") ||
|
|
238
|
-
matchesKey(data, "right") ||
|
|
239
|
-
matchesKey(data, "space") ||
|
|
240
|
-
matchesKey(data, "enter")
|
|
241
|
-
)
|
|
242
|
-
cycle(1);
|
|
243
|
-
else if (matchesKey(data, "escape") || matchesKey(data, "q")) {
|
|
272
|
+
if (kb.matches(data, "tui.select.cancel")) {
|
|
244
273
|
done(null);
|
|
245
274
|
return;
|
|
246
|
-
}
|
|
275
|
+
}
|
|
276
|
+
if (kb.matches(data, "tui.select.up")) move(-1);
|
|
277
|
+
else if (kb.matches(data, "tui.select.down")) move(1);
|
|
278
|
+
else if (matchesKey(data, Key.left)) cycle(-1);
|
|
279
|
+
else if (matchesKey(data, Key.right)) cycle(1);
|
|
280
|
+
else return;
|
|
247
281
|
tui.requestRender();
|
|
248
282
|
},
|
|
249
283
|
};
|
|
@@ -346,12 +380,13 @@ function frameRows(
|
|
|
346
380
|
const dashes = "─".repeat(width - 2);
|
|
347
381
|
const SENTINEL = "\x00";
|
|
348
382
|
const bgOpen = bg(SENTINEL).split(SENTINEL)[0] ?? "";
|
|
349
|
-
const reassert = (s: string): string =>
|
|
350
|
-
bgOpen
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
383
|
+
const reassert = (s: string): string => {
|
|
384
|
+
if (!bgOpen) return s;
|
|
385
|
+
return s.replace(/\x1b\[([0-9;]*)m/g, (seq, p: string) => {
|
|
386
|
+
if (p === "0" || p.split(";").includes("49")) return `${seq}${bgOpen}`;
|
|
387
|
+
return seq;
|
|
388
|
+
});
|
|
389
|
+
};
|
|
355
390
|
const row = (content: string): string => {
|
|
356
391
|
const fitted = truncateToWidth(content, inner, "…");
|
|
357
392
|
const padded = fitted + " ".repeat(Math.max(0, inner - visibleWidth(fitted)));
|
package/src/runtime.ts
CHANGED
|
@@ -347,12 +347,24 @@ class RuntimeImpl implements PixRuntime {
|
|
|
347
347
|
const values = new Map<string, unknown>();
|
|
348
348
|
for (const s of this.registry.all()) values.set(s.key, s.parse(base[s.key], ctx));
|
|
349
349
|
|
|
350
|
-
|
|
351
|
-
|
|
350
|
+
// Guard against a version-skewed second runtime copy whose registry
|
|
351
|
+
// lacks this section (e.g. an old npm copy handling a new handle). The
|
|
352
|
+
// functional updater would otherwise crash the whole process on
|
|
353
|
+
// `undefined`. Fall back to the handle's own parse of the raw doc.
|
|
354
|
+
const currentValue = (
|
|
355
|
+
values.has(section.key)
|
|
356
|
+
? values.get(section.key)
|
|
357
|
+
: section.__section.parse(base[section.key], ctx)
|
|
358
|
+
) as Readonly<T>;
|
|
359
|
+
const mergedValue =
|
|
352
360
|
typeof updater === "function"
|
|
353
361
|
? (updater as (c: Readonly<T>) => T)(currentValue)
|
|
354
362
|
: deepMerge(currentValue as T, updater);
|
|
355
363
|
|
|
364
|
+
// Re-validate through the section's own parse so a bad patch (NaN,
|
|
365
|
+
// Infinity, wrong type) can never enter the live snapshot.
|
|
366
|
+
const nextValue = section.__section.parse(mergedValue, ctx) as T;
|
|
367
|
+
|
|
356
368
|
if (JSON.stringify(currentValue) === JSON.stringify(nextValue)) return undefined;
|
|
357
369
|
values.set(section.key, nextValue);
|
|
358
370
|
|
|
@@ -14,11 +14,12 @@ export interface CompactionConfig {
|
|
|
14
14
|
minimumTokens: number;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const MINIMUM_TOKEN_FLOOR =
|
|
17
|
+
const MINIMUM_TOKEN_FLOOR = 25_000;
|
|
18
|
+
const MINIMUM_TOKEN_DEFAULT = 100_000;
|
|
18
19
|
|
|
19
20
|
const DEFAULTS: Readonly<CompactionConfig> = {
|
|
20
21
|
triggerPercent: 60,
|
|
21
|
-
minimumTokens:
|
|
22
|
+
minimumTokens: MINIMUM_TOKEN_DEFAULT,
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
/** Clamp to [0, 100]; non-numbers fall back. */
|
|
@@ -27,20 +28,38 @@ function pctOr(v: unknown, fallback: number): number {
|
|
|
27
28
|
return Math.min(100, Math.max(0, v));
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
/** Require a finite token floor of at least
|
|
31
|
+
/** Require a finite token floor of at least 25k. */
|
|
31
32
|
function minimumTokensOr(v: unknown, fallback: number): number {
|
|
32
33
|
if (typeof v !== "number" || !Number.isFinite(v)) return fallback;
|
|
34
|
+
// Strings from older/hand-edited pix.json (e.g. "150k") → parse before clamping.
|
|
33
35
|
return Math.max(MINIMUM_TOKEN_FLOOR, Math.round(v));
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
function parseTokenString(label: string): number | undefined {
|
|
39
|
+
const raw = label.trim();
|
|
40
|
+
if (!raw) return undefined;
|
|
41
|
+
const m = raw.match(/^([0-9]*\.?[0-9]+)\s*([km])?$/i);
|
|
42
|
+
if (!m) return undefined;
|
|
43
|
+
const numStr = m[1] ?? "";
|
|
44
|
+
const n = Number.parseFloat(numStr);
|
|
45
|
+
if (!Number.isFinite(n)) return undefined;
|
|
46
|
+
const suf = (m[2] ?? "").toLowerCase();
|
|
47
|
+
if (suf === "m") return Math.round(n * 1_000_000);
|
|
48
|
+
if (suf === "k") return Math.round(n * 1000);
|
|
49
|
+
return Math.round(n);
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
export const compactionSection = defineSection<"compaction", CompactionConfig>({
|
|
37
53
|
key: "compaction",
|
|
38
54
|
defaults: DEFAULTS,
|
|
39
55
|
parse(raw) {
|
|
40
56
|
if (!isObj(raw)) return { ...DEFAULTS };
|
|
57
|
+
const rawMin = raw.minimumTokens;
|
|
58
|
+
const coercedMin =
|
|
59
|
+
typeof rawMin === "string" ? (parseTokenString(rawMin) ?? Number.NaN) : rawMin;
|
|
41
60
|
return {
|
|
42
61
|
triggerPercent: pctOr(raw.triggerPercent, DEFAULTS.triggerPercent),
|
|
43
|
-
minimumTokens: minimumTokensOr(
|
|
62
|
+
minimumTokens: minimumTokensOr(coercedMin, DEFAULTS.minimumTokens),
|
|
44
63
|
};
|
|
45
64
|
},
|
|
46
65
|
});
|