@xynogen/pix-runtime 0.1.2 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-runtime",
3
- "version": "0.1.2",
3
+ "version": "0.3.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",
package/src/collapse.ts CHANGED
@@ -18,3 +18,34 @@ export function shouldCollapse(toolName: string): boolean {
18
18
  export function collapseDelayMs(): number {
19
19
  return pixRuntime().get(collapseSection).delaySec * 1000;
20
20
  }
21
+
22
+ /** Per-card render state bag for the collapse timer. */
23
+ export interface CollapseState {
24
+ collapsed?: boolean;
25
+ timer?: ReturnType<typeof setTimeout>;
26
+ }
27
+
28
+ /**
29
+ * Run the collapse timer for a tool card. Call this inside `renderResult`.
30
+ *
31
+ * @param toolName — the tool name (e.g. "bash", "read") for per-tool config
32
+ * @param state — the render context's `state` bag (mutable, per-card)
33
+ * @param invalidate — `context.invalidate()` to trigger re-render
34
+ * @param expanded — whether the host currently requests the detailed view
35
+ * @returns `true` if the card is currently collapsed and not expanded
36
+ */
37
+ export function tickCollapse(
38
+ toolName: string,
39
+ state: CollapseState,
40
+ invalidate: () => void,
41
+ expanded = false,
42
+ ): boolean {
43
+ if (!shouldCollapse(toolName)) return false;
44
+ if (!state.timer && !state.collapsed) {
45
+ state.timer = setTimeout(() => {
46
+ state.collapsed = true;
47
+ invalidate();
48
+ }, collapseDelayMs());
49
+ }
50
+ return state.collapsed === true && !expanded;
51
+ }
@@ -12,7 +12,6 @@ import type { PixRuntime } from "./runtime.ts";
12
12
  import type { DeepPartial, SectionHandle } from "./schema.ts";
13
13
  import { collapseSection } from "./sections/collapse.ts";
14
14
  import { gateSection } from "./sections/gate.ts";
15
- import { optimizerSection } from "./sections/optimizer.ts";
16
15
  import { prettySection } from "./sections/pretty.ts";
17
16
 
18
17
  interface SettingRow<T> {
@@ -62,29 +61,13 @@ const SETTINGS: SettingRow<unknown>[] = [
62
61
  read: (v) => String(v.delaySec),
63
62
  patch: (value) => ({ delaySec: Number(value) }),
64
63
  }),
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
64
  row({
82
65
  section: "Gate",
83
- label: "disable defaults",
66
+ label: "Guardrails",
84
67
  handle: gateSection,
85
- values: ["false", "true"],
86
- read: (v) => String(v.disableDefaults),
87
- patch: (value) => ({ disableDefaults: value === "true" }),
68
+ values: ["on", "off"],
69
+ read: (v) => v.guardrails,
70
+ patch: (guardrails) => ({ guardrails: guardrails as "on" | "off" }),
88
71
  }),
89
72
  ];
90
73
 
@@ -124,6 +107,7 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
124
107
  tui: { requestRender(): void },
125
108
  theme: {
126
109
  fg(c: string, t: string): string;
110
+ bg(c: string, t: string): string;
127
111
  bold(t: string): string;
128
112
  },
129
113
  _kb: unknown,
@@ -165,7 +149,12 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
165
149
  }
166
150
  lines.push("");
167
151
  lines.push(theme.fg("dim", "←→ change · ↑↓ move · esc close"));
168
- return lines;
152
+ return frameLines({
153
+ width: boxW,
154
+ lines,
155
+ color: (s: string) => theme.fg("accent", s),
156
+ bg: (s: string) => theme.bg("customMessageBg", s),
157
+ });
169
158
  },
170
159
  invalidate: () => {},
171
160
  handleInput: (data: string) => {
@@ -195,3 +184,47 @@ export function registerPixCommand(pi: ExtensionAPI, runtime: PixRuntime): void
195
184
  },
196
185
  });
197
186
  }
187
+
188
+ // ── Inline frameLines (runtime must not depend on pix-pretty — pretty depends on us) ─
189
+
190
+ interface FrameOptions {
191
+ width: number;
192
+ lines: string[];
193
+ color: (s: string) => string;
194
+ bg?: (s: string) => string;
195
+ }
196
+
197
+ function visibleWidth(s: string): number {
198
+ // Strip ANSI escape sequences for width calculation.
199
+ return s.replace(/\x1b\[[0-9;]*m/g, "").length;
200
+ }
201
+
202
+ /** Render a rounded bordered modal box (╭─╮╰─╯) with solid background fill. */
203
+ function frameLines(opts: FrameOptions): string[] {
204
+ const { width, lines, color } = opts;
205
+ const bg = opts.bg ?? ((s: string) => s);
206
+ const inner = Math.max(1, width - 4); // 2 border + 2 padding
207
+ 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
+ const SENTINEL = "\x00";
212
+ const bgOpen = bg(SENTINEL).split(SENTINEL)[0] ?? "";
213
+ const reassert = (s: string): string =>
214
+ bgOpen
215
+ ? s.replace(/\x1b\[([0-9;]*)m/g, (seq, p: string) =>
216
+ p === "0" || p.split(";").includes("49") ? `${seq}${bgOpen}` : seq,
217
+ )
218
+ : s;
219
+
220
+ const row = (content: string): string => {
221
+ const pad = inner - visibleWidth(content);
222
+ const padded = pad > 0 ? content + " ".repeat(pad) : content.slice(0, inner);
223
+ return bg(`${color("│")} ${reassert(padded)} ${color("│")}`);
224
+ };
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;
230
+ }
@@ -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
  };