@eiei114/pi-sub-core 1.5.1
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/CHANGELOG.md +190 -0
- package/README.md +178 -0
- package/index.ts +540 -0
- package/package.json +35 -0
- package/src/cache.ts +546 -0
- package/src/config.ts +35 -0
- package/src/dependencies.ts +37 -0
- package/src/errors.ts +71 -0
- package/src/paths.ts +55 -0
- package/src/provider.ts +66 -0
- package/src/providers/detection.ts +51 -0
- package/src/providers/impl/anthropic.ts +174 -0
- package/src/providers/impl/antigravity.ts +226 -0
- package/src/providers/impl/codex.ts +186 -0
- package/src/providers/impl/copilot.ts +176 -0
- package/src/providers/impl/gemini.ts +130 -0
- package/src/providers/impl/kiro.ts +92 -0
- package/src/providers/impl/zai.ts +120 -0
- package/src/providers/index.ts +5 -0
- package/src/providers/metadata.ts +16 -0
- package/src/providers/registry.ts +54 -0
- package/src/providers/settings.ts +109 -0
- package/src/providers/status.ts +25 -0
- package/src/settings/behavior.ts +58 -0
- package/src/settings/menu.ts +83 -0
- package/src/settings/tools.ts +38 -0
- package/src/settings/ui.ts +450 -0
- package/src/settings-types.ts +95 -0
- package/src/settings-ui.ts +1 -0
- package/src/settings.ts +137 -0
- package/src/status.ts +245 -0
- package/src/storage/lock.ts +150 -0
- package/src/storage.ts +61 -0
- package/src/types.ts +33 -0
- package/src/ui/keybindings.ts +92 -0
- package/src/ui/settings-list.ts +290 -0
- package/src/usage/controller.ts +250 -0
- package/src/usage/fetch.ts +215 -0
- package/src/usage/types.ts +5 -0
- package/src/utils.ts +158 -0
- package/test/all.test.ts +9 -0
- package/test/cache.test.ts +157 -0
- package/test/controller.test.ts +101 -0
- package/test/detection.test.ts +24 -0
- package/test/extension.test.ts +233 -0
- package/test/helpers.ts +48 -0
- package/test/keybindings.test.ts +59 -0
- package/test/lock.test.ts +49 -0
- package/test/prioritize.test.ts +81 -0
- package/test/providers.test.ts +385 -0
- package/test/status.test.ts +70 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings UI for sub-core
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { DynamicBorder, getSettingsListTheme } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
import { Container, Input, type SelectItem, SelectList, Spacer, Text } from "@mariozechner/pi-tui";
|
|
8
|
+
import { SettingsList, type SettingItem, CUSTOM_OPTION } from "../ui/settings-list.js";
|
|
9
|
+
import type { ProviderName } from "../types.js";
|
|
10
|
+
import type { Settings } from "../settings-types.js";
|
|
11
|
+
import { getDefaultSettings } from "../settings-types.js";
|
|
12
|
+
import { getSettings, saveSettings, resetSettings } from "../settings.js";
|
|
13
|
+
import { PROVIDER_DISPLAY_NAMES } from "../providers/metadata.js";
|
|
14
|
+
import { buildProviderSettingsItems, applyProviderSettingsChange } from "../providers/settings.js";
|
|
15
|
+
import { buildRefreshItems, applyRefreshChange } from "./behavior.js";
|
|
16
|
+
import { buildToolItems, applyToolChange } from "./tools.js";
|
|
17
|
+
import { buildMainMenuItems, buildProviderListItems, buildProviderOrderItems, type TooltipSelectItem } from "./menu.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Settings category
|
|
21
|
+
*/
|
|
22
|
+
type ProviderCategory = `provider-${ProviderName}`;
|
|
23
|
+
|
|
24
|
+
type SettingsCategory =
|
|
25
|
+
| "main"
|
|
26
|
+
| "providers"
|
|
27
|
+
| ProviderCategory
|
|
28
|
+
| "behavior"
|
|
29
|
+
| "status-refresh"
|
|
30
|
+
| "tools"
|
|
31
|
+
| "provider-order";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extract provider name from category
|
|
35
|
+
*/
|
|
36
|
+
function getProviderFromCategory(category: SettingsCategory): ProviderName | null {
|
|
37
|
+
const match = category.match(/^provider-(\w+)$/);
|
|
38
|
+
if (match && match[1] !== "order") {
|
|
39
|
+
return match[1] as ProviderName;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Show the settings UI
|
|
46
|
+
*/
|
|
47
|
+
export async function showSettingsUI(
|
|
48
|
+
ctx: ExtensionContext,
|
|
49
|
+
onSettingsChange?: (settings: Settings) => void | Promise<void>
|
|
50
|
+
): Promise<Settings> {
|
|
51
|
+
let settings = getSettings();
|
|
52
|
+
let currentCategory: SettingsCategory = "main";
|
|
53
|
+
let providerOrderSelectedIndex = 0;
|
|
54
|
+
let providerOrderReordering = false;
|
|
55
|
+
let suppressProviderOrderChange = false;
|
|
56
|
+
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
ctx.ui.custom<Settings>((tui, theme, _kb, done) => {
|
|
59
|
+
let container = new Container();
|
|
60
|
+
let activeList: SelectList | SettingsList | null = null;
|
|
61
|
+
const clamp = (value: number, min: number, max: number): number => Math.min(max, Math.max(min, value));
|
|
62
|
+
|
|
63
|
+
const buildInputSubmenu = (
|
|
64
|
+
label: string,
|
|
65
|
+
parseValue: (value: string) => string | null,
|
|
66
|
+
formatInitial?: (value: string) => string,
|
|
67
|
+
) => {
|
|
68
|
+
return (currentValue: string, done: (selectedValue?: string) => void) => {
|
|
69
|
+
const input = new Input();
|
|
70
|
+
input.focused = true;
|
|
71
|
+
input.setValue(formatInitial ? formatInitial("") : "");
|
|
72
|
+
input.onSubmit = (value) => {
|
|
73
|
+
const parsed = parseValue(value);
|
|
74
|
+
if (!parsed) return;
|
|
75
|
+
done(parsed);
|
|
76
|
+
};
|
|
77
|
+
input.onEscape = () => {
|
|
78
|
+
done();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const inputContainer = new Container();
|
|
82
|
+
inputContainer.addChild(new Text(theme.fg("muted", label), 1, 0));
|
|
83
|
+
inputContainer.addChild(new Spacer(1));
|
|
84
|
+
inputContainer.addChild(input);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
render: (width: number) => inputContainer.render(width),
|
|
88
|
+
invalidate: () => inputContainer.invalidate(),
|
|
89
|
+
handleInput: (data: string) => input.handleInput(data),
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const parseRefreshInterval = (raw: string): string | null => {
|
|
95
|
+
const trimmed = raw.trim().toLowerCase();
|
|
96
|
+
if (!trimmed) {
|
|
97
|
+
ctx.ui.notify("Enter a value", "warning");
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
if (trimmed === "off") return "off";
|
|
101
|
+
const cleaned = trimmed.replace(/s$/, "");
|
|
102
|
+
const parsed = Number.parseInt(cleaned, 10);
|
|
103
|
+
if (Number.isNaN(parsed)) {
|
|
104
|
+
ctx.ui.notify("Enter seconds", "warning");
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const clamped = parsed <= 0 ? 0 : clamp(parsed, 5, 3600);
|
|
108
|
+
return clamped === 0 ? "off" : `${clamped}s`;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const parseMinRefreshInterval = (raw: string): string | null => {
|
|
112
|
+
const trimmed = raw.trim().toLowerCase();
|
|
113
|
+
if (!trimmed) {
|
|
114
|
+
ctx.ui.notify("Enter a value", "warning");
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
if (trimmed === "off") return "off";
|
|
118
|
+
const cleaned = trimmed.replace(/s$/, "");
|
|
119
|
+
const parsed = Number.parseInt(cleaned, 10);
|
|
120
|
+
if (Number.isNaN(parsed)) {
|
|
121
|
+
ctx.ui.notify("Enter seconds", "warning");
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const clamped = parsed <= 0 ? 0 : clamp(parsed, 5, 3600);
|
|
125
|
+
return clamped === 0 ? "off" : `${clamped}s`;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const parseCurrencySymbol = (raw: string): string | null => {
|
|
129
|
+
const trimmed = raw.trim();
|
|
130
|
+
if (!trimmed) {
|
|
131
|
+
ctx.ui.notify("Enter a symbol or 'none'", "warning");
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
if (trimmed.toLowerCase() === "none") return "none";
|
|
135
|
+
return trimmed;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
function rebuild(): void {
|
|
139
|
+
container = new Container();
|
|
140
|
+
let tooltipText: Text | null = null;
|
|
141
|
+
|
|
142
|
+
const attachTooltip = (items: TooltipSelectItem[], selectList: SelectList): void => {
|
|
143
|
+
if (!items.some((item) => item.tooltip)) return;
|
|
144
|
+
const tooltipComponent = new Text("", 1, 0);
|
|
145
|
+
const setTooltip = (item?: TooltipSelectItem | null) => {
|
|
146
|
+
const tooltip = item?.tooltip?.trim();
|
|
147
|
+
tooltipComponent.setText(tooltip ? theme.fg("dim", tooltip) : "");
|
|
148
|
+
};
|
|
149
|
+
setTooltip(selectList.getSelectedItem() as TooltipSelectItem | null);
|
|
150
|
+
const existingHandler = selectList.onSelectionChange;
|
|
151
|
+
selectList.onSelectionChange = (item) => {
|
|
152
|
+
if (existingHandler) existingHandler(item);
|
|
153
|
+
setTooltip(item as TooltipSelectItem);
|
|
154
|
+
tui.requestRender();
|
|
155
|
+
};
|
|
156
|
+
tooltipText = tooltipComponent;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Top border
|
|
160
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
161
|
+
|
|
162
|
+
// Title
|
|
163
|
+
const titles: Record<string, string> = {
|
|
164
|
+
main: "sub-core Settings",
|
|
165
|
+
providers: "Provider Settings",
|
|
166
|
+
behavior: "Usage Refresh Settings",
|
|
167
|
+
"status-refresh": "Status Refresh Settings",
|
|
168
|
+
tools: "Tool Settings",
|
|
169
|
+
"provider-order": "Provider Order",
|
|
170
|
+
};
|
|
171
|
+
const providerCategory = getProviderFromCategory(currentCategory);
|
|
172
|
+
const title = providerCategory
|
|
173
|
+
? `${PROVIDER_DISPLAY_NAMES[providerCategory]} Settings`
|
|
174
|
+
: titles[currentCategory] ?? "sub-core Settings";
|
|
175
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(title)), 1, 0));
|
|
176
|
+
container.addChild(new Spacer(1));
|
|
177
|
+
|
|
178
|
+
if (currentCategory === "main") {
|
|
179
|
+
const items = buildMainMenuItems(settings);
|
|
180
|
+
const selectList = new SelectList(items, Math.min(items.length, 10), {
|
|
181
|
+
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
182
|
+
selectedText: (t: string) => theme.fg("accent", t),
|
|
183
|
+
description: (t: string) => theme.fg("muted", t),
|
|
184
|
+
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
185
|
+
noMatch: (t: string) => theme.fg("warning", t),
|
|
186
|
+
});
|
|
187
|
+
attachTooltip(items, selectList);
|
|
188
|
+
selectList.onSelect = (item) => {
|
|
189
|
+
if (item.value === "reset") {
|
|
190
|
+
settings = resetSettings();
|
|
191
|
+
if (onSettingsChange) void onSettingsChange(settings);
|
|
192
|
+
ctx.ui.notify("Settings reset to defaults", "info");
|
|
193
|
+
rebuild();
|
|
194
|
+
tui.requestRender();
|
|
195
|
+
} else {
|
|
196
|
+
currentCategory = item.value as SettingsCategory;
|
|
197
|
+
rebuild();
|
|
198
|
+
tui.requestRender();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
selectList.onCancel = () => {
|
|
202
|
+
saveSettings(settings);
|
|
203
|
+
done(settings);
|
|
204
|
+
};
|
|
205
|
+
activeList = selectList;
|
|
206
|
+
container.addChild(selectList);
|
|
207
|
+
} else if (currentCategory === "providers") {
|
|
208
|
+
const items = buildProviderListItems(settings);
|
|
209
|
+
const selectList = new SelectList(items, Math.min(items.length, 10), {
|
|
210
|
+
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
211
|
+
selectedText: (t: string) => theme.fg("accent", t),
|
|
212
|
+
description: (t: string) => theme.fg("muted", t),
|
|
213
|
+
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
214
|
+
noMatch: (t: string) => theme.fg("warning", t),
|
|
215
|
+
});
|
|
216
|
+
attachTooltip(items, selectList);
|
|
217
|
+
selectList.onSelect = (item) => {
|
|
218
|
+
currentCategory = item.value as SettingsCategory;
|
|
219
|
+
rebuild();
|
|
220
|
+
tui.requestRender();
|
|
221
|
+
};
|
|
222
|
+
selectList.onCancel = () => {
|
|
223
|
+
currentCategory = "main";
|
|
224
|
+
rebuild();
|
|
225
|
+
tui.requestRender();
|
|
226
|
+
};
|
|
227
|
+
activeList = selectList;
|
|
228
|
+
container.addChild(selectList);
|
|
229
|
+
} else if (currentCategory === "provider-order") {
|
|
230
|
+
const items = buildProviderOrderItems(settings);
|
|
231
|
+
const isReordering = providerOrderReordering;
|
|
232
|
+
const selectList = new SelectList(items, Math.min(items.length, 10), {
|
|
233
|
+
selectedPrefix: (t: string) => isReordering ? theme.fg("warning", t) : theme.fg("accent", t),
|
|
234
|
+
selectedText: (t: string) => isReordering ? theme.fg("warning", t) : theme.fg("accent", t),
|
|
235
|
+
description: (t: string) => theme.fg("muted", t),
|
|
236
|
+
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
237
|
+
noMatch: (t: string) => theme.fg("warning", t),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
if (items.length > 0) {
|
|
241
|
+
suppressProviderOrderChange = true;
|
|
242
|
+
providerOrderSelectedIndex = Math.min(providerOrderSelectedIndex, items.length - 1);
|
|
243
|
+
selectList.setSelectedIndex(providerOrderSelectedIndex);
|
|
244
|
+
suppressProviderOrderChange = false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
selectList.onSelectionChange = (item) => {
|
|
248
|
+
if (suppressProviderOrderChange) return;
|
|
249
|
+
|
|
250
|
+
const newIndex = items.findIndex((listItem) => listItem.value === item.value);
|
|
251
|
+
if (newIndex === -1) return;
|
|
252
|
+
|
|
253
|
+
if (!providerOrderReordering) {
|
|
254
|
+
providerOrderSelectedIndex = newIndex;
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const activeProviders = settings.providerOrder.filter((provider) => {
|
|
259
|
+
const enabled = settings.providers[provider].enabled;
|
|
260
|
+
return enabled !== "off" && enabled !== false;
|
|
261
|
+
});
|
|
262
|
+
const oldIndex = providerOrderSelectedIndex;
|
|
263
|
+
if (newIndex === oldIndex) return;
|
|
264
|
+
if (oldIndex < 0 || oldIndex >= activeProviders.length) return;
|
|
265
|
+
|
|
266
|
+
const provider = activeProviders[oldIndex];
|
|
267
|
+
const updatedActive = [...activeProviders];
|
|
268
|
+
updatedActive.splice(oldIndex, 1);
|
|
269
|
+
updatedActive.splice(newIndex, 0, provider);
|
|
270
|
+
|
|
271
|
+
let activeIndex = 0;
|
|
272
|
+
settings.providerOrder = settings.providerOrder.map((existing) => {
|
|
273
|
+
const enabled = settings.providers[existing].enabled;
|
|
274
|
+
if (enabled === "off" || enabled === false) return existing;
|
|
275
|
+
const next = updatedActive[activeIndex];
|
|
276
|
+
activeIndex += 1;
|
|
277
|
+
return next;
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
providerOrderSelectedIndex = newIndex;
|
|
281
|
+
saveSettings(settings);
|
|
282
|
+
if (onSettingsChange) void onSettingsChange(settings);
|
|
283
|
+
rebuild();
|
|
284
|
+
tui.requestRender();
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
attachTooltip(items, selectList);
|
|
288
|
+
|
|
289
|
+
selectList.onSelect = () => {
|
|
290
|
+
if (items.length === 0) return;
|
|
291
|
+
providerOrderReordering = !providerOrderReordering;
|
|
292
|
+
rebuild();
|
|
293
|
+
tui.requestRender();
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
selectList.onCancel = () => {
|
|
297
|
+
if (providerOrderReordering) {
|
|
298
|
+
providerOrderReordering = false;
|
|
299
|
+
rebuild();
|
|
300
|
+
tui.requestRender();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
currentCategory = "main";
|
|
304
|
+
rebuild();
|
|
305
|
+
tui.requestRender();
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
activeList = selectList;
|
|
309
|
+
container.addChild(selectList);
|
|
310
|
+
} else {
|
|
311
|
+
let items: SettingItem[];
|
|
312
|
+
let handleChange: (id: string, value: string) => void;
|
|
313
|
+
let backCategory: SettingsCategory = "main";
|
|
314
|
+
|
|
315
|
+
const provider = getProviderFromCategory(currentCategory);
|
|
316
|
+
if (provider) {
|
|
317
|
+
items = buildProviderSettingsItems(settings, provider);
|
|
318
|
+
const customHandlers: Record<string, ReturnType<typeof buildInputSubmenu>> = {};
|
|
319
|
+
if (provider === "anthropic") {
|
|
320
|
+
customHandlers.extraUsageCurrencySymbol = buildInputSubmenu(
|
|
321
|
+
"Extra Usage Currency Symbol",
|
|
322
|
+
parseCurrencySymbol,
|
|
323
|
+
undefined,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
for (const item of items) {
|
|
327
|
+
if (item.values?.includes(CUSTOM_OPTION) && customHandlers[item.id]) {
|
|
328
|
+
item.submenu = customHandlers[item.id];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
handleChange = (id, value) => {
|
|
332
|
+
settings = applyProviderSettingsChange(settings, provider, id, value);
|
|
333
|
+
saveSettings(settings);
|
|
334
|
+
if (onSettingsChange) void onSettingsChange(settings);
|
|
335
|
+
};
|
|
336
|
+
backCategory = "providers";
|
|
337
|
+
} else if (currentCategory === "tools") {
|
|
338
|
+
items = buildToolItems(settings.tools);
|
|
339
|
+
handleChange = (id, value) => {
|
|
340
|
+
settings = applyToolChange(settings, id, value);
|
|
341
|
+
saveSettings(settings);
|
|
342
|
+
if (onSettingsChange) void onSettingsChange(settings);
|
|
343
|
+
};
|
|
344
|
+
backCategory = "main";
|
|
345
|
+
} else {
|
|
346
|
+
const refreshTarget = currentCategory === "status-refresh" ? settings.statusRefresh : settings.behavior;
|
|
347
|
+
items = buildRefreshItems(refreshTarget);
|
|
348
|
+
const customHandlers: Record<string, ReturnType<typeof buildInputSubmenu>> = {
|
|
349
|
+
refreshInterval: buildInputSubmenu("Auto-refresh Interval (seconds)", parseRefreshInterval),
|
|
350
|
+
minRefreshInterval: buildInputSubmenu("Minimum Refresh Interval (seconds)", parseMinRefreshInterval),
|
|
351
|
+
};
|
|
352
|
+
for (const item of items) {
|
|
353
|
+
if (item.values?.includes(CUSTOM_OPTION) && customHandlers[item.id]) {
|
|
354
|
+
item.submenu = customHandlers[item.id];
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
handleChange = (id, value) => {
|
|
358
|
+
applyRefreshChange(refreshTarget, id, value);
|
|
359
|
+
saveSettings(settings);
|
|
360
|
+
if (onSettingsChange) void onSettingsChange(settings);
|
|
361
|
+
};
|
|
362
|
+
backCategory = "main";
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const settingsHintText = "↓ navigate • ←/→ change • Enter/Space edit custom • Esc to cancel";
|
|
366
|
+
const customTheme = {
|
|
367
|
+
...getSettingsListTheme(),
|
|
368
|
+
hint: (text: string) => {
|
|
369
|
+
if (text.includes("Enter/Space")) {
|
|
370
|
+
return theme.fg("dim", settingsHintText);
|
|
371
|
+
}
|
|
372
|
+
return theme.fg("dim", text);
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
const settingsList = new SettingsList(
|
|
376
|
+
items,
|
|
377
|
+
Math.min(items.length + 2, 15),
|
|
378
|
+
customTheme,
|
|
379
|
+
handleChange,
|
|
380
|
+
() => {
|
|
381
|
+
currentCategory = backCategory;
|
|
382
|
+
rebuild();
|
|
383
|
+
tui.requestRender();
|
|
384
|
+
}
|
|
385
|
+
);
|
|
386
|
+
activeList = settingsList;
|
|
387
|
+
container.addChild(settingsList);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const usesSettingsList =
|
|
391
|
+
currentCategory === "behavior" ||
|
|
392
|
+
currentCategory === "status-refresh" ||
|
|
393
|
+
currentCategory === "tools" ||
|
|
394
|
+
getProviderFromCategory(currentCategory) !== null;
|
|
395
|
+
if (!usesSettingsList) {
|
|
396
|
+
let helpText: string;
|
|
397
|
+
if (currentCategory === "main" || currentCategory === "providers") {
|
|
398
|
+
helpText = "↑↓ navigate • Enter/Space select • Esc back";
|
|
399
|
+
} else if (currentCategory === "provider-order") {
|
|
400
|
+
helpText = providerOrderReordering
|
|
401
|
+
? "↑↓ move provider • Esc back"
|
|
402
|
+
: "↑↓ navigate • Enter/Space select • Esc back";
|
|
403
|
+
} else {
|
|
404
|
+
helpText = "↑↓ navigate • Enter/Space to change • Esc to cancel";
|
|
405
|
+
}
|
|
406
|
+
if (tooltipText) {
|
|
407
|
+
container.addChild(new Spacer(1));
|
|
408
|
+
container.addChild(tooltipText);
|
|
409
|
+
}
|
|
410
|
+
container.addChild(new Spacer(1));
|
|
411
|
+
container.addChild(new Text(theme.fg("dim", helpText), 1, 0));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Bottom border
|
|
415
|
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
rebuild();
|
|
420
|
+
|
|
421
|
+
return {
|
|
422
|
+
render(width: number) {
|
|
423
|
+
return container.render(width);
|
|
424
|
+
},
|
|
425
|
+
invalidate() {
|
|
426
|
+
container.invalidate();
|
|
427
|
+
},
|
|
428
|
+
handleInput(data: string) {
|
|
429
|
+
if (data === " ") {
|
|
430
|
+
if (currentCategory === "provider-order") {
|
|
431
|
+
providerOrderReordering = !providerOrderReordering;
|
|
432
|
+
rebuild();
|
|
433
|
+
tui.requestRender();
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (activeList && "handleInput" in activeList && activeList.handleInput) {
|
|
437
|
+
activeList.handleInput("\r");
|
|
438
|
+
}
|
|
439
|
+
tui.requestRender();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (activeList && "handleInput" in activeList && activeList.handleInput) {
|
|
443
|
+
activeList.handleInput(data);
|
|
444
|
+
}
|
|
445
|
+
tui.requestRender();
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}).then(resolve);
|
|
449
|
+
});
|
|
450
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings types and defaults for sub-core
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
CoreSettings,
|
|
7
|
+
CoreProviderSettingsMap,
|
|
8
|
+
CoreProviderSettings,
|
|
9
|
+
BehaviorSettings,
|
|
10
|
+
ProviderName,
|
|
11
|
+
ProviderEnabledSetting,
|
|
12
|
+
} from "@eiei114/pi-sub-shared";
|
|
13
|
+
import { PROVIDERS, getDefaultCoreSettings } from "@eiei114/pi-sub-shared";
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
CoreProviderSettings,
|
|
17
|
+
CoreProviderSettingsMap,
|
|
18
|
+
BehaviorSettings,
|
|
19
|
+
CoreSettings,
|
|
20
|
+
ProviderEnabledSetting,
|
|
21
|
+
} from "@eiei114/pi-sub-shared";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Tool registration settings
|
|
25
|
+
*/
|
|
26
|
+
export interface ToolSettings {
|
|
27
|
+
usageTool: boolean;
|
|
28
|
+
allUsageTool: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* All settings
|
|
33
|
+
*/
|
|
34
|
+
export interface Settings extends CoreSettings {
|
|
35
|
+
/** Version for migration */
|
|
36
|
+
version: number;
|
|
37
|
+
/** Tool registration settings */
|
|
38
|
+
tools: ToolSettings;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Current settings version
|
|
43
|
+
*/
|
|
44
|
+
export const SETTINGS_VERSION = 3;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Default settings
|
|
48
|
+
*/
|
|
49
|
+
export function getDefaultSettings(): Settings {
|
|
50
|
+
const coreDefaults = getDefaultCoreSettings();
|
|
51
|
+
return {
|
|
52
|
+
version: SETTINGS_VERSION,
|
|
53
|
+
tools: {
|
|
54
|
+
usageTool: false,
|
|
55
|
+
allUsageTool: false,
|
|
56
|
+
},
|
|
57
|
+
providers: coreDefaults.providers,
|
|
58
|
+
behavior: coreDefaults.behavior,
|
|
59
|
+
statusRefresh: coreDefaults.statusRefresh,
|
|
60
|
+
providerOrder: coreDefaults.providerOrder,
|
|
61
|
+
defaultProvider: coreDefaults.defaultProvider,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Deep merge two objects
|
|
67
|
+
*/
|
|
68
|
+
function deepMerge<T extends object>(target: T, source: Partial<T>): T {
|
|
69
|
+
const result = { ...target } as T;
|
|
70
|
+
for (const key of Object.keys(source) as (keyof T)[]) {
|
|
71
|
+
const sourceValue = source[key];
|
|
72
|
+
const targetValue = result[key];
|
|
73
|
+
if (
|
|
74
|
+
sourceValue !== undefined &&
|
|
75
|
+
typeof sourceValue === "object" &&
|
|
76
|
+
sourceValue !== null &&
|
|
77
|
+
!Array.isArray(sourceValue) &&
|
|
78
|
+
typeof targetValue === "object" &&
|
|
79
|
+
targetValue !== null &&
|
|
80
|
+
!Array.isArray(targetValue)
|
|
81
|
+
) {
|
|
82
|
+
result[key] = deepMerge(targetValue as object, sourceValue as object) as T[keyof T];
|
|
83
|
+
} else if (sourceValue !== undefined) {
|
|
84
|
+
result[key] = sourceValue as T[keyof T];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Merge settings with defaults (no legacy migrations).
|
|
92
|
+
*/
|
|
93
|
+
export function mergeSettings(loaded: Partial<Settings>): Settings {
|
|
94
|
+
return deepMerge(getDefaultSettings(), loaded);
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { showSettingsUI } from "./settings/ui.js";
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings persistence for sub-core
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import type { Settings } from "./settings-types.js";
|
|
7
|
+
import { getDefaultSettings, mergeSettings, SETTINGS_VERSION } from "./settings-types.js";
|
|
8
|
+
import { getStorage } from "./storage.js";
|
|
9
|
+
import { getLegacySettingsPath, getSettingsPath } from "./paths.js";
|
|
10
|
+
import { clearCache } from "./cache.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Settings file path
|
|
14
|
+
*/
|
|
15
|
+
export const SETTINGS_PATH = getSettingsPath();
|
|
16
|
+
const LEGACY_SETTINGS_PATH = getLegacySettingsPath();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* In-memory settings cache
|
|
20
|
+
*/
|
|
21
|
+
let cachedSettings: Settings | undefined;
|
|
22
|
+
|
|
23
|
+
type LoadedSettings = {
|
|
24
|
+
settings: Settings;
|
|
25
|
+
loadedVersion: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Ensure the settings directory exists
|
|
30
|
+
*/
|
|
31
|
+
function ensureSettingsDir(): void {
|
|
32
|
+
const storage = getStorage();
|
|
33
|
+
const dir = path.dirname(SETTINGS_PATH);
|
|
34
|
+
storage.ensureDir(dir);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function loadSettingsFromDisk(settingsPath: string): LoadedSettings | null {
|
|
38
|
+
const storage = getStorage();
|
|
39
|
+
if (!storage.exists(settingsPath)) return null;
|
|
40
|
+
const content = storage.readFile(settingsPath);
|
|
41
|
+
if (!content) return null;
|
|
42
|
+
const loaded = JSON.parse(content) as Partial<Settings>;
|
|
43
|
+
const loadedVersion = typeof loaded.version === "number" ? loaded.version : 0;
|
|
44
|
+
const merged = mergeSettings(loaded);
|
|
45
|
+
return { settings: merged, loadedVersion };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function applyVersionMigration(settings: Settings, loadedVersion: number): { settings: Settings; needsSave: boolean } {
|
|
49
|
+
if (loadedVersion < SETTINGS_VERSION) {
|
|
50
|
+
clearCache();
|
|
51
|
+
return { settings: { ...settings, version: SETTINGS_VERSION }, needsSave: true };
|
|
52
|
+
}
|
|
53
|
+
return { settings, needsSave: false };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function tryLoadSettings(settingsPath: string): LoadedSettings | null {
|
|
57
|
+
try {
|
|
58
|
+
return loadSettingsFromDisk(settingsPath);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(`Failed to load settings from ${settingsPath}:`, error);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Load settings from disk
|
|
67
|
+
*/
|
|
68
|
+
export function loadSettings(): Settings {
|
|
69
|
+
if (cachedSettings) {
|
|
70
|
+
return cachedSettings;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const diskSettings = tryLoadSettings(SETTINGS_PATH);
|
|
74
|
+
if (diskSettings) {
|
|
75
|
+
const { settings: next, needsSave } = applyVersionMigration(diskSettings.settings, diskSettings.loadedVersion);
|
|
76
|
+
if (needsSave) {
|
|
77
|
+
saveSettings(next);
|
|
78
|
+
}
|
|
79
|
+
cachedSettings = next;
|
|
80
|
+
return cachedSettings;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const legacySettings = tryLoadSettings(LEGACY_SETTINGS_PATH);
|
|
84
|
+
if (legacySettings) {
|
|
85
|
+
const { settings: next } = applyVersionMigration(legacySettings.settings, legacySettings.loadedVersion);
|
|
86
|
+
const saved = saveSettings(next);
|
|
87
|
+
if (saved) {
|
|
88
|
+
getStorage().removeFile(LEGACY_SETTINGS_PATH);
|
|
89
|
+
}
|
|
90
|
+
cachedSettings = next;
|
|
91
|
+
return cachedSettings;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Return defaults if file doesn't exist or failed to load
|
|
95
|
+
cachedSettings = getDefaultSettings();
|
|
96
|
+
return cachedSettings;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Save settings to disk
|
|
101
|
+
*/
|
|
102
|
+
export function saveSettings(settings: Settings): boolean {
|
|
103
|
+
const storage = getStorage();
|
|
104
|
+
try {
|
|
105
|
+
ensureSettingsDir();
|
|
106
|
+
const content = JSON.stringify(settings, null, 2);
|
|
107
|
+
storage.writeFile(SETTINGS_PATH, content);
|
|
108
|
+
cachedSettings = settings;
|
|
109
|
+
return true;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(`Failed to save settings to ${SETTINGS_PATH}:`, error);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Reset settings to defaults
|
|
118
|
+
*/
|
|
119
|
+
export function resetSettings(): Settings {
|
|
120
|
+
const defaults = getDefaultSettings();
|
|
121
|
+
saveSettings(defaults);
|
|
122
|
+
return defaults;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get current settings (cached)
|
|
127
|
+
*/
|
|
128
|
+
export function getSettings(): Settings {
|
|
129
|
+
return loadSettings();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Clear the settings cache (force reload on next access)
|
|
134
|
+
*/
|
|
135
|
+
export function clearSettingsCache(): void {
|
|
136
|
+
cachedSettings = undefined;
|
|
137
|
+
}
|