pi-archimedes 2.10.2 → 2.10.4
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 +13 -13
- package/src/settings.test.ts +159 -0
- package/src/settings.ts +22 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-archimedes",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.4",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/danielcherubini/pi-archimedes.git"
|
|
@@ -16,18 +16,18 @@
|
|
|
16
16
|
],
|
|
17
17
|
"main": "./src/index.ts",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@pi-archimedes/core": "2.10.
|
|
20
|
-
"@pi-archimedes/footer": "2.10.
|
|
21
|
-
"@pi-archimedes/diff": "2.10.
|
|
22
|
-
"@pi-archimedes/ask": "2.10.
|
|
23
|
-
"@pi-archimedes/image-paste": "2.10.
|
|
24
|
-
"@pi-archimedes/subagent": "2.10.
|
|
25
|
-
"@pi-archimedes/todo": "2.10.
|
|
26
|
-
"@pi-archimedes/notify": "2.10.
|
|
27
|
-
"@pi-archimedes/session-name": "2.10.
|
|
28
|
-
"@pi-archimedes/sudo": "2.10.
|
|
29
|
-
"@pi-archimedes/ui": "2.10.
|
|
30
|
-
"@pi-archimedes/web": "2.10.
|
|
19
|
+
"@pi-archimedes/core": "2.10.4",
|
|
20
|
+
"@pi-archimedes/footer": "2.10.4",
|
|
21
|
+
"@pi-archimedes/diff": "2.10.4",
|
|
22
|
+
"@pi-archimedes/ask": "2.10.4",
|
|
23
|
+
"@pi-archimedes/image-paste": "2.10.4",
|
|
24
|
+
"@pi-archimedes/subagent": "2.10.4",
|
|
25
|
+
"@pi-archimedes/todo": "2.10.4",
|
|
26
|
+
"@pi-archimedes/notify": "2.10.4",
|
|
27
|
+
"@pi-archimedes/session-name": "2.10.4",
|
|
28
|
+
"@pi-archimedes/sudo": "2.10.4",
|
|
29
|
+
"@pi-archimedes/ui": "2.10.4",
|
|
30
|
+
"@pi-archimedes/web": "2.10.4"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"@earendil-works/pi-coding-agent": ">=0.1.0",
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
// ── Panel wiring guard ──────────────────────────────────────────────────────
|
|
5
|
+
//
|
|
6
|
+
// A SettingItem is only changeable in the /archimedes overlay if it either has
|
|
7
|
+
// `values` (←/→ cycles them) or is registered in PROMPTS (Enter opens a
|
|
8
|
+
// free-text/number field). An item with neither renders as a read-only row:
|
|
9
|
+
// the cursor lands on it, every key does nothing, and the user cannot tell the
|
|
10
|
+
// control is inert. `sessionNameModel` shipped that way — it even has a
|
|
11
|
+
// working `onChange` case waiting for a value it could never receive.
|
|
12
|
+
//
|
|
13
|
+
// The second assertion guards the other half of the same class of bug: an item
|
|
14
|
+
// whose id has no `case` in the onChange switch. Such a row can be changed on
|
|
15
|
+
// screen but the edit is never written to settings.json — the same shape as the
|
|
16
|
+
// dead reasoning row reported on #81, caught before it ships again.
|
|
17
|
+
|
|
18
|
+
vi.mock("@pi-archimedes/core/settings-io", () => {
|
|
19
|
+
const store: Record<string, unknown> = {};
|
|
20
|
+
return {
|
|
21
|
+
loadConfig: vi.fn(
|
|
22
|
+
(ns: string, defaults: object) =>
|
|
23
|
+
({ ...defaults, ...((store[ns] as object) ?? {}) }),
|
|
24
|
+
),
|
|
25
|
+
saveConfig: vi.fn((ns: string, config: object) => {
|
|
26
|
+
store[ns] = config;
|
|
27
|
+
}),
|
|
28
|
+
removeConfig: vi.fn((ns: string) => {
|
|
29
|
+
delete store[ns];
|
|
30
|
+
}),
|
|
31
|
+
isConfigEnabled: vi.fn(() => true),
|
|
32
|
+
setConfigEnabled: vi.fn(),
|
|
33
|
+
__store: store,
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
import { buildSettingsItems, openSettings, PROMPTS } from "./settings.js";
|
|
38
|
+
import { loadAllConfig } from "./config.js";
|
|
39
|
+
|
|
40
|
+
// Capture what openSettings hands the manager so the onChange switch can be
|
|
41
|
+
// driven directly, and what lands in settings.json after onSave.
|
|
42
|
+
const managerCalls: Array<{
|
|
43
|
+
onChange: (id: string, value: string) => void;
|
|
44
|
+
onSave: () => void;
|
|
45
|
+
items: Array<{ id: string; currentValue: string }>;
|
|
46
|
+
}> = [];
|
|
47
|
+
|
|
48
|
+
vi.mock("./settings-manager.js", async (importOriginal) => ({
|
|
49
|
+
...(await importOriginal as object),
|
|
50
|
+
createSettingsManager: vi.fn((opts: any) => {
|
|
51
|
+
managerCalls.push({ onChange: opts.onChange, onSave: opts.onSave, items: opts.items });
|
|
52
|
+
return { render: () => [], handleInput: () => {}, invalidate: () => {}, dispose: () => {} };
|
|
53
|
+
}),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
const store = (vi.mocked(await import("@pi-archimedes/core/settings-io")) as any).__store;
|
|
57
|
+
|
|
58
|
+
// The mocked settings-io store is module-level, so every test in this file
|
|
59
|
+
// shares it: without this reset a test that saves a model leaks it into
|
|
60
|
+
// whichever test runs next, making the suite order-dependent.
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
for (const key of Object.keys(store)) delete store[key];
|
|
63
|
+
managerCalls.length = 0;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const SOURCE = readFileSync(new URL("./settings.ts", import.meta.url), "utf8");
|
|
67
|
+
|
|
68
|
+
describe("/archimedes settings panel wiring", () => {
|
|
69
|
+
it("every item is editable in the panel (has values or a prompt)", async () => {
|
|
70
|
+
const items = await buildSettingsItems(loadAllConfig());
|
|
71
|
+
expect(items.length).toBeGreaterThan(0);
|
|
72
|
+
|
|
73
|
+
const inert = items
|
|
74
|
+
.filter((item) => item.values === undefined && !(item.id in PROMPTS))
|
|
75
|
+
.map((item) => item.id);
|
|
76
|
+
|
|
77
|
+
// e.g. ["sessionNameModel"] — a row that renders but swallows every key.
|
|
78
|
+
expect(inert).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("every item persists through onChange", async () => {
|
|
82
|
+
const items = await buildSettingsItems(loadAllConfig());
|
|
83
|
+
|
|
84
|
+
const unhandled = items
|
|
85
|
+
.filter((item) => !SOURCE.includes(`case "${item.id}":`))
|
|
86
|
+
.map((item) => item.id);
|
|
87
|
+
|
|
88
|
+
expect(unhandled).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("every prompt has a matching label", () => {
|
|
92
|
+
for (const [id, descriptor] of Object.entries(PROMPTS)) {
|
|
93
|
+
expect(descriptor.label, id).not.toBe("");
|
|
94
|
+
expect(["text", "number"]).toContain(descriptor.kind);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("sessionNameModel editing", () => {
|
|
100
|
+
async function openPanel() {
|
|
101
|
+
managerCalls.length = 0;
|
|
102
|
+
const custom = vi.fn((factory: any) => factory({}, {}, {}, () => {}));
|
|
103
|
+
// openSettings awaits buildSettingsItems (diff is lazy-imported) before
|
|
104
|
+
// it reaches ctx.ui.custom, so this must be awaited, not fired and forgotten.
|
|
105
|
+
await openSettings({} as any, { ui: { custom } } as any);
|
|
106
|
+
expect(custom).toHaveBeenCalledTimes(1);
|
|
107
|
+
const panel = managerCalls.at(-1);
|
|
108
|
+
if (!panel) throw new Error("openSettings never built a settings manager");
|
|
109
|
+
return panel;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function savedModel() {
|
|
113
|
+
return (store["archimedes.sessionName"] as any)?.model;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
it("stores a typed model reference", async () => {
|
|
117
|
+
const panel = await openPanel();
|
|
118
|
+
panel.onChange("sessionNameModel", "openai/gpt-4o-mini");
|
|
119
|
+
panel.onSave();
|
|
120
|
+
expect(savedModel()).toBe("openai/gpt-4o-mini");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("treats a blank field as 'use the current model' and leaves the key unset", async () => {
|
|
124
|
+
const panel = await openPanel();
|
|
125
|
+
panel.onChange("sessionNameModel", " ");
|
|
126
|
+
panel.onSave();
|
|
127
|
+
expect(savedModel()).toBeUndefined();
|
|
128
|
+
expect(JSON.stringify(store["archimedes.sessionName"])).not.toContain('"model"');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("treats the displayed placeholder the same way", async () => {
|
|
132
|
+
const panel = await openPanel();
|
|
133
|
+
panel.onChange("sessionNameModel", "(current model)");
|
|
134
|
+
panel.onSave();
|
|
135
|
+
expect(savedModel()).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("seeds the prompt with an empty field, not the '(current model)' placeholder", async () => {
|
|
139
|
+
// Enter seeds from item.currentValue; leaving the placeholder there would
|
|
140
|
+
// prepend it to whatever the user types (the delayMs pitfall).
|
|
141
|
+
const panel = await openPanel();
|
|
142
|
+
const item = panel.items.find((i) => i.id === "sessionNameModel");
|
|
143
|
+
expect(item?.currentValue).toBe("");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("seeds the prompt with the configured model when one is set", async () => {
|
|
147
|
+
store["archimedes.sessionName"] = { model: "openai/gpt-4o-mini" };
|
|
148
|
+
const panel = await openPanel();
|
|
149
|
+
const item = panel.items.find((i) => i.id === "sessionNameModel");
|
|
150
|
+
expect(item?.currentValue).toBe("openai/gpt-4o-mini");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("trims whitespace around a reference", async () => {
|
|
154
|
+
const panel = await openPanel();
|
|
155
|
+
panel.onChange("sessionNameModel", " anthropic/claude ");
|
|
156
|
+
panel.onSave();
|
|
157
|
+
expect(savedModel()).toBe("anthropic/claude");
|
|
158
|
+
});
|
|
159
|
+
});
|
package/src/settings.ts
CHANGED
|
@@ -19,7 +19,8 @@ import { createSettingsManager, type PromptDescriptor } from "./settings-manager
|
|
|
19
19
|
|
|
20
20
|
// ── Free-input prompt descriptors (keyed by item.id) ───────────────────────
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// Exported for the panel-wiring test: every item must be reachable through PROMPTS or values.
|
|
23
|
+
export const PROMPTS: Record<string, PromptDescriptor> = {
|
|
23
24
|
labelText: { kind: "text", label: "Label text" },
|
|
24
25
|
editorSpinLabel: { kind: "text", label: "Spinner label" },
|
|
25
26
|
labelColor: { kind: "text", label: "RGB color (e.g. 255,215,0)" },
|
|
@@ -29,6 +30,11 @@ const PROMPTS: Record<string, PromptDescriptor> = {
|
|
|
29
30
|
splitThreshold: { kind: "number", label: "Footer split threshold", min: 80 },
|
|
30
31
|
branchMaxLength: { kind: "number", label: "Branch max length", min: 0 },
|
|
31
32
|
delayMs: { kind: "number", label: "Notify delay (seconds)", min: 1 },
|
|
33
|
+
// A model reference, so free text: the catalogue is only known at runtime and
|
|
34
|
+
// resolveModel() tolerates canonical provider/id, bare id, and thinking-suffix
|
|
35
|
+
// forms. Without this entry the row renders but swallows every key — inert
|
|
36
|
+
// ever since a2bd4d6 added it alongside an onChange case that could never fire.
|
|
37
|
+
sessionNameModel: { kind: "text", label: "Model for naming (blank = current model)" },
|
|
32
38
|
};
|
|
33
39
|
|
|
34
40
|
// ── Settings UI ─────────────────────────────────────────────────────────────
|
|
@@ -84,6 +90,14 @@ export async function openSettings(pi: ExtensionAPI, ctx: ExtensionContext): Pro
|
|
|
84
90
|
|
|
85
91
|
const items = await buildSettingsItems(allConfig);
|
|
86
92
|
|
|
93
|
+
// The row displays "(current model)" when unset, but Enter seeds the prompt
|
|
94
|
+
// from item.currentValue, so a typed reference would be appended to the
|
|
95
|
+
// placeholder — the same pitfall buildSettingsItems works around for delayMs.
|
|
96
|
+
// Show an empty field instead, which is what the row's own description and
|
|
97
|
+
// PROMPTS label both promise ("blank = current model").
|
|
98
|
+
const modelItem = items.find((i) => i.id === "sessionNameModel");
|
|
99
|
+
if (modelItem && modelItem.currentValue === "(current model)") modelItem.currentValue = "";
|
|
100
|
+
|
|
87
101
|
ctx.ui.custom((_tui, theme, _keybindings, done) => {
|
|
88
102
|
const settingsManager = createSettingsManager({
|
|
89
103
|
items,
|
|
@@ -144,7 +158,13 @@ export async function openSettings(pi: ExtensionAPI, ctx: ExtensionContext): Pro
|
|
|
144
158
|
}
|
|
145
159
|
|
|
146
160
|
// ── Session name settings ──
|
|
147
|
-
|
|
161
|
+
// Blank and the displayed placeholder both mean "use the current
|
|
162
|
+
// model", and are stored as unset rather than "" so the key disappears.
|
|
163
|
+
case "sessionNameModel": {
|
|
164
|
+
const ref = newValue.trim();
|
|
165
|
+
sessionNameConfig.model = !ref || ref === "(current model)" ? undefined : ref;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
148
168
|
}
|
|
149
169
|
},
|
|
150
170
|
onSave: () => {
|