@aeondave/pi-persona 1.16.3 → 1.17.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/AGENTS.md +22 -3
- package/CHANGELOG.md +56 -0
- package/README.md +18 -2
- package/docs/ARCHITECTURE.md +58 -7
- package/docs/REFERENCE.md +125 -3
- package/docs/TELEMETRY.md +8 -0
- package/package.json +2 -2
- package/src/core/config.ts +1 -1
- package/src/core/settings.ts +251 -0
- package/src/engine/adapter.ts +1 -1
- package/src/engine/async.ts +53 -13
- package/src/engine/child.ts +1 -1
- package/src/engine/errors.ts +1 -0
- package/src/engine/inproc.ts +3 -3
- package/src/engine/stream.ts +20 -1
- package/src/extension/companion-settings.ts +29 -0
- package/src/extension/hooks.ts +47 -12
- package/src/extension/persona-model-settings.ts +318 -0
- package/src/extension/persona-settings.ts +459 -0
- package/src/extension.ts +257 -50
- package/src/orchestration/sdk.ts +2 -0
- package/src/persona/config-store.ts +6 -195
- package/src/persona/controller.ts +16 -3
- package/src/persona/model-participants.ts +1 -1
- package/src/persona/settings-store.ts +636 -0
- package/src/telemetry/contract.ts +7 -5
- package/src/tools/delegate-tool.ts +6 -6
- package/src/tools/delegate.ts +3 -1
- package/src/tools/exocom.ts +14 -1
- package/src/ui/persona-settings.ts +322 -0
- package/src/persona/state.ts +0 -28
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import type { PiPersonaConfig } from "../core/config.ts";
|
|
4
|
+
import {
|
|
5
|
+
parseSettingInput,
|
|
6
|
+
PERSONA_SETTINGS,
|
|
7
|
+
resolveSettingsValues,
|
|
8
|
+
settingHasEnvironmentOverride,
|
|
9
|
+
type PersonaPreferences,
|
|
10
|
+
type PersonaSetting,
|
|
11
|
+
type SettingId,
|
|
12
|
+
} from "../core/settings.ts";
|
|
13
|
+
import {
|
|
14
|
+
formatSettingValue,
|
|
15
|
+
PersonaSettingsPanel,
|
|
16
|
+
type SettingsPanelAction,
|
|
17
|
+
type SettingsPanelRow,
|
|
18
|
+
} from "../ui/persona-settings.ts";
|
|
19
|
+
import { compactInlineText } from "../ui/presentation.ts";
|
|
20
|
+
import type { CompanionSettingsSnapshot } from "./companion-settings.ts";
|
|
21
|
+
|
|
22
|
+
export interface PersonaSettingsDeps {
|
|
23
|
+
file: string;
|
|
24
|
+
activeConfig: PiPersonaConfig;
|
|
25
|
+
read: () => { preferences: PersonaPreferences; lastPersona: string | null };
|
|
26
|
+
write: (patch: {
|
|
27
|
+
preferences?: Partial<Record<SettingId, unknown | null>>;
|
|
28
|
+
lastPersona?: string | null;
|
|
29
|
+
}) => void | Promise<void>;
|
|
30
|
+
env?: Record<string, string | undefined>;
|
|
31
|
+
models: (ctx: ExtensionContext) => Promise<void>;
|
|
32
|
+
diagnostics?: () => string;
|
|
33
|
+
isCurrent?: (ctx: ExtensionContext) => boolean;
|
|
34
|
+
/** Current values supplied only by companions actually loaded in this Pi session. */
|
|
35
|
+
companions?: () => CompanionSettingsSnapshot;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface SettingsSnapshot {
|
|
39
|
+
preferences: PersonaPreferences;
|
|
40
|
+
lastPersona: string | null;
|
|
41
|
+
rows: SettingsPanelRow[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const COMMAND = "persona-settings";
|
|
45
|
+
|
|
46
|
+
function hasEnvOverride(setting: PersonaSetting, env: Record<string, string | undefined>): boolean {
|
|
47
|
+
return settingHasEnvironmentOverride(setting, env);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function safeText(value: string, limit = 320): string {
|
|
51
|
+
return compactInlineText(value, { maxChars: limit });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function makeSnapshot(deps: PersonaSettingsDeps): SettingsSnapshot {
|
|
55
|
+
const state = deps.read();
|
|
56
|
+
const env = deps.env ?? process.env;
|
|
57
|
+
const preferences = state.preferences;
|
|
58
|
+
const saved = preferences as Record<string, unknown>;
|
|
59
|
+
const companions = deps.companions?.() ?? {};
|
|
60
|
+
const active = { ...deps.activeConfig, ...companions.mind, ...companions.flow } as unknown as Record<string, unknown>;
|
|
61
|
+
const next = resolveSettingsValues(preferences, env) as unknown as Record<string, unknown>;
|
|
62
|
+
const rows = PERSONA_SETTINGS.filter((setting) => !setting.owner ||
|
|
63
|
+
(companions[setting.owner] !== undefined && Object.hasOwn(companions[setting.owner]!, setting.id))).map((setting): SettingsPanelRow => {
|
|
64
|
+
const hasSavedValue = Object.hasOwn(saved, setting.id);
|
|
65
|
+
const locked = hasEnvOverride(setting, env);
|
|
66
|
+
return {
|
|
67
|
+
id: setting.id,
|
|
68
|
+
group: setting.group,
|
|
69
|
+
label: setting.label,
|
|
70
|
+
description: setting.description,
|
|
71
|
+
kind: setting.kind,
|
|
72
|
+
...(setting.choices ? { choices: setting.choices } : {}),
|
|
73
|
+
activeValue: active[setting.id],
|
|
74
|
+
savedValue: hasSavedValue ? saved[setting.id] : undefined,
|
|
75
|
+
hasSavedValue,
|
|
76
|
+
source: locked ? "environment" : hasSavedValue ? "saved" : "default",
|
|
77
|
+
locked,
|
|
78
|
+
...(!equalSettingValues(setting.id, next[setting.id], active[setting.id]) ? { pendingReload: true } : {}),
|
|
79
|
+
applyWhen: setting.applyWhen,
|
|
80
|
+
...(setting.min !== undefined ? { min: setting.min } : {}),
|
|
81
|
+
...(setting.max !== undefined ? { max: setting.max } : {}),
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
return { preferences, lastPersona: state.lastPersona, rows };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function settingById(id: SettingId): PersonaSetting | undefined {
|
|
88
|
+
return PERSONA_SETTINGS.find((setting) => setting.id === id);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function formatSettingsReport(deps: PersonaSettingsDeps, snapshot = makeSnapshot(deps)): string {
|
|
92
|
+
const lines = [
|
|
93
|
+
`Persona settings · ${safeText(deps.file, 500)}`,
|
|
94
|
+
`Remembered persona: ${snapshot.lastPersona ? safeText(snapshot.lastPersona, 160) : "none"}`,
|
|
95
|
+
];
|
|
96
|
+
for (const row of snapshot.rows) {
|
|
97
|
+
const saved = row.hasSavedValue ? formatSettingValue(row.savedValue) : "none";
|
|
98
|
+
const env = row.locked ? ` · locked by ${settingById(row.id)?.env ?? "environment"}` : "";
|
|
99
|
+
const pending = row.pendingReload ? " · pending /reload" : "";
|
|
100
|
+
lines.push(
|
|
101
|
+
`${row.id}: active=${formatSettingValue(row.activeValue)}; saved=${saved}; source=${row.source}${pending}; applies=${safeText(row.applyWhen, 120)}${env}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (deps.diagnostics) {
|
|
105
|
+
try {
|
|
106
|
+
const diagnostic = deps.diagnostics();
|
|
107
|
+
if (diagnostic.trim()) lines.push(`Diagnostics: ${safeText(diagnostic, 1000)}`);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
lines.push(`Diagnostics unavailable: ${errorText(error)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function safeSettingsReport(deps: PersonaSettingsDeps): string {
|
|
116
|
+
try {
|
|
117
|
+
return formatSettingsReport(deps);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return `Could not read persona settings: ${errorText(error)}.`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function errorText(error: unknown): string {
|
|
124
|
+
return safeText(error instanceof Error ? error.message : String(error), 240) || "unknown error";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function report(ctx: ExtensionContext, message: string, type: "info" | "warning" | "error" = "info"): void {
|
|
128
|
+
if (ctx.hasUI) ctx.ui.notify(message, type);
|
|
129
|
+
else console.error(message);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function preferencesPatch(id: SettingId, value: unknown | null): Partial<Record<SettingId, unknown | null>> {
|
|
133
|
+
return { [id]: value } as Partial<Record<SettingId, unknown | null>>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function rowValue(row: SettingsPanelRow | undefined): unknown {
|
|
137
|
+
if (!row) return undefined;
|
|
138
|
+
return row.hasSavedValue ? row.savedValue : row.activeValue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function equalValues(left: unknown, right: unknown): boolean {
|
|
142
|
+
if (Object.is(left, right)) return true;
|
|
143
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
144
|
+
return left.length === right.length && left.every((value, index) => Object.is(value, right[index]));
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function equalSettingValues(id: SettingId, left: unknown, right: unknown): boolean {
|
|
150
|
+
const normalize = (value: unknown): unknown => {
|
|
151
|
+
if ((id === "spine" || id === "spineLegs") && typeof value === "string" && value.trim().toLowerCase() === "off") {
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
};
|
|
156
|
+
return equalValues(normalize(left), normalize(right));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isCancelled(ctx: ExtensionCommandContext): boolean {
|
|
160
|
+
return ctx.signal?.aborted === true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isCurrent(ctx: ExtensionCommandContext, deps: PersonaSettingsDeps): boolean {
|
|
164
|
+
return !deps.isCurrent || deps.isCurrent(ctx);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function reportUnavailable(ctx: ExtensionCommandContext): void {
|
|
168
|
+
if (isCancelled(ctx)) report(ctx, "The settings command was cancelled. No setting was saved.", "warning");
|
|
169
|
+
else report(ctx, "The session changed while the settings panel was open. No setting was saved.", "warning");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function statusForValue(row: SettingsPanelRow): string {
|
|
173
|
+
if (row.locked) {
|
|
174
|
+
const locked = `Environment ${settingById(row.id)?.env ?? "override"} is active and locked.`;
|
|
175
|
+
return row.pendingReload ? `${locked} The running value differs; use /reload to load it (${row.applyWhen}).` : locked;
|
|
176
|
+
}
|
|
177
|
+
if (row.pendingReload) {
|
|
178
|
+
return `Saved value differs from the running value; use /reload to load it (${row.applyWhen}).`;
|
|
179
|
+
}
|
|
180
|
+
return `Changes apply ${row.applyWhen}.`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function writePreference(
|
|
184
|
+
ctx: ExtensionCommandContext,
|
|
185
|
+
deps: PersonaSettingsDeps,
|
|
186
|
+
id: SettingId,
|
|
187
|
+
value: unknown | null,
|
|
188
|
+
): Promise<boolean> {
|
|
189
|
+
if (isCancelled(ctx) || !isCurrent(ctx, deps)) {
|
|
190
|
+
reportUnavailable(ctx);
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
await deps.write({ preferences: preferencesPatch(id, value) });
|
|
195
|
+
return true;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
report(ctx, `Could not confirm save for ${id}: ${errorText(error)}. Reopen /persona-settings to verify the stored value.`, "error");
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function activeEnvironmentValue(setting: PersonaSetting, deps: PersonaSettingsDeps): boolean {
|
|
203
|
+
return hasEnvOverride(setting, deps.env ?? process.env);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function promptValue(
|
|
207
|
+
ctx: ExtensionCommandContext,
|
|
208
|
+
setting: PersonaSetting,
|
|
209
|
+
currentValue: unknown,
|
|
210
|
+
): Promise<unknown | undefined> {
|
|
211
|
+
if (setting.kind === "boolean") {
|
|
212
|
+
const selected = await ctx.ui.select(`Edit ${setting.label}`, ["On", "Off"]);
|
|
213
|
+
if (selected === undefined) return undefined;
|
|
214
|
+
if (selected === "On") return true;
|
|
215
|
+
if (selected === "Off") return false;
|
|
216
|
+
throw new Error(`unexpected choice for ${setting.label}`);
|
|
217
|
+
}
|
|
218
|
+
if (setting.kind === "choice") {
|
|
219
|
+
const choices = setting.choices ?? [];
|
|
220
|
+
const selected = await ctx.ui.select(`Edit ${setting.label} · current ${formatSettingValue(currentValue)}`, [...choices]);
|
|
221
|
+
if (selected === undefined) return undefined;
|
|
222
|
+
return parseSettingInput(setting, selected);
|
|
223
|
+
}
|
|
224
|
+
const placeholder = setting.kind === "string-list"
|
|
225
|
+
? "JSON array or one path per line; commas and semicolons stay literal"
|
|
226
|
+
: setting.kind === "number"
|
|
227
|
+
? "Enter a whole number"
|
|
228
|
+
: "Enter a value";
|
|
229
|
+
const current = setting.kind === "string-list" && Array.isArray(currentValue)
|
|
230
|
+
? JSON.stringify(currentValue)
|
|
231
|
+
: formatSettingValue(currentValue);
|
|
232
|
+
const entered = await ctx.ui.input(`Edit ${setting.label} · current ${current}`, placeholder);
|
|
233
|
+
if (entered === undefined) return undefined;
|
|
234
|
+
return parseSettingInput(setting, entered);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function resetPreference(
|
|
238
|
+
ctx: ExtensionCommandContext,
|
|
239
|
+
deps: PersonaSettingsDeps,
|
|
240
|
+
row: SettingsPanelRow,
|
|
241
|
+
): Promise<string> {
|
|
242
|
+
if (!row.hasSavedValue) {
|
|
243
|
+
return row.locked
|
|
244
|
+
? `No saved override exists. ${settingById(row.id)?.env ?? "Environment"} still controls this value.`
|
|
245
|
+
: "No saved override exists; the setting already uses its default source.";
|
|
246
|
+
}
|
|
247
|
+
if (!(await writePreference(ctx, deps, row.id, null))) return "The saved override could not be confirmed; reopen settings to verify.";
|
|
248
|
+
const updated = makeSnapshot(deps).rows.find((candidate) => candidate.id === row.id) ?? row;
|
|
249
|
+
if (updated.locked) {
|
|
250
|
+
return updated.pendingReload
|
|
251
|
+
? `Saved override removed. ${settingById(row.id)?.env ?? "Environment"} still controls the next value; the running value differs, so use /reload to load it.`
|
|
252
|
+
: `Saved override removed. ${settingById(row.id)?.env ?? "Environment"} still controls the active value.`;
|
|
253
|
+
}
|
|
254
|
+
return updated.pendingReload
|
|
255
|
+
? `Saved override removed. The next-session default differs from the running value; use /reload to load it.`
|
|
256
|
+
: `Saved override removed. The active value already matches its default ${updated.applyWhen}.`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function applySettingAction(
|
|
260
|
+
ctx: ExtensionCommandContext,
|
|
261
|
+
deps: PersonaSettingsDeps,
|
|
262
|
+
row: SettingsPanelRow,
|
|
263
|
+
action: "edit" | "reset",
|
|
264
|
+
): Promise<string | undefined> {
|
|
265
|
+
const setting = settingById(row.id);
|
|
266
|
+
if (!setting) return "The selected setting is no longer in the registry.";
|
|
267
|
+
if (action === "reset") return resetPreference(ctx, deps, row);
|
|
268
|
+
if (activeEnvironmentValue(setting, deps)) return `Cannot edit this setting while ${setting.env ?? "an environment override"} is set.`;
|
|
269
|
+
let value: unknown | undefined;
|
|
270
|
+
try {
|
|
271
|
+
value = await promptValue(ctx, setting, rowValue(row));
|
|
272
|
+
} catch (error) {
|
|
273
|
+
return `Invalid ${setting.label}: ${errorText(error)}. The saved value was not changed.`;
|
|
274
|
+
}
|
|
275
|
+
if (value === undefined) return undefined;
|
|
276
|
+
if (!(await writePreference(ctx, deps, row.id, value))) return "The save could not be confirmed. Reopen settings to verify the stored value.";
|
|
277
|
+
const updated = makeSnapshot(deps).rows.find((candidate) => candidate.id === row.id);
|
|
278
|
+
const pending = updated?.pendingReload ?? !equalSettingValues(row.id, value, row.activeValue);
|
|
279
|
+
return `Saved ${setting.label}. The running value remains ${formatSettingValue(row.activeValue)}; it applies ${setting.applyWhen}${pending ? "; use Pi's /reload or restart to load the saved preference" : ""}.`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function runTui(ctx: ExtensionCommandContext, deps: PersonaSettingsDeps): Promise<void> {
|
|
283
|
+
let initialQuery = "";
|
|
284
|
+
let selectedId: SettingId | undefined;
|
|
285
|
+
let notice: string | undefined;
|
|
286
|
+
for (;;) {
|
|
287
|
+
if (isCancelled(ctx) || !isCurrent(ctx, deps)) {
|
|
288
|
+
reportUnavailable(ctx);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
let snapshot: SettingsSnapshot;
|
|
292
|
+
try {
|
|
293
|
+
snapshot = makeSnapshot(deps);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
report(ctx, `Could not read persona settings: ${errorText(error)}.`, "error");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
let panel: PersonaSettingsPanel | undefined;
|
|
299
|
+
const action = await ctx.ui.custom<SettingsPanelAction>((tui, theme, keybindings, done) => {
|
|
300
|
+
panel = new PersonaSettingsPanel(tui, theme, keybindings, {
|
|
301
|
+
file: deps.file,
|
|
302
|
+
lastPersona: snapshot.lastPersona,
|
|
303
|
+
rows: snapshot.rows,
|
|
304
|
+
...(notice ? { notice } : {}),
|
|
305
|
+
initialQuery,
|
|
306
|
+
...(selectedId ? { initialSelectedId: selectedId } : {}),
|
|
307
|
+
}, done);
|
|
308
|
+
return panel;
|
|
309
|
+
});
|
|
310
|
+
const view = panel?.viewState;
|
|
311
|
+
initialQuery = view?.query ?? initialQuery;
|
|
312
|
+
selectedId = view?.selectedId ?? selectedId;
|
|
313
|
+
notice = undefined;
|
|
314
|
+
if (isCancelled(ctx) || !isCurrent(ctx, deps)) {
|
|
315
|
+
reportUnavailable(ctx);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (!action || action.type === "close") return;
|
|
319
|
+
if (action.type === "models") {
|
|
320
|
+
await deps.models(ctx);
|
|
321
|
+
notice = "Model assignments were handled by the existing Models selector.";
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (action.type === "diagnostics") {
|
|
325
|
+
try {
|
|
326
|
+
report(ctx, deps.diagnostics?.() || "No diagnostics are available.");
|
|
327
|
+
} catch (error) {
|
|
328
|
+
report(ctx, `Diagnostics unavailable: ${errorText(error)}`, "error");
|
|
329
|
+
}
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const row = snapshot.rows.find((candidate) => candidate.id === action.id);
|
|
333
|
+
if (!row) {
|
|
334
|
+
notice = "That setting is no longer available.";
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
notice = await applySettingAction(ctx, deps, row, action.type);
|
|
338
|
+
if (isCancelled(ctx) || !isCurrent(ctx, deps)) {
|
|
339
|
+
if (!notice?.startsWith("The save could not be confirmed") && !notice?.startsWith("The saved override could not be confirmed")) reportUnavailable(ctx);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (notice && !notice.startsWith("Saved ") && !notice.startsWith("The save could not be confirmed")) {
|
|
343
|
+
// Errors and locks are also visible in the panel; command-level notifications make them
|
|
344
|
+
// available to hosts that do not preserve a dismissed custom component.
|
|
345
|
+
if (notice.startsWith("Invalid ") || notice.startsWith("Cannot ") || notice.startsWith("The saved override could not")) {
|
|
346
|
+
report(ctx, notice, "warning");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function rpcOption(row: SettingsPanelRow): string {
|
|
353
|
+
return `${row.label} [${row.id}] · ${formatSettingValue(row.activeValue)}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function runRpc(ctx: ExtensionCommandContext, deps: PersonaSettingsDeps): Promise<void> {
|
|
357
|
+
let query = "";
|
|
358
|
+
for (;;) {
|
|
359
|
+
if (isCancelled(ctx) || !isCurrent(ctx, deps)) {
|
|
360
|
+
reportUnavailable(ctx);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
let snapshot: SettingsSnapshot;
|
|
364
|
+
try {
|
|
365
|
+
snapshot = makeSnapshot(deps);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
report(ctx, `Could not read persona settings: ${errorText(error)}.`, "error");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const visibleRows = query
|
|
371
|
+
? snapshot.rows.filter((row) => `${row.id} ${row.group} ${row.label} ${row.description}`.toLocaleLowerCase().includes(query.toLocaleLowerCase()))
|
|
372
|
+
: snapshot.rows;
|
|
373
|
+
const settings = visibleRows.map(rpcOption);
|
|
374
|
+
const mainOptions = [...settings, "Search settings", "Models", "Diagnostics", "Close"];
|
|
375
|
+
const chosen = await ctx.ui.select(`Persona settings${query ? ` · filter ${query}` : ""}`, mainOptions);
|
|
376
|
+
if (isCancelled(ctx) || !isCurrent(ctx, deps)) {
|
|
377
|
+
reportUnavailable(ctx);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (chosen === undefined || chosen === "Close") return;
|
|
381
|
+
if (chosen === "Search settings") {
|
|
382
|
+
const next = await ctx.ui.input("Filter settings by name or description", query || "clear to show all settings");
|
|
383
|
+
if (isCancelled(ctx) || !isCurrent(ctx, deps)) {
|
|
384
|
+
reportUnavailable(ctx);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (next !== undefined) query = next.trim();
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (chosen === "Models") {
|
|
391
|
+
await deps.models(ctx);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (chosen === "Diagnostics") {
|
|
395
|
+
try {
|
|
396
|
+
report(ctx, deps.diagnostics?.() || "No diagnostics are available.");
|
|
397
|
+
} catch (error) {
|
|
398
|
+
report(ctx, `Diagnostics unavailable: ${errorText(error)}`, "error");
|
|
399
|
+
}
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const row = visibleRows.find((candidate) => rpcOption(candidate) === chosen);
|
|
403
|
+
if (!row) continue;
|
|
404
|
+
const action = await ctx.ui.select(`${row.label} · ${statusForValue(row)}`, ["Edit", "Reset saved override", "Details", "Back"]);
|
|
405
|
+
if (isCancelled(ctx) || !isCurrent(ctx, deps)) {
|
|
406
|
+
reportUnavailable(ctx);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (action === undefined || action === "Back") continue;
|
|
410
|
+
if (action === "Details") {
|
|
411
|
+
report(ctx, `${row.label}\n${row.description}\nActive now: ${formatSettingValue(row.activeValue)}\nSaved override: ${row.hasSavedValue ? formatSettingValue(row.savedValue) : "none"}\nSource: ${row.locked ? "environment · locked" : row.source}\nStatus: ${statusForValue(row)}\nApplies: ${row.applyWhen}`);
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const result = await applySettingAction(ctx, deps, row, action === "Edit" ? "edit" : "reset");
|
|
415
|
+
if (isCancelled(ctx) || !isCurrent(ctx, deps)) {
|
|
416
|
+
if (!result?.startsWith("The save could not be confirmed") && !result?.startsWith("The saved override could not be confirmed")) reportUnavailable(ctx);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (result && !result.startsWith("The save could not be confirmed") && !result.startsWith("The saved override could not be confirmed")) {
|
|
420
|
+
report(ctx, result, result.startsWith("Invalid ") || result.startsWith("Cannot ") ? "warning" : "info");
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function formatShowMode(ctx: ExtensionCommandContext, deps: PersonaSettingsDeps): void {
|
|
426
|
+
const status = safeSettingsReport(deps);
|
|
427
|
+
report(ctx, status, status.startsWith("Could not read ") ? "error" : "info");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Register the separate, registry-driven Persona settings command. */
|
|
431
|
+
export function registerPersonaSettings(pi: ExtensionAPI, deps: PersonaSettingsDeps): void {
|
|
432
|
+
pi.registerCommand(COMMAND, {
|
|
433
|
+
description: "Open pi-persona's settings panel; use /persona-settings show for a read-only report",
|
|
434
|
+
handler: async (args, ctx) => {
|
|
435
|
+
const initialSessionId = ctx.sessionManager.getSessionId();
|
|
436
|
+
const commandDeps: PersonaSettingsDeps = {
|
|
437
|
+
...deps,
|
|
438
|
+
isCurrent: (currentCtx) =>
|
|
439
|
+
currentCtx.sessionManager.getSessionId() === initialSessionId &&
|
|
440
|
+
(!deps.isCurrent || deps.isCurrent(currentCtx)),
|
|
441
|
+
};
|
|
442
|
+
const command = args.trim();
|
|
443
|
+
if (command === "show") {
|
|
444
|
+
formatShowMode(ctx, commandDeps);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (command !== "") {
|
|
448
|
+
report(ctx, "Usage: /persona-settings [show]", "warning");
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (!ctx.hasUI || (ctx.mode !== "tui" && ctx.mode !== "rpc")) {
|
|
452
|
+
report(ctx, `Persona settings are read-only in ${ctx.mode} mode. Run /persona-settings in the TUI or RPC UI to edit them.\n\n${safeSettingsReport(commandDeps)}`, "warning");
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (ctx.mode === "tui") await runTui(ctx, commandDeps);
|
|
456
|
+
else await runRpc(ctx, commandDeps);
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
}
|