@mrclrchtr/supi-settings 4.6.0 → 4.8.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 +8 -6
- package/node_modules/@mrclrchtr/supi-core/README.md +26 -34
- package/node_modules/@mrclrchtr/supi-core/package.json +4 -7
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +4 -6
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +0 -20
- package/node_modules/@mrclrchtr/supi-core/src/config/prompt-surface.ts +7 -1
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +0 -1
- package/node_modules/@mrclrchtr/supi-core/src/context.ts +1 -9
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +4 -6
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +54 -28
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +91 -125
- package/node_modules/@mrclrchtr/supi-core/src/settings.ts +10 -7
- package/package.json +2 -2
- package/src/api.ts +2 -1
- package/src/extension.ts +5 -2
- package/src/index.ts +1 -1
- package/{node_modules/@mrclrchtr/supi-core/src/settings → src/ui}/scoped-settings-list.ts +112 -93
- package/{node_modules/@mrclrchtr/supi-core/src/settings → src/ui}/settings-action-menu.ts +1 -2
- package/src/ui/settings-module-reader.ts +36 -0
- package/src/ui/settings-row-model.ts +30 -0
- package/{node_modules/@mrclrchtr/supi-core/src/settings → src/ui}/settings-submenus.ts +2 -2
- package/{node_modules/@mrclrchtr/supi-core/src/settings → src/ui}/settings-ui.ts +43 -34
- package/node_modules/@mrclrchtr/supi-core/src/context/context-messages.ts +0 -119
- package/node_modules/@mrclrchtr/supi-core/src/progress-widget.ts +0 -189
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-command.ts +0 -15
- package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +0 -3
- package/node_modules/@mrclrchtr/supi-core/src/tool-framework.ts +0 -192
|
@@ -1,42 +1,35 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// Replaces pi-tui's generic SettingsList with a source-aware list that
|
|
4
|
-
// renders source badges, dispatches Enter / Space / action-menu semantics,
|
|
5
|
-
// and delegates custom-field submenus.
|
|
1
|
+
// Source-aware settings list with scope actions and custom submenus.
|
|
6
2
|
|
|
7
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
Input,
|
|
6
|
+
Key,
|
|
7
|
+
matchesKey,
|
|
8
|
+
truncateToWidth,
|
|
9
|
+
visibleWidth,
|
|
10
|
+
wrapTextWithAnsi,
|
|
11
|
+
} from "@earendil-works/pi-tui";
|
|
12
|
+
import type { SettingsAction, SettingsModule, SettingsScope } from "@mrclrchtr/supi-core/settings";
|
|
9
13
|
import type { ThemeAccessor } from "./settings-action-menu.ts";
|
|
10
14
|
import {
|
|
11
15
|
buildActionMenu,
|
|
12
16
|
createActionMenuComponent,
|
|
13
17
|
getConcreteChoices,
|
|
14
18
|
} from "./settings-action-menu.ts";
|
|
15
|
-
import type
|
|
16
|
-
import
|
|
19
|
+
import { type LoadedSettingsModule, readSettingsModules } from "./settings-module-reader.ts";
|
|
20
|
+
import { filterSettingsRows, rowsFromModules, type ScopedRow } from "./settings-row-model.ts";
|
|
17
21
|
import { createInputSubmenu, createModelPickerSubmenu } from "./settings-submenus.ts";
|
|
18
22
|
|
|
19
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
-
// Scoped settings list
|
|
21
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
-
|
|
23
|
-
interface ScopedRow {
|
|
24
|
-
flatId: string;
|
|
25
|
-
sectionLabel: string;
|
|
26
|
-
field: ScopedFieldValue;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
23
|
interface SubmenuState {
|
|
30
24
|
component: {
|
|
31
25
|
render: (w: number) => string[];
|
|
32
26
|
invalidate?: () => void;
|
|
33
27
|
handleInput?: (data: string) => void;
|
|
34
28
|
};
|
|
35
|
-
onDone: () => void;
|
|
36
29
|
}
|
|
37
30
|
|
|
38
31
|
export class ScopedSettingsList {
|
|
39
|
-
private
|
|
32
|
+
private modules: SettingsModule[];
|
|
40
33
|
private scope: SettingsScope;
|
|
41
34
|
private cwd: string;
|
|
42
35
|
private ctx: ExtensionContext | undefined;
|
|
@@ -49,12 +42,14 @@ export class ScopedSettingsList {
|
|
|
49
42
|
private submenu: SubmenuState | null = null;
|
|
50
43
|
private searchInput?: Input;
|
|
51
44
|
private searchQuery = "";
|
|
45
|
+
private actionPending = false;
|
|
52
46
|
private cachedWidth?: number;
|
|
53
47
|
private cachedLines?: string[];
|
|
54
48
|
|
|
55
49
|
// biome-ignore lint/complexity/useMaxParams: component constructor needs all dependencies upfront for immutable wiring
|
|
56
50
|
constructor(
|
|
57
|
-
|
|
51
|
+
modules: SettingsModule[],
|
|
52
|
+
initial: LoadedSettingsModule[],
|
|
58
53
|
scope: SettingsScope,
|
|
59
54
|
cwd: string,
|
|
60
55
|
ctx: ExtensionContext | undefined,
|
|
@@ -63,7 +58,7 @@ export class ScopedSettingsList {
|
|
|
63
58
|
onCancel: () => void,
|
|
64
59
|
onError?: (message: string) => void,
|
|
65
60
|
) {
|
|
66
|
-
this.
|
|
61
|
+
this.modules = modules;
|
|
67
62
|
this.scope = scope;
|
|
68
63
|
this.cwd = cwd;
|
|
69
64
|
this.ctx = ctx;
|
|
@@ -71,16 +66,30 @@ export class ScopedSettingsList {
|
|
|
71
66
|
this.tui = tui;
|
|
72
67
|
this.onCancel = onCancel;
|
|
73
68
|
this.onError = onError;
|
|
74
|
-
this.rebuildRows();
|
|
69
|
+
this.rebuildRows(initial);
|
|
75
70
|
}
|
|
76
71
|
|
|
77
|
-
reload(scope: SettingsScope, cwd: string, ctx?: ExtensionContext): void {
|
|
72
|
+
async reload(scope: SettingsScope, cwd: string, ctx?: ExtensionContext): Promise<void> {
|
|
78
73
|
this.scope = scope;
|
|
79
74
|
this.cwd = cwd;
|
|
80
75
|
this.ctx = ctx;
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
|
|
76
|
+
this.actionPending = true;
|
|
77
|
+
try {
|
|
78
|
+
await this.refreshRows();
|
|
79
|
+
this.selectedIndex = Math.min(
|
|
80
|
+
this.selectedIndex,
|
|
81
|
+
Math.max(0, this.filteredRows().length - 1),
|
|
82
|
+
);
|
|
83
|
+
} finally {
|
|
84
|
+
this.actionPending = false;
|
|
85
|
+
this.invalidate();
|
|
86
|
+
this.tui.requestRender();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Return true while a setting editor, action menu, or persistence action owns input. */
|
|
91
|
+
hasOpenSubmenu(): boolean {
|
|
92
|
+
return this.submenu !== null || this.actionPending;
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
invalidate(): void {
|
|
@@ -100,6 +109,7 @@ export class ScopedSettingsList {
|
|
|
100
109
|
return this.cachedLines;
|
|
101
110
|
}
|
|
102
111
|
|
|
112
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: One pass handles filtering, grouping, selection, scrolling, and descriptions.
|
|
103
113
|
private renderList(width: number): string[] {
|
|
104
114
|
const lines: string[] = [];
|
|
105
115
|
if (this.searchInput) {
|
|
@@ -118,39 +128,61 @@ export class ScopedSettingsList {
|
|
|
118
128
|
lines.push(this.renderHint(width));
|
|
119
129
|
return lines;
|
|
120
130
|
}
|
|
121
|
-
const maxVisible = Math.min(displayRows.length
|
|
131
|
+
const maxVisible = Math.min(displayRows.length, 10);
|
|
122
132
|
const start = Math.max(
|
|
123
133
|
0,
|
|
124
134
|
Math.min(this.selectedIndex - Math.floor(maxVisible / 2), displayRows.length - maxVisible),
|
|
125
135
|
);
|
|
126
136
|
const end = Math.min(start + maxVisible, displayRows.length);
|
|
137
|
+
const maxLabelWidth = Math.min(
|
|
138
|
+
30,
|
|
139
|
+
Math.max(...displayRows.map((row) => visibleWidth(row.field.field.label))),
|
|
140
|
+
);
|
|
141
|
+
let previousSection: string | undefined;
|
|
127
142
|
for (let i = start; i < end; i++) {
|
|
128
143
|
const row = displayRows[i];
|
|
129
144
|
if (!row) continue;
|
|
145
|
+
if (row.moduleLabel !== previousSection) {
|
|
146
|
+
lines.push(
|
|
147
|
+
truncateToWidth(` ${this.theme.fg("muted", this.theme.bold(row.moduleLabel))}`, width),
|
|
148
|
+
);
|
|
149
|
+
previousSection = row.moduleLabel;
|
|
150
|
+
}
|
|
130
151
|
const isSelected = i === this.selectedIndex;
|
|
131
|
-
const prefix = isSelected ? "
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
152
|
+
const prefix = isSelected ? ` ${this.theme.fg("accent", "→ ")}` : " ";
|
|
153
|
+
const label = row.field.field.label.padEnd(
|
|
154
|
+
row.field.field.label.length + maxLabelWidth - visibleWidth(row.field.field.label),
|
|
155
|
+
);
|
|
156
|
+
const labelText = this.theme.fg(isSelected ? "accent" : "text", label);
|
|
157
|
+
const valueWidth = Math.max(0, width - 4 - maxLabelWidth - 2);
|
|
158
|
+
const value = truncateToWidth(row.field.displayValue, valueWidth, "");
|
|
159
|
+
const valueText = this.theme.fg(isSelected ? "accent" : "muted", value);
|
|
160
|
+
lines.push(truncateToWidth(`${prefix}${labelText} ${valueText}`, width));
|
|
137
161
|
}
|
|
138
162
|
if (start > 0 || end < displayRows.length) {
|
|
139
163
|
lines.push(this.theme.fg("dim", ` (${this.selectedIndex + 1}/${displayRows.length})`));
|
|
140
164
|
}
|
|
165
|
+
const description = displayRows[this.selectedIndex]?.field.field.description;
|
|
166
|
+
if (description) {
|
|
167
|
+
lines.push("");
|
|
168
|
+
for (const line of wrapTextWithAnsi(description, Math.max(1, width - 4))) {
|
|
169
|
+
lines.push(this.theme.fg("dim", ` ${line}`));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
141
172
|
lines.push("");
|
|
142
173
|
lines.push(this.renderHint(width));
|
|
143
174
|
return lines;
|
|
144
175
|
}
|
|
145
176
|
|
|
146
|
-
private renderHint(
|
|
177
|
+
private renderHint(width: number): string {
|
|
147
178
|
const hints = [];
|
|
148
179
|
if (this.searchInput) hints.push("Type to search");
|
|
149
|
-
hints.push("Enter actions", "Space cycle", "Esc close");
|
|
150
|
-
return this.theme.fg("dim", hints.join(" · "));
|
|
180
|
+
hints.push("Enter for actions", "Space to cycle", "Tab for scope", "Esc to close");
|
|
181
|
+
return truncateToWidth(this.theme.fg("dim", hints.join(" · ")), width);
|
|
151
182
|
}
|
|
152
183
|
|
|
153
184
|
handleInput(data: string): void {
|
|
185
|
+
if (this.actionPending) return;
|
|
154
186
|
if (this.submenu) {
|
|
155
187
|
this.submenu.component.handleInput?.(data);
|
|
156
188
|
this.invalidate();
|
|
@@ -208,29 +240,22 @@ export class ScopedSettingsList {
|
|
|
208
240
|
if (!this.searchInput) this.searchInput = new Input();
|
|
209
241
|
}
|
|
210
242
|
|
|
211
|
-
private rebuildRows(): void {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
this.
|
|
243
|
+
private rebuildRows(loaded: LoadedSettingsModule[]): void {
|
|
244
|
+
this.rows = rowsFromModules(loaded);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private async refreshRows(): Promise<void> {
|
|
248
|
+
const result = await readSettingsModules(this.modules, {
|
|
249
|
+
scope: this.scope,
|
|
250
|
+
cwd: this.cwd,
|
|
251
|
+
ctx: this.ctx,
|
|
252
|
+
});
|
|
253
|
+
this.rebuildRows(result.loaded);
|
|
254
|
+
for (const error of result.errors) this.onError?.(error);
|
|
223
255
|
}
|
|
224
256
|
|
|
225
257
|
private filteredRows(): ScopedRow[] {
|
|
226
|
-
|
|
227
|
-
const q = this.searchQuery.toLowerCase();
|
|
228
|
-
return this.rows.filter(
|
|
229
|
-
(r) =>
|
|
230
|
-
r.field.field.label.toLowerCase().includes(q) ||
|
|
231
|
-
r.field.field.key.toLowerCase().includes(q) ||
|
|
232
|
-
r.field.displayValue.toLowerCase().includes(q),
|
|
233
|
-
);
|
|
258
|
+
return filterSettingsRows(this.rows, this.searchQuery);
|
|
234
259
|
}
|
|
235
260
|
|
|
236
261
|
private activateSelected(): void {
|
|
@@ -242,11 +267,6 @@ export class ScopedSettingsList {
|
|
|
242
267
|
if (menu.length === 0) return;
|
|
243
268
|
this.submenu = {
|
|
244
269
|
component: createActionMenuComponent(menu, this.doneAction(row), this.theme),
|
|
245
|
-
onDone: () => {
|
|
246
|
-
this.submenu = null;
|
|
247
|
-
this.invalidate();
|
|
248
|
-
this.tui.requestRender();
|
|
249
|
-
},
|
|
250
270
|
};
|
|
251
271
|
this.invalidate();
|
|
252
272
|
this.tui.requestRender();
|
|
@@ -260,10 +280,8 @@ export class ScopedSettingsList {
|
|
|
260
280
|
this.tui.requestRender();
|
|
261
281
|
return;
|
|
262
282
|
}
|
|
263
|
-
if (action === "inherit") {
|
|
264
|
-
this.dispatchAction(row.flatId, { kind: "
|
|
265
|
-
} else if (action === "resetToDefault") {
|
|
266
|
-
this.dispatchAction(row.flatId, { kind: "resetToDefault" });
|
|
283
|
+
if (action === "inherit" || action === "resetToDefault") {
|
|
284
|
+
this.dispatchAction(row.flatId, { kind: "unset" });
|
|
267
285
|
} else if (action === "edit") {
|
|
268
286
|
this.openFreeInputSubmenu(row);
|
|
269
287
|
} else if (action.startsWith("set:")) {
|
|
@@ -295,7 +313,7 @@ export class ScopedSettingsList {
|
|
|
295
313
|
if (selectedValue !== undefined) {
|
|
296
314
|
this.dispatchAction(row.flatId, { kind: "set", value: selectedValue });
|
|
297
315
|
} else {
|
|
298
|
-
this.reload(this.scope, this.cwd, this.ctx);
|
|
316
|
+
void this.reload(this.scope, this.cwd, this.ctx);
|
|
299
317
|
}
|
|
300
318
|
this.invalidate();
|
|
301
319
|
this.tui.requestRender();
|
|
@@ -304,15 +322,7 @@ export class ScopedSettingsList {
|
|
|
304
322
|
this.cwd,
|
|
305
323
|
this.ctx,
|
|
306
324
|
);
|
|
307
|
-
this.submenu = {
|
|
308
|
-
component: comp,
|
|
309
|
-
onDone: () => {
|
|
310
|
-
this.submenu = null;
|
|
311
|
-
this.reload(this.scope, this.cwd, this.ctx);
|
|
312
|
-
this.invalidate();
|
|
313
|
-
this.tui.requestRender();
|
|
314
|
-
},
|
|
315
|
-
};
|
|
325
|
+
this.submenu = { component: comp };
|
|
316
326
|
} else if (row.field.field.kind === "modelPicker") {
|
|
317
327
|
this.submenu = {
|
|
318
328
|
component: createModelPickerSubmenu(
|
|
@@ -326,11 +336,6 @@ export class ScopedSettingsList {
|
|
|
326
336
|
this.ctx,
|
|
327
337
|
row.field.field,
|
|
328
338
|
),
|
|
329
|
-
onDone: () => {
|
|
330
|
-
this.submenu = null;
|
|
331
|
-
this.invalidate();
|
|
332
|
-
this.tui.requestRender();
|
|
333
|
-
},
|
|
334
339
|
};
|
|
335
340
|
} else {
|
|
336
341
|
const label =
|
|
@@ -344,30 +349,44 @@ export class ScopedSettingsList {
|
|
|
344
349
|
this.invalidate();
|
|
345
350
|
this.tui.requestRender();
|
|
346
351
|
}),
|
|
347
|
-
onDone: () => {
|
|
348
|
-
this.submenu = null;
|
|
349
|
-
this.invalidate();
|
|
350
|
-
this.tui.requestRender();
|
|
351
|
-
},
|
|
352
352
|
};
|
|
353
353
|
}
|
|
354
354
|
this.invalidate();
|
|
355
355
|
this.tui.requestRender();
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
-
private dispatchAction(flatId: string, action:
|
|
358
|
+
private dispatchAction(flatId: string, action: SettingsAction): void {
|
|
359
|
+
void this.runAction(flatId, action);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private async runAction(flatId: string, action: SettingsAction): Promise<void> {
|
|
359
363
|
const dotIndex = flatId.indexOf(".");
|
|
360
364
|
if (dotIndex === -1) return;
|
|
361
|
-
const
|
|
365
|
+
const moduleId = flatId.slice(0, dotIndex);
|
|
362
366
|
const fieldKey = flatId.slice(dotIndex + 1);
|
|
363
|
-
const
|
|
364
|
-
if (!
|
|
367
|
+
const module = this.modules.find((candidate) => candidate.id === moduleId);
|
|
368
|
+
if (!module) return;
|
|
369
|
+
|
|
370
|
+
this.actionPending = true;
|
|
365
371
|
try {
|
|
366
|
-
|
|
372
|
+
const result = await module.apply({
|
|
373
|
+
scope: this.scope,
|
|
374
|
+
cwd: this.cwd,
|
|
375
|
+
ctx: this.ctx,
|
|
376
|
+
fieldKey,
|
|
377
|
+
action,
|
|
378
|
+
});
|
|
379
|
+
if (result?.notice) {
|
|
380
|
+
this.ctx?.ui.notify(result.notice.message, result.notice.level);
|
|
381
|
+
}
|
|
382
|
+
await this.refreshRows();
|
|
367
383
|
} catch (err) {
|
|
368
384
|
this.onError?.(err instanceof Error ? err.message : String(err));
|
|
385
|
+
await this.refreshRows();
|
|
386
|
+
} finally {
|
|
387
|
+
this.actionPending = false;
|
|
388
|
+
this.invalidate();
|
|
389
|
+
this.tui.requestRender();
|
|
369
390
|
}
|
|
370
|
-
this.reload(this.scope, this.cwd, this.ctx);
|
|
371
|
-
this.tui.requestRender();
|
|
372
391
|
}
|
|
373
392
|
}
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { Container, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui";
|
|
8
|
-
import type { SettingsScope } from "
|
|
9
|
-
import type { ScopedFieldValue, SettingsField } from "./settings-schema.ts";
|
|
8
|
+
import type { ScopedFieldValue, SettingsField, SettingsScope } from "@mrclrchtr/supi-core/settings";
|
|
10
9
|
|
|
11
10
|
/** Theme accessor type matching the TUI custom() theme parameter. */
|
|
12
11
|
export type ThemeAccessor = Parameters<Parameters<ExtensionContext["ui"]["custom"]>[0]>[1];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SettingsContext,
|
|
3
|
+
SettingsModule,
|
|
4
|
+
SettingsSnapshot,
|
|
5
|
+
} from "@mrclrchtr/supi-core/settings";
|
|
6
|
+
|
|
7
|
+
export interface LoadedSettingsModule {
|
|
8
|
+
module: SettingsModule;
|
|
9
|
+
snapshot: SettingsSnapshot;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SettingsReadResult {
|
|
13
|
+
loaded: LoadedSettingsModule[];
|
|
14
|
+
errors: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Read independent settings modules without hiding successful modules when one fails. */
|
|
18
|
+
export async function readSettingsModules(
|
|
19
|
+
modules: SettingsModule[],
|
|
20
|
+
context: SettingsContext,
|
|
21
|
+
): Promise<SettingsReadResult> {
|
|
22
|
+
const results = await Promise.all(
|
|
23
|
+
modules.map(async (module) => {
|
|
24
|
+
try {
|
|
25
|
+
return { loaded: { module, snapshot: await module.read(context) } };
|
|
26
|
+
} catch (error) {
|
|
27
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
28
|
+
return { error: `${module.label}: ${message}` };
|
|
29
|
+
}
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
return {
|
|
33
|
+
loaded: results.flatMap((result) => (result.loaded ? [result.loaded] : [])),
|
|
34
|
+
errors: results.flatMap((result) => (result.error ? [result.error] : [])),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ScopedFieldValue } from "@mrclrchtr/supi-core/settings";
|
|
2
|
+
import type { LoadedSettingsModule } from "./settings-module-reader.ts";
|
|
3
|
+
|
|
4
|
+
export interface ScopedRow {
|
|
5
|
+
flatId: string;
|
|
6
|
+
moduleLabel: string;
|
|
7
|
+
field: ScopedFieldValue;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function rowsFromModules(loaded: LoadedSettingsModule[]): ScopedRow[] {
|
|
11
|
+
return loaded.flatMap(({ module, snapshot }) =>
|
|
12
|
+
snapshot.rows.map((field) => ({
|
|
13
|
+
flatId: `${module.id}.${field.field.key}`,
|
|
14
|
+
moduleLabel: module.label,
|
|
15
|
+
field,
|
|
16
|
+
})),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function filterSettingsRows(rows: ScopedRow[], query: string): ScopedRow[] {
|
|
21
|
+
if (!query) return rows;
|
|
22
|
+
const normalized = query.toLowerCase();
|
|
23
|
+
return rows.filter(
|
|
24
|
+
(row) =>
|
|
25
|
+
row.moduleLabel.toLowerCase().includes(normalized) ||
|
|
26
|
+
row.field.field.label.toLowerCase().includes(normalized) ||
|
|
27
|
+
row.field.field.key.toLowerCase().includes(normalized) ||
|
|
28
|
+
row.field.displayValue.toLowerCase().includes(normalized),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
SelectList,
|
|
14
14
|
Text,
|
|
15
15
|
} from "@earendil-works/pi-tui";
|
|
16
|
-
import { getSelectableModels } from "
|
|
17
|
-
import type { ModelPickerField } from "
|
|
16
|
+
import { getSelectableModels } from "@mrclrchtr/supi-core/model-selection";
|
|
17
|
+
import type { ModelPickerField } from "@mrclrchtr/supi-core/settings";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Creates a pi-tui Input-backed submenu component with enter-to-confirm
|
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
// Declarative settings
|
|
2
|
-
//
|
|
3
|
-
// Thin orchestration layer that collects settings contributions and opens the
|
|
4
|
-
// scoped settings list inside a pi-tui custom component overlay.
|
|
1
|
+
// Declarative settings screen for SuPi extensions.
|
|
5
2
|
|
|
6
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
DynamicBorder,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionContext,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
8
|
import { Container, Key, matchesKey, Text } from "@earendil-works/pi-tui";
|
|
8
|
-
import { ScopedSettingsList } from "./scoped-settings-list.ts";
|
|
9
|
-
import type { SettingsCollectionDiagnostic, SettingsScope } from "./settings-registry.ts";
|
|
10
9
|
import {
|
|
11
10
|
createSettingsContributionCollector,
|
|
11
|
+
type SettingsCollectionDiagnostic,
|
|
12
|
+
type SettingsScope,
|
|
12
13
|
SUPI_SETTINGS_COLLECT_EVENT,
|
|
13
|
-
} from "
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export { createInputSubmenu, createModelPickerSubmenu } from "./settings-submenus.ts";
|
|
17
|
-
|
|
18
|
-
// ── Overlay ────────────────────────────────────────────────────────────────
|
|
14
|
+
} from "@mrclrchtr/supi-core/settings";
|
|
15
|
+
import { ScopedSettingsList } from "./scoped-settings-list.ts";
|
|
16
|
+
import { readSettingsModules } from "./settings-module-reader.ts";
|
|
19
17
|
|
|
20
18
|
interface OverlayStatus {
|
|
21
19
|
kind: "warning" | "error";
|
|
@@ -28,11 +26,7 @@ interface OverlayState {
|
|
|
28
26
|
status?: OverlayStatus;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
function
|
|
32
|
-
return scope === "project" ? "Project" : "Global";
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function collectSettingsSections(pi: ExtensionAPI) {
|
|
29
|
+
function collectSettingsModules(pi: ExtensionAPI) {
|
|
36
30
|
const collector = createSettingsContributionCollector();
|
|
37
31
|
pi.events.emit(SUPI_SETTINGS_COLLECT_EVENT, collector);
|
|
38
32
|
return collector.result();
|
|
@@ -43,23 +37,32 @@ function latestStatus(diagnostics: SettingsCollectionDiagnostic[]): OverlayStatu
|
|
|
43
37
|
return latest ? { kind: latest.kind, message: latest.message } : undefined;
|
|
44
38
|
}
|
|
45
39
|
|
|
46
|
-
export function openSettingsOverlay(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
47
|
-
const collection =
|
|
48
|
-
if (collection.
|
|
40
|
+
export async function openSettingsOverlay(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
41
|
+
const collection = collectSettingsModules(pi);
|
|
42
|
+
if (collection.modules.length === 0) {
|
|
49
43
|
ctx.ui.notify("No settings registered by SuPi extensions", "info");
|
|
50
44
|
return;
|
|
51
45
|
}
|
|
52
46
|
|
|
47
|
+
const initial = await readSettingsModules(collection.modules, {
|
|
48
|
+
scope: "project",
|
|
49
|
+
cwd: ctx.cwd,
|
|
50
|
+
ctx,
|
|
51
|
+
});
|
|
52
|
+
const initialError = initial.errors.at(-1);
|
|
53
53
|
void ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
54
54
|
const state: OverlayState = {
|
|
55
55
|
scope: "project",
|
|
56
56
|
cwd: ctx.cwd,
|
|
57
|
-
status:
|
|
57
|
+
status: initialError
|
|
58
|
+
? { kind: "error", message: initialError }
|
|
59
|
+
: latestStatus(collection.diagnostics),
|
|
58
60
|
};
|
|
59
61
|
|
|
60
62
|
const container = new Container();
|
|
61
63
|
const scopedList = new ScopedSettingsList(
|
|
62
|
-
collection.
|
|
64
|
+
collection.modules,
|
|
65
|
+
initial.loaded,
|
|
63
66
|
state.scope,
|
|
64
67
|
state.cwd,
|
|
65
68
|
ctx,
|
|
@@ -76,35 +79,41 @@ export function openSettingsOverlay(pi: ExtensionAPI, ctx: ExtensionContext): vo
|
|
|
76
79
|
|
|
77
80
|
const rebuildOverlay = () => {
|
|
78
81
|
container.clear();
|
|
79
|
-
const
|
|
80
|
-
|
|
82
|
+
const scope = (label: string, value: SettingsScope) =>
|
|
83
|
+
value === state.scope
|
|
84
|
+
? theme.fg("accent", theme.bold(`[${label}]`))
|
|
85
|
+
: theme.fg("dim", label);
|
|
86
|
+
container.addChild(new DynamicBorder((text: string) => theme.fg("borderMuted", text)));
|
|
81
87
|
container.addChild(
|
|
82
88
|
new Text(
|
|
83
|
-
`${theme.fg("accent", theme.bold("SuPi Settings"))} ${theme.fg("
|
|
84
|
-
|
|
89
|
+
`${theme.fg("accent", theme.bold("SuPi Settings"))} ${theme.fg("dim", "Scope")} ${scope("Project", "project")} ${scope("Global", "global")}`,
|
|
90
|
+
1,
|
|
85
91
|
0,
|
|
86
92
|
),
|
|
87
93
|
);
|
|
88
94
|
if (state.status) {
|
|
89
|
-
container.addChild(new Text(theme.fg(state.status.kind, state.status.message),
|
|
95
|
+
container.addChild(new Text(theme.fg(state.status.kind, state.status.message), 1, 0));
|
|
90
96
|
}
|
|
97
|
+
container.addChild(scopedList);
|
|
98
|
+
container.addChild(new DynamicBorder((text: string) => theme.fg("borderMuted", text)));
|
|
91
99
|
};
|
|
92
100
|
|
|
93
101
|
rebuildOverlay();
|
|
94
102
|
|
|
95
103
|
const component = {
|
|
96
|
-
render: (width: number) =>
|
|
104
|
+
render: (width: number) => container.render(width),
|
|
97
105
|
invalidate: () => {
|
|
106
|
+
rebuildOverlay();
|
|
98
107
|
container.invalidate();
|
|
99
|
-
scopedList.invalidate();
|
|
100
108
|
},
|
|
101
109
|
handleInput: (data: string) => {
|
|
102
|
-
if (matchesKey(data, Key.tab)) {
|
|
110
|
+
if (matchesKey(data, Key.tab) && !scopedList.hasOpenSubmenu()) {
|
|
103
111
|
state.scope = state.scope === "project" ? "global" : "project";
|
|
104
112
|
state.status = undefined;
|
|
105
|
-
scopedList.reload(state.scope, state.cwd, ctx)
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
void scopedList.reload(state.scope, state.cwd, ctx).then(() => {
|
|
114
|
+
rebuildOverlay();
|
|
115
|
+
tui.requestRender();
|
|
116
|
+
});
|
|
108
117
|
return true;
|
|
109
118
|
}
|
|
110
119
|
scopedList.handleInput(data);
|