@xynogen/pix-commands 0.6.1 → 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 +2 -1
- package/src/btw/render.ts +3 -3
- package/src/btw/widget.ts +10 -10
- package/src/btw/index.test.ts +0 -46
- package/src/btw/render.test.ts +0 -102
- package/src/btw/session.test.ts +0 -128
- package/src/btw/widget.test.ts +0 -99
- package/src/extension.test.ts +0 -74
- package/src/unattended.test.ts +0 -76
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-commands",
|
|
3
|
-
"version": "0.6.
|
|
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
|
],
|
package/src/btw/render.ts
CHANGED
|
@@ -49,10 +49,10 @@ export function registerBtwRenderer(
|
|
|
49
49
|
// header text embedded inside Markdown can confuse wrapping and parsing.
|
|
50
50
|
const card = new Box(1, 1, (text) => theme.bg("selectedBg", text));
|
|
51
51
|
card.addChild(
|
|
52
|
-
new Text(`${statusGlyph} ${theme.bold("BTW")} ${theme.fg("
|
|
52
|
+
new Text(`${statusGlyph} ${theme.bold("BTW")} ${theme.fg("muted", `· ${meta}`)}`, 0, 0),
|
|
53
53
|
);
|
|
54
54
|
card.addChild(
|
|
55
|
-
new Text(`${theme.fg("accent", "▐")} ${theme.fg("
|
|
55
|
+
new Text(`${theme.fg("accent", "▐")} ${theme.fg("dim", details.question)}`, 0, 0),
|
|
56
56
|
);
|
|
57
57
|
card.addChild(new Spacer(1));
|
|
58
58
|
|
|
@@ -69,7 +69,7 @@ export function registerBtwRenderer(
|
|
|
69
69
|
card.addChild(new Text(theme.fg("thinkingText", details.thinking), 0, 0));
|
|
70
70
|
card.addChild(new Spacer(1));
|
|
71
71
|
} else {
|
|
72
|
-
card.addChild(new Text(theme.fg("
|
|
72
|
+
card.addChild(new Text(theme.fg("muted", "› reasoning hidden — expand to view"), 0, 0));
|
|
73
73
|
card.addChild(new Spacer(1));
|
|
74
74
|
}
|
|
75
75
|
}
|
package/src/btw/widget.ts
CHANGED
|
@@ -108,23 +108,23 @@ function finishedLine(job: BtwWidgetJob, theme: WidgetTheme): string {
|
|
|
108
108
|
if (job.status === "completed") {
|
|
109
109
|
mark = theme.fg("success", "\u2713");
|
|
110
110
|
} else if (job.status === "stopped") {
|
|
111
|
-
mark = theme.fg("
|
|
112
|
-
suffix = theme.fg("
|
|
111
|
+
mark = theme.fg("muted", "\u25a0");
|
|
112
|
+
suffix = theme.fg("muted", " stopped");
|
|
113
113
|
} else {
|
|
114
114
|
mark = theme.fg("error", "\u2717");
|
|
115
115
|
suffix = theme.fg("error", job.error ? ` ${job.error.slice(0, 60)}` : " error");
|
|
116
116
|
}
|
|
117
117
|
const model = theme.fg("muted", `[${job.model}]`);
|
|
118
|
-
const stats = theme.fg("
|
|
119
|
-
return `${mark} ${theme.fg("dim", `#${job.id}`)} ${model} ${theme.fg("
|
|
118
|
+
const stats = theme.fg("muted", statsFor(job, end));
|
|
119
|
+
return `${mark} ${theme.fg("dim", `#${job.id}`)} ${model} ${theme.fg("muted", "\u00b7")} ${stats}${suffix}`;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
function runningLine(job: BtwWidgetJob, theme: WidgetTheme, frame: string, now: number): string {
|
|
123
123
|
const model = theme.fg("muted", `[${job.model}]`);
|
|
124
|
-
const stats = theme.fg("
|
|
124
|
+
const stats = theme.fg("muted", statsFor(job, now));
|
|
125
125
|
const activeMap = new Map<string, string>(job.activeTools.map((name, i) => [String(i), name]));
|
|
126
126
|
const activity = theme.fg("dim", describeActivity(activeMap, job.text));
|
|
127
|
-
const dot = theme.fg("
|
|
127
|
+
const dot = theme.fg("muted", "\u00b7");
|
|
128
128
|
return `${theme.fg("accent", frame)} ${theme.fg("toolTitle", theme.bold(`#${job.id}`))} ${model} ${dot} ${stats} ${dot} ${activity}`;
|
|
129
129
|
}
|
|
130
130
|
|
|
@@ -152,15 +152,15 @@ export function renderBtwWidget(
|
|
|
152
152
|
const spinner = SPINNER[frame % SPINNER.length] ?? "";
|
|
153
153
|
|
|
154
154
|
const runningLines = running.map((j) =>
|
|
155
|
-
truncate(`${theme.fg("
|
|
155
|
+
truncate(`${theme.fg("muted", "\u251c\u2500")} ${runningLine(j, theme, spinner, now)}`),
|
|
156
156
|
);
|
|
157
157
|
const finishedLines = finished.map((j) =>
|
|
158
|
-
truncate(`${theme.fg("
|
|
158
|
+
truncate(`${theme.fg("muted", "\u251c\u2500")} ${finishedLine(j, theme)}`),
|
|
159
159
|
);
|
|
160
160
|
|
|
161
161
|
const lines: string[] = [
|
|
162
162
|
truncate(
|
|
163
|
-
`${theme.fg(headingColor, headingIcon)} ${theme.fg(headingColor,
|
|
163
|
+
`${theme.fg(headingColor, headingIcon)} ${theme.fg(headingColor, "BTW")}${theme.fg("muted", ` (${running.length})`)}`,
|
|
164
164
|
),
|
|
165
165
|
];
|
|
166
166
|
|
|
@@ -173,7 +173,7 @@ export function renderBtwWidget(
|
|
|
173
173
|
const hidden = body.length - shown.length;
|
|
174
174
|
lines.push(...shown);
|
|
175
175
|
lines.push(
|
|
176
|
-
truncate(`${theme.fg("
|
|
176
|
+
truncate(`${theme.fg("muted", "\u251c\u2500")} ${theme.fg("muted", `+${hidden} more`)}`),
|
|
177
177
|
);
|
|
178
178
|
}
|
|
179
179
|
|
package/src/btw/index.test.ts
DELETED
|
@@ -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
|
-
});
|
package/src/btw/render.test.ts
DELETED
|
@@ -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
|
-
});
|
package/src/btw/session.test.ts
DELETED
|
@@ -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
|
-
});
|
package/src/btw/widget.test.ts
DELETED
|
@@ -1,99 +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("finished jobs linger with a check, then drop after the window", () => {
|
|
53
|
-
const done = job({ status: "completed", completedAt: 1_000, toolUses: 2, turnCount: 1 });
|
|
54
|
-
// Default base 10s × 3 = 30s ok-window.
|
|
55
|
-
expect(shouldShowFinished(done, 1_500)).toBe(true);
|
|
56
|
-
expect(shouldShowFinished(done, 40_000)).toBe(false); // 39s elapsed > 30s window
|
|
57
|
-
|
|
58
|
-
const out = render([done], 1_500);
|
|
59
|
-
expect(out).toContain("\u2713");
|
|
60
|
-
// All jobs finished → filled heading disk.
|
|
61
|
-
expect(out).toContain("\u25cf BTW (0)");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test("errors linger longer and show the message", () => {
|
|
65
|
-
const failed = job({ status: "error", completedAt: 1_000, error: "boom" });
|
|
66
|
-
// Default base 10s × 9 = 90s error-window; 40s elapsed is past ok (30s) but well inside error.
|
|
67
|
-
expect(shouldShowFinished(failed, 40_000)).toBe(true);
|
|
68
|
-
expect(render([failed], 40_000)).toContain("boom");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("linger windows scale off the config collapse delay", () => {
|
|
72
|
-
// Defaults derive from the fallback base.
|
|
73
|
-
expect(lingerWindows()).toEqual({
|
|
74
|
-
ok: DEFAULT_LINGER_BASE_MS * OK_LINGER_SCALE,
|
|
75
|
-
error: DEFAULT_LINGER_BASE_MS * ERROR_LINGER_SCALE,
|
|
76
|
-
});
|
|
77
|
-
// A custom base (e.g. config collapse.delaySec = 4 → 4000ms) scales both.
|
|
78
|
-
expect(lingerWindows(4_000)).toEqual({ ok: 12_000, error: 36_000 });
|
|
79
|
-
// Non-positive base falls back to the default so windows never collapse to 0.
|
|
80
|
-
expect(lingerWindows(0)).toEqual(lingerWindows());
|
|
81
|
-
|
|
82
|
-
// shouldShowFinished honors the threaded base: a 4s base drops an ok job at 13s.
|
|
83
|
-
const done = job({ status: "completed", completedAt: 0 });
|
|
84
|
-
expect(shouldShowFinished(done, 11_000, 4_000)).toBe(true); // < 12s
|
|
85
|
-
expect(shouldShowFinished(done, 13_000, 4_000)).toBe(false); // > 12s
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("overflow collapses excess rows into a +N more line", () => {
|
|
89
|
-
const many = Array.from({ length: 20 }, (_, i) => job({ id: i + 1 }));
|
|
90
|
-
const lines = renderBtwWidget(many, theme, 0, 1_000, 200);
|
|
91
|
-
expect(lines.length).toBeLessThanOrEqual(12);
|
|
92
|
-
expect(lines.at(-1)).toContain("more");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("hasVisibleJobs mirrors render visibility", () => {
|
|
96
|
-
expect(hasVisibleJobs([job({})], 1_000)).toBe(true);
|
|
97
|
-
expect(hasVisibleJobs([job({ status: "completed", completedAt: 0 })], 999_999)).toBe(false);
|
|
98
|
-
});
|
|
99
|
-
});
|
package/src/extension.test.ts
DELETED
|
@@ -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
|
-
});
|
package/src/unattended.test.ts
DELETED
|
@@ -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
|
-
});
|