pi-archimedes 2.7.3 → 2.9.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/README.md +5 -16
- package/package.json +16 -15
- package/src/config.ts +21 -7
- package/src/factory-lifecycle.test.ts +74 -25
- package/src/index.ts +36 -24
- package/src/onboarding/index.test.ts +391 -0
- package/src/onboarding/index.ts +80 -0
- package/src/onboarding/overlay.test.ts +503 -0
- package/src/onboarding/overlay.ts +255 -0
- package/src/plugins.test.ts +24 -10
- package/src/plugins.ts +5 -4
- package/src/settings.ts +25 -21
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import {
|
|
3
|
+
padEnd,
|
|
4
|
+
renderFooter,
|
|
5
|
+
renderHeader,
|
|
6
|
+
wrapWithBorder,
|
|
7
|
+
type OverlayTheme,
|
|
8
|
+
} from "@pi-archimedes/core/overlay";
|
|
9
|
+
import type { ThinkingStyle, ToolStyle } from "@pi-archimedes/ui/config";
|
|
10
|
+
import { spinFrame } from "@pi-archimedes/ui/editor";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The onboarding modal's result. Each `*Answered` flag tells the orchestrator
|
|
14
|
+
* (Task 4) whether the user actually committed that step — an unanswered step
|
|
15
|
+
* is left untouched on the write side. `thinkingValue`/`toolValue` are only
|
|
16
|
+
* meaningful when their step was answered; `pluginSelections` maps every plugin
|
|
17
|
+
* id to its final on/off state (present regardless, so the orchestrator can
|
|
18
|
+
* diff against the current flags).
|
|
19
|
+
*/
|
|
20
|
+
export interface OnboardingResult {
|
|
21
|
+
thinkingAnswered: boolean;
|
|
22
|
+
thinkingValue: ThinkingStyle;
|
|
23
|
+
toolAnswered: boolean;
|
|
24
|
+
toolValue: ToolStyle;
|
|
25
|
+
pluginsAnswered: boolean;
|
|
26
|
+
pluginSelections: Record<string, boolean>;
|
|
27
|
+
spinnerAnswered: boolean;
|
|
28
|
+
spinnerValue: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface OnboardingOverlayOptions {
|
|
32
|
+
theme: OverlayTheme;
|
|
33
|
+
thinkingDefault: ThinkingStyle;
|
|
34
|
+
toolDefault: ToolStyle;
|
|
35
|
+
plugins: { id: string; label: string; description: string; selected: boolean }[];
|
|
36
|
+
spinners: readonly string[];
|
|
37
|
+
spinnerDefault: string;
|
|
38
|
+
/** The TUI repaint hook (the `ui.custom` factory's `tui.requestRender`) — when provided, the spinner step's live 4-cell previews animate on a 40 ms tick; without it the previews are static (tick 0) and no timer runs. */
|
|
39
|
+
requestRender?: () => void;
|
|
40
|
+
onDone: (result: OnboardingResult) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The single "style" question was split into two INDEPENDENT ones (thinking +
|
|
44
|
+
// tool) so the user can mix, e.g. "Full thinking" + "Minimal tools". The tool
|
|
45
|
+
// question is the styling master switch: Minimal = archimedes-styled tools,
|
|
46
|
+
// Native = no archimedes tool styling at all. Both steps list the standard
|
|
47
|
+
// option first and the compact option second (matching the /archimedes
|
|
48
|
+
// panel's value order: thinkingStyle [Full, Compact], toolStyle
|
|
49
|
+
// [Native, Minimal]).
|
|
50
|
+
const THINKING_OPTIONS: { label: ThinkingStyle; description: string }[] = [
|
|
51
|
+
{ label: "Full", description: "Show all of the model's reasoning" },
|
|
52
|
+
{ label: "Compact", description: "One line of reasoning (click to expand)" },
|
|
53
|
+
];
|
|
54
|
+
const TOOL_OPTIONS: { label: ToolStyle; description: string }[] = [
|
|
55
|
+
{ label: "Native", description: "Pi's native tool rendering (no styling)" },
|
|
56
|
+
{ label: "Minimal", description: "Archimedes-styled tools (minimal view)" },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build the onboarding wizard as a single pure TUI component: one `ui.custom`
|
|
61
|
+
* overlay with an internal `step` state (0 → 1 → 2 → 3) and no open/close
|
|
62
|
+
* flicker between steps. It is deliberately NOT a reuse of the `ask`
|
|
63
|
+
* package's `askQuestionsWithTabs` (whose "Other" option,
|
|
64
|
+
* cancel-discards-everything semantics, and blocked empty-multi-select don't
|
|
65
|
+
* fit a setup wizard).
|
|
66
|
+
*
|
|
67
|
+
* It is a pure function: it takes the pre-selected values + an `onDone`
|
|
68
|
+
* callback and returns a TUI component; it knows nothing about settings or
|
|
69
|
+
* plugins, so it is testable in isolation. The `theme` is typed as the
|
|
70
|
+
* structural `OverlayTheme` (not the `Theme` class, which has private members
|
|
71
|
+
* and can't be satisfied by a plain object mock).
|
|
72
|
+
*/
|
|
73
|
+
export function createOnboardingOverlay(opts: OnboardingOverlayOptions) {
|
|
74
|
+
const theme = opts.theme;
|
|
75
|
+
let activeStep = 0;
|
|
76
|
+
// Clamp the pre-selected indices (a corrupt/unknown value → index 0, never -1).
|
|
77
|
+
const cursorByStep: number[] = [
|
|
78
|
+
Math.max(0, THINKING_OPTIONS.findIndex((o) => o.label === opts.thinkingDefault)),
|
|
79
|
+
Math.max(0, TOOL_OPTIONS.findIndex((o) => o.label === opts.toolDefault)),
|
|
80
|
+
0,
|
|
81
|
+
Math.max(0, opts.spinners.indexOf(opts.spinnerDefault)),
|
|
82
|
+
];
|
|
83
|
+
const pluginToggles: boolean[] = opts.plugins.map((p) => p.selected);
|
|
84
|
+
const confirmed: boolean[] = [false, false, false, false];
|
|
85
|
+
let finalized = false;
|
|
86
|
+
// The live spinner-preview tick (the spinner step renders `spinFrame(name, tick)` next to each name); only advances when a repaint hook is provided.
|
|
87
|
+
let tick = 0;
|
|
88
|
+
const onTick = opts.requestRender;
|
|
89
|
+
// Only the spinner step (3) renders a live frame — don't advance the tick or
|
|
90
|
+
// request a repaint while the user is on the thinking/tool/plugin steps.
|
|
91
|
+
const timer = onTick
|
|
92
|
+
? setInterval(() => {
|
|
93
|
+
if (activeStep === 3) {
|
|
94
|
+
tick += 1;
|
|
95
|
+
onTick();
|
|
96
|
+
}
|
|
97
|
+
}, 40)
|
|
98
|
+
: undefined;
|
|
99
|
+
|
|
100
|
+
function optionsCountForStep(step: number): number {
|
|
101
|
+
if (step === 0) return THINKING_OPTIONS.length;
|
|
102
|
+
if (step === 1) return TOOL_OPTIONS.length;
|
|
103
|
+
if (step === 2) return opts.plugins.length;
|
|
104
|
+
return opts.spinners.length;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function finalize(): void {
|
|
108
|
+
if (finalized) return;
|
|
109
|
+
finalized = true;
|
|
110
|
+
// Belt-and-braces: stop the interval the moment the wizard is done (in case
|
|
111
|
+
// the TUI tears down without calling dispose()).
|
|
112
|
+
if (timer) clearInterval(timer);
|
|
113
|
+
const thinkingIdx = cursorByStep[0] ?? 0;
|
|
114
|
+
const toolIdx = cursorByStep[1] ?? 0;
|
|
115
|
+
const spinnerIdx = cursorByStep[3] ?? 0;
|
|
116
|
+
const pluginSelections: Record<string, boolean> = {};
|
|
117
|
+
for (let i = 0; i < opts.plugins.length; i++) {
|
|
118
|
+
const p = opts.plugins[i];
|
|
119
|
+
if (!p) continue;
|
|
120
|
+
pluginSelections[p.id] = pluginToggles[i] ?? false;
|
|
121
|
+
}
|
|
122
|
+
opts.onDone({
|
|
123
|
+
thinkingAnswered: confirmed[0] ?? false,
|
|
124
|
+
thinkingValue: THINKING_OPTIONS[thinkingIdx]?.label ?? "Full",
|
|
125
|
+
toolAnswered: confirmed[1] ?? false,
|
|
126
|
+
toolValue: TOOL_OPTIONS[toolIdx]?.label ?? "Minimal",
|
|
127
|
+
pluginsAnswered: confirmed[2] ?? false,
|
|
128
|
+
pluginSelections,
|
|
129
|
+
spinnerAnswered: confirmed[3] ?? false,
|
|
130
|
+
spinnerValue: opts.spinners[spinnerIdx] ?? "",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function handleInput(data: string): void {
|
|
135
|
+
if (matchesKey(data, Key.up)) {
|
|
136
|
+
cursorByStep[activeStep] = Math.max(0, (cursorByStep[activeStep] ?? 0) - 1);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (matchesKey(data, Key.down)) {
|
|
140
|
+
// Math.max(0, …): with an empty options list the min() bound alone is -1,
|
|
141
|
+
// which would drive the cursor negative (same discipline as the up clamp).
|
|
142
|
+
cursorByStep[activeStep] = Math.max(
|
|
143
|
+
0,
|
|
144
|
+
Math.min(
|
|
145
|
+
optionsCountForStep(activeStep) - 1,
|
|
146
|
+
(cursorByStep[activeStep] ?? 0) + 1,
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (matchesKey(data, Key.space) && activeStep === 2) {
|
|
152
|
+
const cur = cursorByStep[2] ?? 0;
|
|
153
|
+
pluginToggles[cur] = !(pluginToggles[cur] ?? false);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (matchesKey(data, Key.enter)) {
|
|
157
|
+
confirmed[activeStep] = true;
|
|
158
|
+
if (activeStep === 3) finalize();
|
|
159
|
+
else activeStep += 1;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (matchesKey(data, Key.escape)) {
|
|
163
|
+
finalize();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function render(width: number): string[] {
|
|
169
|
+
const lines: string[] = [];
|
|
170
|
+
lines.push(renderHeader(" Welcome to pi-archimedes ", width - 2, theme));
|
|
171
|
+
lines.push(padEnd(`Set up your preferences · ${activeStep + 1}/4`, width - 2));
|
|
172
|
+
lines.push(padEnd("Choices apply from your next session.", width - 2));
|
|
173
|
+
|
|
174
|
+
if (activeStep === 0) {
|
|
175
|
+
lines.push(padEnd("Thinking style", width - 2));
|
|
176
|
+
lines.push("");
|
|
177
|
+
for (let i = 0; i < THINKING_OPTIONS.length; i++) {
|
|
178
|
+
const opt = THINKING_OPTIONS[i];
|
|
179
|
+
if (!opt) continue;
|
|
180
|
+
const marker = i === (cursorByStep[0] ?? 0) ? "> " : " ";
|
|
181
|
+
lines.push(
|
|
182
|
+
padEnd(
|
|
183
|
+
`${marker}${opt.label.padEnd(9)}${truncateToWidth(opt.description, width - 14, "")}`,
|
|
184
|
+
width - 2,
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
lines.push("");
|
|
189
|
+
lines.push(renderFooter(" [↑↓] move [enter] next [esc] finish ", width - 2, theme));
|
|
190
|
+
} else if (activeStep === 1) {
|
|
191
|
+
lines.push(padEnd("Tool output style", width - 2));
|
|
192
|
+
lines.push("");
|
|
193
|
+
for (let i = 0; i < TOOL_OPTIONS.length; i++) {
|
|
194
|
+
const opt = TOOL_OPTIONS[i];
|
|
195
|
+
if (!opt) continue;
|
|
196
|
+
const marker = i === (cursorByStep[1] ?? 0) ? "> " : " ";
|
|
197
|
+
lines.push(
|
|
198
|
+
padEnd(
|
|
199
|
+
`${marker}${opt.label.padEnd(9)}${truncateToWidth(opt.description, width - 14, "")}`,
|
|
200
|
+
width - 2,
|
|
201
|
+
),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
lines.push("");
|
|
205
|
+
lines.push(renderFooter(" [↑↓] move [enter] next [esc] finish ", width - 2, theme));
|
|
206
|
+
} else if (activeStep === 2) {
|
|
207
|
+
lines.push(padEnd("Which plugins do you want?", width - 2));
|
|
208
|
+
lines.push("");
|
|
209
|
+
for (let i = 0; i < opts.plugins.length; i++) {
|
|
210
|
+
const p = opts.plugins[i];
|
|
211
|
+
if (!p) continue;
|
|
212
|
+
// The cursor (>) marks the row Space will toggle; the ✓/· marker shows
|
|
213
|
+
// the row's enabled state — both, so the user sees what they're about
|
|
214
|
+
// to change and what it currently is.
|
|
215
|
+
const cursor = i === (cursorByStep[2] ?? 0) ? "> " : " ";
|
|
216
|
+
const marker = (pluginToggles[i] ?? false) ? "✓" : "·";
|
|
217
|
+
const label = truncateToWidth(p.label, 15, "");
|
|
218
|
+
lines.push(
|
|
219
|
+
padEnd(
|
|
220
|
+
`${cursor} ${marker} ${label.padEnd(16)}${truncateToWidth(p.description, width - 26, "")}`,
|
|
221
|
+
width - 2,
|
|
222
|
+
),
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
lines.push("");
|
|
226
|
+
lines.push(renderFooter(" [↑↓] move [space] toggle [enter] next [esc] finish ", width - 2, theme));
|
|
227
|
+
} else {
|
|
228
|
+
lines.push(padEnd("Which spinner for the editor border?", width - 2));
|
|
229
|
+
lines.push("");
|
|
230
|
+
for (let i = 0; i < opts.spinners.length; i++) {
|
|
231
|
+
const name = opts.spinners[i] ?? "";
|
|
232
|
+
const marker = i === (cursorByStep[3] ?? 0) ? "> " : " ";
|
|
233
|
+
const preview = spinFrame(name, tick);
|
|
234
|
+
// Options render consecutively (no blank line between them): the 10
|
|
235
|
+
// options must fit the overlay's 80% max height on a small terminal
|
|
236
|
+
// (24 rows → ~19), so the per-option vertical spacing was dropped.
|
|
237
|
+
lines.push(padEnd(`${marker}${name.padEnd(15)}${preview}`, width - 2));
|
|
238
|
+
}
|
|
239
|
+
lines.push("");
|
|
240
|
+
lines.push(renderFooter(" [↑↓] move [enter] done [esc] finish (skip rest) ", width - 2, theme));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return wrapWithBorder(lines, width, theme);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
focused: true,
|
|
248
|
+
render,
|
|
249
|
+
handleInput,
|
|
250
|
+
invalidate(): void {},
|
|
251
|
+
dispose(): void {
|
|
252
|
+
if (timer) clearInterval(timer);
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
}
|
package/src/plugins.test.ts
CHANGED
|
@@ -51,6 +51,16 @@ vi.mock("@pi-archimedes/core", () => ({
|
|
|
51
51
|
]),
|
|
52
52
|
}));
|
|
53
53
|
|
|
54
|
+
vi.mock("@pi-archimedes/ui", () => ({
|
|
55
|
+
getUISettingsItems: vi.fn(() => [
|
|
56
|
+
{ id: "mutedTheme", label: "Muted theme", currentValue: "Off", values: ["On", "Off"] },
|
|
57
|
+
]),
|
|
58
|
+
loadUIConfig: vi.fn(() => ({})),
|
|
59
|
+
saveUIConfig: vi.fn(),
|
|
60
|
+
registerUI: vi.fn(),
|
|
61
|
+
unpatchConsoleLog: vi.fn(),
|
|
62
|
+
}));
|
|
63
|
+
|
|
54
64
|
vi.mock("@pi-archimedes/footer/config", () => ({
|
|
55
65
|
getFooterSettingsItems: vi.fn(() => [
|
|
56
66
|
{ id: "splitThreshold", label: "Footer split threshold", currentValue: "120" },
|
|
@@ -76,6 +86,7 @@ vi.mock("@pi-archimedes/diff", () => ({
|
|
|
76
86
|
{ id: "diffTheme", label: "Diff theme", currentValue: "github-dark" },
|
|
77
87
|
]),
|
|
78
88
|
}));
|
|
89
|
+
const { getUISettingsItems } = await import("@pi-archimedes/ui");
|
|
79
90
|
const {
|
|
80
91
|
PLUGINS,
|
|
81
92
|
isPluginEnabled,
|
|
@@ -89,14 +100,14 @@ describe("isPluginEnabled (per-namespace gate)", () => {
|
|
|
89
100
|
});
|
|
90
101
|
|
|
91
102
|
it("defaults to enabled when config is empty", () => {
|
|
92
|
-
expect(isPluginEnabled("
|
|
103
|
+
expect(isPluginEnabled("web")).toBe(true);
|
|
93
104
|
expect(isPluginEnabled("footer")).toBe(true);
|
|
94
105
|
expect(isPluginEnabled("diff")).toBe(true);
|
|
95
106
|
});
|
|
96
107
|
|
|
97
108
|
it("returns false only for the plugin disabled in its own namespace", () => {
|
|
98
|
-
mockStore["archimedes.
|
|
99
|
-
expect(isPluginEnabled("
|
|
109
|
+
mockStore["archimedes.web"] = { enabled: false };
|
|
110
|
+
expect(isPluginEnabled("web")).toBe(false);
|
|
100
111
|
expect(isPluginEnabled("footer")).toBe(true);
|
|
101
112
|
expect(isPluginEnabled("todo")).toBe(true);
|
|
102
113
|
});
|
|
@@ -186,6 +197,7 @@ describe("setPluginEnabled (save path)", () => {
|
|
|
186
197
|
|
|
187
198
|
describe("PLUGINS manifest integrity", () => {
|
|
188
199
|
const EXPECTED_IDS = [
|
|
200
|
+
"ui",
|
|
189
201
|
"footer",
|
|
190
202
|
"todo",
|
|
191
203
|
"ask",
|
|
@@ -194,11 +206,11 @@ describe("PLUGINS manifest integrity", () => {
|
|
|
194
206
|
"diff",
|
|
195
207
|
"image-paste",
|
|
196
208
|
"subagent",
|
|
197
|
-
"
|
|
209
|
+
"web",
|
|
198
210
|
"sudo",
|
|
199
211
|
];
|
|
200
212
|
|
|
201
|
-
it("lists exactly the
|
|
213
|
+
it("lists exactly the 11 non-core packages (no drift)", () => {
|
|
202
214
|
expect([...PLUGINS.map((p) => p.id)].sort()).toEqual([...EXPECTED_IDS].sort());
|
|
203
215
|
});
|
|
204
216
|
|
|
@@ -243,12 +255,12 @@ const ARROW_RIGHT = "\x1b[C";
|
|
|
243
255
|
|
|
244
256
|
/** All enabled — no `enabled` keys persisted anywhere. */
|
|
245
257
|
function fakeAllConfig(): Parameters<typeof buildSettingsItems>[0] {
|
|
246
|
-
// Minimal shape — buildSettingsItems only reads
|
|
258
|
+
// Minimal shape — buildSettingsItems only reads ui/notify/sessionName
|
|
247
259
|
// through the (mocked) item builders, so missing fields never surface.
|
|
248
260
|
return {
|
|
249
|
-
|
|
261
|
+
ui: {},
|
|
250
262
|
footer: { splitThreshold: 120 },
|
|
251
|
-
diff: { diffTheme: "github-dark", diffSplitMinWidth: 150, diffSplitMinCodeWidth: 60 },
|
|
263
|
+
diff: { diffTheme: "github-dark", diffSplitMinWidth: 150, diffSplitMinCodeWidth: 60, diffSplitWrapCheck: true },
|
|
252
264
|
notify: { delayMs: 30000, notifyOnAgentEnd: true, notifyOnQuestion: true },
|
|
253
265
|
sessionName: {},
|
|
254
266
|
} as unknown as Parameters<typeof buildSettingsItems>[0];
|
|
@@ -258,6 +270,7 @@ describe("buildSettingsItems (settings gate)", () => {
|
|
|
258
270
|
beforeEach(() => {
|
|
259
271
|
for (const key of Object.keys(mockStore)) delete mockStore[key];
|
|
260
272
|
vi.mocked(getDiffSettingsItems).mockClear();
|
|
273
|
+
vi.mocked(getUISettingsItems).mockClear();
|
|
261
274
|
});
|
|
262
275
|
|
|
263
276
|
it("includes all packages' items when everything is enabled, and lazy-imports diff", async () => {
|
|
@@ -272,9 +285,10 @@ describe("buildSettingsItems (settings gate)", () => {
|
|
|
272
285
|
mockStore["archimedes.diff"] = { enabled: false };
|
|
273
286
|
mockStore["archimedes.sessionName"] = { enabled: false };
|
|
274
287
|
mockStore["archimedes.notify"] = { enabled: false };
|
|
288
|
+
mockStore["archimedes.ui"] = { enabled: false };
|
|
275
289
|
const items = await buildSettingsItems(fakeAllConfig());
|
|
276
|
-
// Core items are
|
|
277
|
-
expect(items.map((i) => i.id)).toContain("mutedTheme");
|
|
290
|
+
// Core (UI) items are no longer automatically present
|
|
291
|
+
expect(items.map((i) => i.id)).not.toContain("mutedTheme");
|
|
278
292
|
// No item from a disabled package
|
|
279
293
|
for (const id of ["splitThreshold", "diffTheme", "delayMs", "sessionNameModel"]) {
|
|
280
294
|
expect(items.map((i) => i.id)).not.toContain(id);
|
package/src/plugins.ts
CHANGED
|
@@ -10,15 +10,16 @@
|
|
|
10
10
|
import { isConfigEnabled, setConfigEnabled, loadConfig, removeConfig } from "@pi-archimedes/core/settings-io";
|
|
11
11
|
|
|
12
12
|
export interface PluginDef {
|
|
13
|
-
id: string; // matches package npm name suffix, e.g. "
|
|
14
|
-
label: string; // human label, e.g. "
|
|
13
|
+
id: string; // matches package npm name suffix, e.g. "web"
|
|
14
|
+
label: string; // human label, e.g. "Web"
|
|
15
15
|
description: string; // one-liner shown in the menu
|
|
16
|
-
namespace: string; // "archimedes.
|
|
16
|
+
namespace: string; // "archimedes.web" — the settings.json key holding this plugin's { enabled, ...settings }
|
|
17
17
|
load: () => Promise<unknown>; // lazy import for instance probing + future lazy mount
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// Array order = menu display order. There is no separate order constant.
|
|
21
21
|
export const PLUGINS: PluginDef[] = [
|
|
22
|
+
{ id: "ui", label: "UI Enhancements", description: "Bash styling, editor spinner, thinking collapse, splash animation", namespace: "archimedes.ui", load: () => import("@pi-archimedes/ui") },
|
|
22
23
|
{ id: "footer", label: "Footer status bar", description: "Status bar with cost/timer", namespace: "archimedes.footer", load: () => import("@pi-archimedes/footer") },
|
|
23
24
|
{ id: "todo", label: "Todo list", description: "manage_todo_list tool + widget", namespace: "archimedes.todo", load: () => import("@pi-archimedes/todo") },
|
|
24
25
|
{ id: "ask", label: "Ask tool", description: "Structured in-conversation questions", namespace: "archimedes.ask", load: () => import("@pi-archimedes/ask") },
|
|
@@ -27,7 +28,7 @@ export const PLUGINS: PluginDef[] = [
|
|
|
27
28
|
{ id: "diff", label: "Diff rendering", description: "Shiki-powered diff display", namespace: "archimedes.diff", load: () => import("@pi-archimedes/diff") },
|
|
28
29
|
{ id: "image-paste", label: "Image paste", description: "Clipboard image paste", namespace: "archimedes.imagePaste", load: () => import("@pi-archimedes/image-paste") },
|
|
29
30
|
{ id: "subagent", label: "Subagents", description: "Live subagent dispatch (general, reviewer, …)", namespace: "archimedes.subagent", load: () => import("@pi-archimedes/subagent") },
|
|
30
|
-
{ id: "
|
|
31
|
+
{ id: "web", label: "Web", description: "Web search and content fetching with multi-provider routing", namespace: "archimedes.web", load: () => import("@pi-archimedes/web") },
|
|
31
32
|
{ id: "sudo", label: "Sudo", description: "Safe privileged execution", namespace: "archimedes.sudo", load: () => import("@pi-archimedes/sudo") },
|
|
32
33
|
];
|
|
33
34
|
|
package/src/settings.ts
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { OVERLAY_CHROME } from "@pi-archimedes/core/overlay";
|
|
4
|
+
import { getUISettingsItems, saveUIConfig, normalizeThinkingStyle, normalizeToolStyle, type UIConfig } from "@pi-archimedes/ui";
|
|
6
5
|
import { getFooterSettingsItems } from "@pi-archimedes/footer/config";
|
|
7
|
-
// diff (shiki) is lazy-loaded inside buildSettingsItems AND gated by the
|
|
8
|
-
// per-namespace plugin gate (archimedes.diff.enabled — see ADR 0012) —
|
|
9
|
-
// disabled means shiki is never imported
|
|
10
6
|
import { getNotifySettingsItems } from "@pi-archimedes/notify";
|
|
11
7
|
import { getSessionNameSettingsItems } from "@pi-archimedes/session-name";
|
|
12
8
|
import {
|
|
13
9
|
loadAllConfig,
|
|
14
|
-
saveCoreConfig,
|
|
15
10
|
saveFooterConfig,
|
|
16
11
|
saveDiffConfig,
|
|
17
12
|
saveNotifyConfig,
|
|
18
13
|
saveSessionNameConfig,
|
|
19
|
-
type CoreConfig,
|
|
20
14
|
type NotifyConfig,
|
|
21
15
|
} from "./config.js";
|
|
22
16
|
import { isPluginEnabled } from "./plugins.js";
|
|
17
|
+
import { OVERLAY_CHROME } from "@pi-archimedes/core/overlay";
|
|
23
18
|
import { createSettingsManager, type PromptDescriptor } from "./settings-manager.js";
|
|
24
19
|
|
|
25
20
|
// ── Free-input prompt descriptors (keyed by item.id) ───────────────────────
|
|
@@ -43,7 +38,11 @@ const PROMPTS: Record<string, PromptDescriptor> = {
|
|
|
43
38
|
// not leak back in through the settings overlay. The diff import (heavy —
|
|
44
39
|
// pulls in shiki) is lazy AND inside the gate: disabled diff is never loaded.
|
|
45
40
|
export async function buildSettingsItems(allConfig: ReturnType<typeof loadAllConfig>): Promise<SettingItem[]> {
|
|
46
|
-
const items: SettingItem[] = [
|
|
41
|
+
const items: SettingItem[] = [];
|
|
42
|
+
|
|
43
|
+
if (isPluginEnabled("ui")) {
|
|
44
|
+
items.push(...getUISettingsItems({ ...allConfig.ui }));
|
|
45
|
+
}
|
|
47
46
|
|
|
48
47
|
if (isPluginEnabled("footer")) {
|
|
49
48
|
items.push(...getFooterSettingsItems());
|
|
@@ -51,7 +50,7 @@ export async function buildSettingsItems(allConfig: ReturnType<typeof loadAllCon
|
|
|
51
50
|
|
|
52
51
|
if (isPluginEnabled("diff")) {
|
|
53
52
|
const { getDiffSettingsItems } = await import("@pi-archimedes/diff");
|
|
54
|
-
items.push(...getDiffSettingsItems());
|
|
53
|
+
items.push(...getDiffSettingsItems(allConfig.diff));
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
if (isPluginEnabled("notify")) {
|
|
@@ -76,7 +75,7 @@ export async function buildSettingsItems(allConfig: ReturnType<typeof loadAllCon
|
|
|
76
75
|
export async function openSettings(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
77
76
|
const allConfig = loadAllConfig();
|
|
78
77
|
|
|
79
|
-
const
|
|
78
|
+
const uiConfig: UIConfig = { ...allConfig.ui };
|
|
80
79
|
const notifyConfig: NotifyConfig = { ...allConfig.notify };
|
|
81
80
|
const footerConfig = { ...allConfig.footer };
|
|
82
81
|
const diffConfig = { ...allConfig.diff };
|
|
@@ -91,16 +90,20 @@ export async function openSettings(pi: ExtensionAPI, ctx: ExtensionContext): Pro
|
|
|
91
90
|
theme,
|
|
92
91
|
onChange: (id: string, newValue: string) => {
|
|
93
92
|
switch (id) {
|
|
94
|
-
// ──
|
|
95
|
-
case "
|
|
96
|
-
case "
|
|
97
|
-
case "
|
|
98
|
-
case "
|
|
99
|
-
case "
|
|
100
|
-
case "
|
|
101
|
-
case "
|
|
102
|
-
case "
|
|
103
|
-
case "
|
|
93
|
+
// ── UI settings ──
|
|
94
|
+
case "bashToolStyling": uiConfig.bashToolStyling = newValue === "On"; break;
|
|
95
|
+
case "mutedTheme": uiConfig.mutedTheme = newValue === "On"; break;
|
|
96
|
+
case "autoCollapseThinking": uiConfig.autoCollapseThinking = newValue === "On"; break;
|
|
97
|
+
case "thinkingStyle": uiConfig.thinkingStyle = normalizeThinkingStyle(newValue); break;
|
|
98
|
+
case "toolStyle": uiConfig.toolStyle = normalizeToolStyle(newValue); break;
|
|
99
|
+
case "codeUnindent": uiConfig.codeUnindent = newValue === "On"; break;
|
|
100
|
+
case "editorSpinBorder": uiConfig.editorSpinBorder = newValue === "On"; break;
|
|
101
|
+
case "editorSpinSpeed": uiConfig.editorSpinSpeed = newValue.toLowerCase() as UIConfig["editorSpinSpeed"]; break;
|
|
102
|
+
case "editorSpinStyle": uiConfig.editorSpinStyle = newValue.toLowerCase().replace(/ /g, "-") as UIConfig["editorSpinStyle"]; break;
|
|
103
|
+
case "editorSpinLabel": uiConfig.editorSpinLabel = newValue; break;
|
|
104
|
+
case "labelText": uiConfig.labelText = newValue; break;
|
|
105
|
+
case "labelColor": uiConfig.labelColor = newValue; break;
|
|
106
|
+
case "animationStyle": uiConfig.animationStyle = newValue as UIConfig["animationStyle"]; break;
|
|
104
107
|
|
|
105
108
|
// ── Footer settings ──
|
|
106
109
|
case "splitThreshold": {
|
|
@@ -121,6 +124,7 @@ export async function openSettings(pi: ExtensionAPI, ctx: ExtensionContext): Pro
|
|
|
121
124
|
if (Number.isFinite(v)) diffConfig.diffSplitMinCodeWidth = v;
|
|
122
125
|
break;
|
|
123
126
|
}
|
|
127
|
+
case "diffSplitWrapCheck": diffConfig.diffSplitWrapCheck = newValue === "On"; break;
|
|
124
128
|
|
|
125
129
|
// ── Notify settings ──
|
|
126
130
|
case "notifyOnAgentEnd": notifyConfig.notifyOnAgentEnd = newValue === "On"; break;
|
|
@@ -136,7 +140,7 @@ export async function openSettings(pi: ExtensionAPI, ctx: ExtensionContext): Pro
|
|
|
136
140
|
}
|
|
137
141
|
},
|
|
138
142
|
onSave: () => {
|
|
139
|
-
|
|
143
|
+
saveUIConfig(uiConfig);
|
|
140
144
|
saveFooterConfig(footerConfig);
|
|
141
145
|
saveDiffConfig(diffConfig);
|
|
142
146
|
saveNotifyConfig(notifyConfig);
|