@xynogen/pix-commands 0.6.2 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-commands",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Pi extension — slash commands for cache clearing and isolated side questions",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "files": [
15
15
  "src",
16
+ "!src/**/*.test.*",
16
17
  "README.md",
17
18
  "LICENSE"
18
19
  ],
@@ -1,46 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { registerBtw, shortModelName } from "./index.ts";
3
-
4
- describe("BTW display helpers", () => {
5
- test("prefers model display name and falls back to id", () => {
6
- expect(shortModelName({ id: "id", name: "Friendly" })).toBe("Friendly");
7
- expect(shortModelName({ id: "id", name: " " })).toBe("id");
8
- });
9
-
10
- test("registers a display-only entry renderer, never a context-bearing message renderer", () => {
11
- let entryRenderer: string | undefined;
12
- let messageRenderer: string | undefined;
13
- const pi = {
14
- on() {},
15
- registerCommand() {},
16
- registerEntryRenderer(name: string) {
17
- entryRenderer = name;
18
- },
19
- registerMessageRenderer(name: string) {
20
- messageRenderer = name;
21
- },
22
- } as any;
23
- registerBtw(pi);
24
-
25
- // pix-btw-answer must be a CustomEntry (display-only, never in LLM context),
26
- // not a CustomMessageEntry — that is what lets the card land mid-stream.
27
- expect(entryRenderer).toBe("pix-btw-answer");
28
- expect(messageRenderer).toBeUndefined();
29
- });
30
-
31
- test("does not register a context handler (BTW cards never enter LLM context)", () => {
32
- const events: string[] = [];
33
- const pi = {
34
- on(event: string) {
35
- events.push(event);
36
- },
37
- registerCommand() {},
38
- registerEntryRenderer() {},
39
- } as any;
40
- registerBtw(pi);
41
-
42
- // A CustomEntry is ignored by buildSessionContext, so there is nothing to
43
- // strip — the old pi.on("context", filterBtwMessages) hack is gone.
44
- expect(events).not.toContain("context");
45
- });
46
- });
@@ -1,102 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { type ExtensionAPI, initTheme } from "@earendil-works/pi-coding-agent";
3
- import { type BtwMessageDetails, formatDuration, registerBtwRenderer } from "./render.ts";
4
-
5
- const stripAnsi = (text: string) => text.replace(/\x1b\[[0-9;]*m/g, "");
6
-
7
- function captureRenderer() {
8
- let renderer: ((entry: unknown, options: unknown, theme: unknown) => unknown) | undefined;
9
- const pi = {
10
- registerEntryRenderer(_name: string, fn: typeof renderer) {
11
- renderer = fn;
12
- },
13
- } as unknown as ExtensionAPI;
14
- registerBtwRenderer(pi);
15
- if (!renderer) throw new Error("renderer was not registered");
16
- return renderer;
17
- }
18
-
19
- const backgrounds: string[] = [];
20
- const theme = {
21
- fg: (_color: string, text: string) => text,
22
- bg: (color: string, text: string) => {
23
- backgrounds.push(color);
24
- return text;
25
- },
26
- bold: (text: string) => text,
27
- };
28
-
29
- function render(details: BtwMessageDetails, expanded = false): string {
30
- const renderer = captureRenderer();
31
- const component = renderer(
32
- { type: "custom", customType: "pix-btw-answer", data: details },
33
- { expanded },
34
- theme,
35
- ) as {
36
- render(width: number): string[];
37
- };
38
- return stripAnsi(component.render(80).join("\n"));
39
- }
40
-
41
- describe("BTW renderer", () => {
42
- test("formats durations compactly", () => {
43
- expect(formatDuration(450)).toBe("450ms");
44
- expect(formatDuration(2_100)).toBe("2.1s");
45
- expect(formatDuration(65_000)).toBe("1m 5s");
46
- });
47
-
48
- test("renders metadata and question as distinct side-thread card chrome", () => {
49
- backgrounds.length = 0;
50
- const output = render({
51
- question: "hello",
52
- answer: "Hi!",
53
- thinking: "",
54
- model: "GPT-5.6",
55
- thinkingLevel: "high",
56
- durationMs: 2_100,
57
- toolUses: 0,
58
- });
59
- expect(output).toContain("✓ BTW · GPT-5.6 · high · 2.1s");
60
- expect(output).toContain("▐ hello");
61
- expect(output).not.toContain("SIDE THREAD");
62
- expect(backgrounds).toContain("selectedBg");
63
- expect(backgrounds).not.toContain("customMessageBg");
64
- });
65
-
66
- test("renders the answer as Markdown", () => {
67
- initTheme();
68
- const output = render({
69
- question: "show markdown",
70
- answer: "## Heading\n\n- alpha\n- beta\n\n`code`",
71
- thinking: "",
72
- model: "Model",
73
- thinkingLevel: "medium",
74
- durationMs: 1_000,
75
- toolUses: 1,
76
- });
77
- expect(output).toContain("Heading");
78
- expect(output).toContain("alpha");
79
- expect(output).toContain("beta");
80
- expect(output).toContain("code");
81
- });
82
-
83
- test("hides reasoning by default and reveals it when expanded", () => {
84
- initTheme();
85
- const details: BtwMessageDetails = {
86
- question: "why",
87
- answer: "Because.",
88
- thinking: "first I considered the mutex",
89
- model: "Model",
90
- thinkingLevel: "high",
91
- durationMs: 1_000,
92
- toolUses: 0,
93
- };
94
- const collapsed = render(details, false);
95
- expect(collapsed).toContain("reasoning hidden");
96
- expect(collapsed).not.toContain("first I considered the mutex");
97
-
98
- const expanded = render(details, true);
99
- expect(expanded).toContain("Reasoning");
100
- expect(expanded).toContain("first I considered the mutex");
101
- });
102
- });
@@ -1,128 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { Api, Model } from "@earendil-works/pi-ai";
3
- import type { LoadExtensionsResult } from "@earendil-works/pi-coding-agent";
4
- import {
5
- BTW_CTX_TURNS,
6
- BTW_SYSTEM_PROMPT,
7
- buildContextPreamble,
8
- lastAssistantText,
9
- makeLeanExtensions,
10
- selectBtwTools,
11
- snapshotMainSettings,
12
- } from "./session.ts";
13
-
14
- describe("BTW system prompt", () => {
15
- test("is the exact lean Pix identity", () => {
16
- expect(BTW_SYSTEM_PROMPT).toBe(
17
- "You are Pix Coding Agent. You help users accomplish any task they request.",
18
- );
19
- });
20
- });
21
-
22
- describe("snapshotMainSettings", () => {
23
- test("captures model, thinking, cwd, and a defensive copy of active tools", () => {
24
- const tools = ["read", "fetch"];
25
- const model = { id: "model-id", name: "Model" } as Model<Api>;
26
- const snapshot = snapshotMainSettings({ cwd: "/project", model } as never, "high", tools);
27
- tools.push("write");
28
- expect(snapshot.cwd).toBe("/project");
29
- expect(snapshot.model).toBe(model);
30
- expect(snapshot.thinkingLevel).toBe("high");
31
- expect(snapshot.activeToolNames).toEqual(["read", "fetch"]);
32
- });
33
-
34
- test("rejects invocation when the main session has no model", () => {
35
- expect(() =>
36
- snapshotMainSettings({ cwd: "/project", model: undefined } as never, "medium", []),
37
- ).toThrow("No model is selected");
38
- });
39
- });
40
-
41
- describe("selectBtwTools", () => {
42
- test("uses the main active tools and removes duplicates without reordering", () => {
43
- expect(selectBtwTools(["read", "fetch", "read", "agent"])).toEqual(["read", "fetch", "agent"]);
44
- });
45
- });
46
-
47
- describe("makeLeanExtensions", () => {
48
- test("removes discovered before_agent_start mutators but preserves inline override", () => {
49
- const regularHandlers = new Map<string, never[]>([
50
- ["before_agent_start", []],
51
- ["tool_call", []],
52
- ]);
53
- const inlineHandlers = new Map<string, never[]>([["before_agent_start", []]]);
54
- const base = {
55
- extensions: [
56
- { path: "/extensions/pix-prompts.ts", handlers: regularHandlers },
57
- { path: "<inline:1>", handlers: inlineHandlers },
58
- ],
59
- errors: [],
60
- runtime: {},
61
- } as unknown as LoadExtensionsResult;
62
-
63
- const result = makeLeanExtensions(base);
64
- expect(result.extensions[0]?.handlers.has("before_agent_start")).toBe(false);
65
- expect(result.extensions[0]?.handlers.has("tool_call")).toBe(true);
66
- expect(result.extensions[1]?.handlers.has("before_agent_start")).toBe(true);
67
- // Do not mutate the loader's original extension records.
68
- expect(regularHandlers.has("before_agent_start")).toBe(true);
69
- });
70
- });
71
-
72
- describe("buildContextPreamble", () => {
73
- const msg = (role: string, text: string) => ({
74
- type: "message",
75
- message: { role, content: [{ type: "text", text }] },
76
- });
77
-
78
- test("flattens recent user/assistant turns into a labeled preamble", () => {
79
- const out = buildContextPreamble([msg("user", "hi"), msg("assistant", "hello")]);
80
- expect(out).toContain("User: hi");
81
- expect(out).toContain("Assistant: hello");
82
- expect(out).toContain("read-only context");
83
- });
84
-
85
- test("keeps only the last N turns", () => {
86
- const entries = Array.from({ length: BTW_CTX_TURNS + 5 }, (_, i) => msg("user", `q${i}`));
87
- const out = buildContextPreamble(entries);
88
- expect(out).not.toContain("q0"); // dropped
89
- expect(out).toContain(`q${BTW_CTX_TURNS + 4}`); // kept
90
- expect(out).toContain(`most recent ${BTW_CTX_TURNS} turn`);
91
- });
92
-
93
- test("skips non-message entries, tool noise, and string content", () => {
94
- const out = buildContextPreamble([
95
- { type: "model_change", message: undefined },
96
- { type: "message", message: { role: "tool", content: "ignored" } },
97
- { type: "message", message: { role: "user", content: "plain string" } },
98
- ]);
99
- expect(out).toContain("User: plain string");
100
- expect(out).not.toContain("ignored");
101
- });
102
-
103
- test("returns empty when there are no usable turns", () => {
104
- expect(buildContextPreamble([{ type: "compaction", message: undefined }])).toBe("");
105
- expect(buildContextPreamble([])).toBe("");
106
- });
107
- });
108
-
109
- describe("lastAssistantText", () => {
110
- test("returns text from the latest assistant response", () => {
111
- const messages = [
112
- { role: "assistant", content: [{ type: "text", text: "old" }] },
113
- { role: "user", content: "question" },
114
- {
115
- role: "assistant",
116
- content: [
117
- { type: "thinking", thinking: "hidden" },
118
- { type: "text", text: "latest" },
119
- ],
120
- },
121
- ];
122
- expect(lastAssistantText(messages)).toBe("latest");
123
- });
124
-
125
- test("returns an empty string when there is no assistant text", () => {
126
- expect(lastAssistantText([{ role: "user", content: "hello" }])).toBe("");
127
- });
128
- });
@@ -1,119 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import {
3
- type BtwWidgetJob,
4
- DEFAULT_LINGER_BASE_MS,
5
- ERROR_LINGER_SCALE,
6
- hasVisibleJobs,
7
- lingerWindows,
8
- OK_LINGER_SCALE,
9
- renderBtwWidget,
10
- shouldShowFinished,
11
- type WidgetTheme,
12
- } from "./widget.ts";
13
-
14
- const theme: WidgetTheme = {
15
- fg: (_color, text) => text,
16
- bold: (text) => text,
17
- };
18
-
19
- function job(overrides: Partial<BtwWidgetJob>): BtwWidgetJob {
20
- return {
21
- id: 1,
22
- model: "Model",
23
- status: "running",
24
- startedAt: 0,
25
- activeTools: [],
26
- text: "",
27
- toolUses: 0,
28
- turnCount: 0,
29
- outputTokens: 0,
30
- contextUsage: null,
31
- ...overrides,
32
- };
33
- }
34
-
35
- const render = (jobs: BtwWidgetJob[], now = 1_000) =>
36
- renderBtwWidget(jobs, theme, 0, now, 200).join("\n");
37
-
38
- describe("BTW widget layout", () => {
39
- test("empty when there are no running or lingering jobs", () => {
40
- expect(renderBtwWidget([], theme, 0, 1_000, 200)).toEqual([]);
41
- const old = job({ status: "completed", completedAt: 0 });
42
- expect(renderBtwWidget([old], theme, 0, 999_999, 200)).toEqual([]);
43
- });
44
-
45
- test("running heading is hollow and shows the running count", () => {
46
- const out = render([job({ id: 7, model: "GPT" })]);
47
- expect(out).toContain("\u25cb BTW (1)");
48
- expect(out).toContain("#7");
49
- expect(out).toContain("[GPT]");
50
- });
51
-
52
- test("uses primary identity, secondary activity, and tertiary metadata", () => {
53
- const taggedTheme: WidgetTheme = {
54
- fg: (color, text) => `<${color}>${text}</${color}>`,
55
- bold: (text) => text,
56
- };
57
- const output = renderBtwWidget(
58
- [job({ id: 7, model: "GPT", text: "Reading auth.ts", toolUses: 2 })],
59
- taggedTheme,
60
- 0,
61
- 1_000,
62
- 500,
63
- ).join("\n");
64
- expect(output).toContain("<accent>BTW</accent><muted> (1)</muted>");
65
- expect(output).toContain("<toolTitle>#7</toolTitle>");
66
- expect(output).toContain("<muted>[GPT]</muted>");
67
- expect(output).toMatch(/<muted>[^<]*2 · 1\.0s<\/muted>/);
68
- expect(output).toContain("<dim>Reading auth.ts</dim>");
69
- expect(output).toContain("<muted>└─</muted>");
70
- });
71
-
72
- test("finished jobs linger with a check, then drop after the window", () => {
73
- const done = job({ status: "completed", completedAt: 1_000, toolUses: 2, turnCount: 1 });
74
- // Default base 10s × 3 = 30s ok-window.
75
- expect(shouldShowFinished(done, 1_500)).toBe(true);
76
- expect(shouldShowFinished(done, 40_000)).toBe(false); // 39s elapsed > 30s window
77
-
78
- const out = render([done], 1_500);
79
- expect(out).toContain("\u2713");
80
- // All jobs finished → filled heading disk.
81
- expect(out).toContain("\u25cf BTW (0)");
82
- });
83
-
84
- test("errors linger longer and show the message", () => {
85
- const failed = job({ status: "error", completedAt: 1_000, error: "boom" });
86
- // Default base 10s × 9 = 90s error-window; 40s elapsed is past ok (30s) but well inside error.
87
- expect(shouldShowFinished(failed, 40_000)).toBe(true);
88
- expect(render([failed], 40_000)).toContain("boom");
89
- });
90
-
91
- test("linger windows scale off the config collapse delay", () => {
92
- // Defaults derive from the fallback base.
93
- expect(lingerWindows()).toEqual({
94
- ok: DEFAULT_LINGER_BASE_MS * OK_LINGER_SCALE,
95
- error: DEFAULT_LINGER_BASE_MS * ERROR_LINGER_SCALE,
96
- });
97
- // A custom base (e.g. config collapse.delaySec = 4 → 4000ms) scales both.
98
- expect(lingerWindows(4_000)).toEqual({ ok: 12_000, error: 36_000 });
99
- // Non-positive base falls back to the default so windows never collapse to 0.
100
- expect(lingerWindows(0)).toEqual(lingerWindows());
101
-
102
- // shouldShowFinished honors the threaded base: a 4s base drops an ok job at 13s.
103
- const done = job({ status: "completed", completedAt: 0 });
104
- expect(shouldShowFinished(done, 11_000, 4_000)).toBe(true); // < 12s
105
- expect(shouldShowFinished(done, 13_000, 4_000)).toBe(false); // > 12s
106
- });
107
-
108
- test("overflow collapses excess rows into a +N more line", () => {
109
- const many = Array.from({ length: 20 }, (_, i) => job({ id: i + 1 }));
110
- const lines = renderBtwWidget(many, theme, 0, 1_000, 200);
111
- expect(lines.length).toBeLessThanOrEqual(12);
112
- expect(lines.at(-1)).toContain("more");
113
- });
114
-
115
- test("hasVisibleJobs mirrors render visibility", () => {
116
- expect(hasVisibleJobs([job({})], 1_000)).toBe(true);
117
- expect(hasVisibleJobs([job({ status: "completed", completedAt: 0 })], 999_999)).toBe(false);
118
- });
119
- });
@@ -1,74 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { createEventBus, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
- import { icon } from "@xynogen/pix-pretty/icon-catalog";
4
- import { getUnattendedMode } from "@xynogen/pix-runtime";
5
- import extension from "./extension.ts";
6
-
7
- afterEach(() => {
8
- delete (globalThis as { __pixOnce?: WeakMap<object, Set<string>> }).__pixOnce;
9
- });
10
-
11
- describe("pix-commands registration", () => {
12
- function host() {
13
- const commands: string[] = [];
14
- const handlers = new Map<string, (args: string, ctx: never) => Promise<void>>();
15
- const renderers: string[] = [];
16
- const pi = {
17
- events: createEventBus(),
18
- registerCommand(
19
- name: string,
20
- options: { handler?: (args: string, ctx: never) => Promise<void> },
21
- ) {
22
- commands.push(name);
23
- if (options.handler) handlers.set(name, options.handler);
24
- },
25
- registerEntryRenderer(name: string) {
26
- renderers.push(name);
27
- },
28
- on() {},
29
- } as unknown as ExtensionAPI;
30
- return { pi, commands, handlers, renderers };
31
- }
32
-
33
- test("registers /clear, /btw, /afk, /yolo, and the BTW renderer once per Pi instance", () => {
34
- const { pi, commands, renderers } = host();
35
- extension(pi);
36
- extension(pi);
37
- expect(commands).toEqual(["clear", "btw", "afk", "yolo"]);
38
- expect(renderers).toEqual(["pix-btw-answer"]);
39
- });
40
-
41
- test("registers again for a fresh Pi session", () => {
42
- const first = host();
43
- const second = host();
44
- extension(first.pi);
45
- extension(second.pi);
46
- expect(first.commands).toEqual(["clear", "btw", "afk", "yolo"]);
47
- expect(second.commands).toEqual(["clear", "btw", "afk", "yolo"]);
48
- });
49
-
50
- test("/afk toggles shared state and status", async () => {
51
- const { pi, handlers } = host();
52
- extension(pi);
53
- const statuses: Array<string | undefined> = [];
54
- const notices: string[] = [];
55
- const ctx = {
56
- ui: {
57
- theme: { fg: (color: string, text: string) => `<${color}>${text}</${color}>` },
58
- setStatus: (_key: string, text: string | undefined) => statuses.push(text),
59
- notify: (text: string) => notices.push(text),
60
- },
61
- };
62
- const handler = handlers.get("afk");
63
- if (!handler) throw new Error("/afk not registered");
64
-
65
- await handler("", ctx as never);
66
- expect(getUnattendedMode(pi.events)).toBe("afk");
67
- expect(statuses.at(-1)).toBe(`<error>${icon("afk")} AFK</error>`);
68
- expect(notices.at(-1)).toContain("yellow gates auto-allow");
69
-
70
- await handler("", ctx as never);
71
- expect(getUnattendedMode(pi.events)).toBe("off");
72
- expect(statuses.at(-1)).toBeUndefined();
73
- });
74
- });
@@ -1,76 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { createEventBus } from "@earendil-works/pi-coding-agent";
3
- import {
4
- confirmYoloConsent,
5
- getMode,
6
- modelScore,
7
- setMode,
8
- unattendedBanner,
9
- YOLO_MIN_SCORE,
10
- } from "./unattended.ts";
11
-
12
- describe("unattended mode state", () => {
13
- test("setMode updates one session", () => {
14
- const events = createEventBus();
15
- setMode(events, "afk");
16
- expect(getMode(events)).toBe("afk");
17
-
18
- setMode(events, "yolo");
19
- expect(getMode(events)).toBe("yolo");
20
-
21
- setMode(events, "off");
22
- expect(getMode(events)).toBe("off");
23
- });
24
- });
25
-
26
- describe("modelScore", () => {
27
- test("returns null for an empty / off-catalog model", () => {
28
- expect(modelScore({ model: undefined })).toBeNull();
29
- expect(modelScore({ model: { id: "definitely-not-a-real-model-xyz" } })).toBeNull();
30
- });
31
- });
32
-
33
- describe("unattendedBanner", () => {
34
- test("off => no banner", () => {
35
- const events = createEventBus();
36
- setMode(events, "off");
37
- expect(unattendedBanner(events)).toBeUndefined();
38
- });
39
-
40
- test("afk banner names auto-deny of red and root", () => {
41
- const events = createEventBus();
42
- setMode(events, "afk");
43
- const b = unattendedBanner(events) ?? "";
44
- expect(b).toContain('mode="afk"');
45
- expect(b).toContain("auto-DENY");
46
- });
47
-
48
- test("yolo banner demands red/root self-justification", () => {
49
- const events = createEventBus();
50
- setMode(events, "yolo");
51
- const b = unattendedBanner(events) ?? "";
52
- expect(b).toContain('mode="yolo"');
53
- expect(b).toContain("blast radius");
54
- expect(b).toContain("reversible");
55
- });
56
- });
57
-
58
- describe("YOLO score threshold", () => {
59
- test("is a sane capability floor", () => {
60
- expect(YOLO_MIN_SCORE).toBe(75);
61
- });
62
- });
63
-
64
- describe("confirmYoloConsent (session gate)", () => {
65
- test("short-circuits true once consent is recorded for the session", async () => {
66
- const events = createEventBus();
67
- const { setYoloConsent } = await import("@xynogen/pix-runtime");
68
- setYoloConsent(events, true);
69
- // No ui passed: if it did not short-circuit it would return false.
70
- expect(await confirmYoloConsent(events, {})).toBe(true);
71
- });
72
-
73
- test("refuses when there is no ui to render the warning", async () => {
74
- expect(await confirmYoloConsent(createEventBus(), {})).toBe(false);
75
- });
76
- });