pi-advisor-flow 0.5.6 → 0.7.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/CHANGELOG.md +29 -0
- package/README.md +1 -0
- package/dist/index.js +3160 -1255
- package/extensions/index.ts +9 -9
- package/package.json +12 -8
- package/src/{untracked.ts → attachments.ts} +2 -5
- package/src/commands/activation-preparation.ts +6 -6
- package/src/commands/activation.ts +7 -7
- package/src/commands/lifecycle.ts +5 -5
- package/src/commands/manual-command.ts +13 -9
- package/src/commands/manual-consultation.ts +24 -10
- package/src/commands/manual-progress.ts +3 -3
- package/src/commands/model-commands.ts +6 -6
- package/src/commands/model-options.ts +4 -4
- package/src/commands/model-picker.ts +2 -2
- package/src/commands/registration.ts +8 -8
- package/src/commands/renderers.ts +5 -5
- package/src/commands/runtime.ts +8 -8
- package/src/commands/settings-commands.ts +8 -8
- package/src/commands/settings-persistence.ts +52 -4
- package/src/commands/types.ts +6 -13
- package/src/commands.ts +2 -2
- package/src/config/args.ts +3 -3
- package/src/config/defaults.ts +78 -3
- package/src/config/schema.ts +434 -0
- package/src/config/state.ts +71 -2
- package/src/config/storage.ts +27 -119
- package/src/config/types.ts +30 -1
- package/src/config/validation.ts +61 -181
- package/src/config.ts +7 -7
- package/src/content-utils.ts +30 -0
- package/src/conversation.ts +13 -168
- package/src/git.ts +4 -18
- package/src/herdr.ts +6 -8
- package/src/jev/client.ts +348 -0
- package/src/jev/key-store.ts +252 -0
- package/src/jev/ledger.ts +234 -0
- package/src/jev/questions.ts +119 -0
- package/src/jev/state.ts +47 -0
- package/src/jev/transport.ts +58 -0
- package/src/model-stream.ts +5 -8
- package/src/outcomes.ts +3 -2
- package/src/pi-settings.ts +59 -0
- package/src/preferences.ts +2 -2
- package/src/redaction.ts +50 -0
- package/src/scout-context.ts +121 -485
- package/src/scout-curation.ts +84 -0
- package/src/scout-groups.ts +485 -0
- package/src/scout-protocol.ts +122 -0
- package/src/scout-reconstruct.ts +44 -0
- package/src/scout-types.ts +66 -0
- package/src/scout.ts +53 -63
- package/src/session-state.ts +215 -78
- package/src/tool-result-cap.ts +88 -0
- package/src/tools/consult-context.ts +155 -0
- package/src/tools/consultation.ts +71 -220
- package/src/tools/gate-policy.ts +5 -5
- package/src/tools/gate-protocol.ts +2 -2
- package/src/tools/jev-filter.ts +202 -0
- package/src/tools/jev-turn-gate.ts +183 -0
- package/src/tools/loop-gate.ts +71 -61
- package/src/tools/outage-notifier.ts +30 -0
- package/src/tools/prompts.ts +4 -7
- package/src/tools/register-ask-advisor.ts +46 -15
- package/src/tools/register-lifecycle.ts +7 -7
- package/src/tools/register-outcome.ts +5 -5
- package/src/tools/register-renderers.ts +63 -5
- package/src/tools/registration.ts +27 -10
- package/src/tools/render-advisor-result.ts +54 -22
- package/src/tools/render-common.ts +25 -13
- package/src/tools/scout-status.ts +5 -5
- package/src/tools/session.ts +1 -1
- package/src/tools/types.ts +17 -7
- package/src/tools.ts +9 -9
- package/src/ui/jev-setup-submenu.ts +344 -0
- package/src/ui/manual-dialog-render.ts +2 -2
- package/src/ui/manual-dialog.ts +4 -9
- package/src/ui/masked-input.ts +68 -0
- package/src/ui/model-selector.ts +1 -1
- package/src/ui/settings-formatting.ts +2 -2
- package/src/ui/settings-items.ts +166 -5
- package/src/ui/settings-mutations.ts +37 -2
- package/src/ui/settings-selector.ts +5 -5
- package/src/ui/text-setting-submenu.ts +1 -1
- package/src/ui/types.ts +13 -2
- package/src/ui.ts +4 -4
- package/src/usage.ts +4 -7
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import {
|
|
3
|
+
type Component,
|
|
4
|
+
type Focusable,
|
|
5
|
+
Key,
|
|
6
|
+
matchesKey,
|
|
7
|
+
truncateToWidth,
|
|
8
|
+
} from "@earendil-works/pi-tui";
|
|
9
|
+
import { noul } from "@typesafe-ai/sdk";
|
|
10
|
+
import { jevClientFromCredentials } from "../jev/client.ts";
|
|
11
|
+
import {
|
|
12
|
+
clearKeyTypeSafeKey,
|
|
13
|
+
consumePlaintextKeyWarning,
|
|
14
|
+
type JevKeyStoreResult,
|
|
15
|
+
removeTypeSafeKeyFromAdvisorJson,
|
|
16
|
+
writeKeyTypeSafeKey,
|
|
17
|
+
} from "../jev/key-store.ts";
|
|
18
|
+
import { type JevCredentials, resolveJevTransport } from "../jev/transport.ts";
|
|
19
|
+
import { MaskedInput } from "./masked-input.ts";
|
|
20
|
+
import type { RenderRequester } from "./types.ts";
|
|
21
|
+
|
|
22
|
+
export interface JevSetupSubmenuOptions {
|
|
23
|
+
currentValue: string;
|
|
24
|
+
done: (selectedValue?: string) => void;
|
|
25
|
+
theme: Theme;
|
|
26
|
+
tui: RenderRequester;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface JevSetupDeps {
|
|
30
|
+
clearStoredKey?: () => Promise<JevKeyStoreResult>;
|
|
31
|
+
removePlaintextKey?: () => JevKeyStoreResult;
|
|
32
|
+
resolveTransport?: () => Promise<JevCredentials | undefined>;
|
|
33
|
+
verify?: (credentials: JevCredentials) => Promise<JevFailureLike>;
|
|
34
|
+
writeKey?: (key: string) => Promise<JevKeyStoreResult>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface JevFailureLike {
|
|
38
|
+
message?: string;
|
|
39
|
+
ok: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type SetupMode = "menu" | "key-entry" | "verifying";
|
|
43
|
+
type SetupAction =
|
|
44
|
+
| "verify-enable"
|
|
45
|
+
| "verify-again"
|
|
46
|
+
| "disable"
|
|
47
|
+
| "disable-clear"
|
|
48
|
+
| "enter-key"
|
|
49
|
+
| "done";
|
|
50
|
+
|
|
51
|
+
const transportLabel = (credentials: JevCredentials): string => {
|
|
52
|
+
if (credentials.transport === "openrouter") {
|
|
53
|
+
return "OpenRouter (reusing pi login)";
|
|
54
|
+
}
|
|
55
|
+
switch (credentials.source) {
|
|
56
|
+
case "advisor-json":
|
|
57
|
+
return "TypeSafe (key: advisor.json — plaintext, not recommended)";
|
|
58
|
+
case "bun-secrets":
|
|
59
|
+
return "TypeSafe (key: Bun.secrets)";
|
|
60
|
+
case "file":
|
|
61
|
+
return "TypeSafe (key: stored file, mode 0600)";
|
|
62
|
+
default:
|
|
63
|
+
return "TypeSafe (key: TYPESAFE_API_KEY)";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const defaultVerify = async (credentials: JevCredentials) => {
|
|
68
|
+
const client = jevClientFromCredentials(credentials);
|
|
69
|
+
try {
|
|
70
|
+
await client.ask(
|
|
71
|
+
{ purpose: "pi-advisor setup verification" },
|
|
72
|
+
{ verified: noul("Answer yes.") }
|
|
73
|
+
);
|
|
74
|
+
return { ok: true };
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return {
|
|
77
|
+
message: error instanceof Error ? error.message : String(error),
|
|
78
|
+
ok: false,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Guided Jev credentials setup: detects a reusable login, verifies live
|
|
84
|
+
* before enabling, and never echoes an entered key back. */
|
|
85
|
+
export class JevSetupSubmenu implements Component, Focusable {
|
|
86
|
+
private readonly options: JevSetupSubmenuOptions;
|
|
87
|
+
private readonly deps: JevSetupDeps;
|
|
88
|
+
private readonly maskedInput: MaskedInput;
|
|
89
|
+
private credentials: JevCredentials | undefined;
|
|
90
|
+
private mode: SetupMode = "menu";
|
|
91
|
+
private notice: string | undefined;
|
|
92
|
+
private selectedIndex = 0;
|
|
93
|
+
private _focused = true;
|
|
94
|
+
|
|
95
|
+
constructor(options: JevSetupSubmenuOptions, deps: JevSetupDeps = {}) {
|
|
96
|
+
this.options = options;
|
|
97
|
+
this.deps = {
|
|
98
|
+
clearStoredKey: deps.clearStoredKey ?? clearKeyTypeSafeKey,
|
|
99
|
+
removePlaintextKey:
|
|
100
|
+
deps.removePlaintextKey ?? removeTypeSafeKeyFromAdvisorJson,
|
|
101
|
+
resolveTransport: deps.resolveTransport ?? (() => resolveJevTransport()),
|
|
102
|
+
verify: deps.verify ?? defaultVerify,
|
|
103
|
+
writeKey: deps.writeKey ?? writeKeyTypeSafeKey,
|
|
104
|
+
};
|
|
105
|
+
this.maskedInput = new MaskedInput({
|
|
106
|
+
onEscape: () => this.options.done(),
|
|
107
|
+
onSubmit: (value) => this.submitEnteredKey(value),
|
|
108
|
+
placeholder: "Paste a TypeSafe API key",
|
|
109
|
+
});
|
|
110
|
+
this.refresh().catch(() => undefined);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get focused(): boolean {
|
|
114
|
+
return this._focused;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
set focused(value: boolean) {
|
|
118
|
+
this._focused = value;
|
|
119
|
+
this.maskedInput.focused = value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
invalidate(): void {
|
|
123
|
+
this.maskedInput.invalidate();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
handleInput(keyData: string): void {
|
|
127
|
+
if (this.mode === "key-entry") {
|
|
128
|
+
this.maskedInput.handleInput(keyData);
|
|
129
|
+
this.options.tui.requestRender();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (this.mode !== "menu") {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const actions = this.actions();
|
|
136
|
+
if (
|
|
137
|
+
matchesKey(keyData, Key.down) ||
|
|
138
|
+
keyData === "\u001b[B" ||
|
|
139
|
+
keyData === "\u001bOB"
|
|
140
|
+
) {
|
|
141
|
+
this.selectedIndex = (this.selectedIndex + 1) % actions.length;
|
|
142
|
+
} else if (
|
|
143
|
+
matchesKey(keyData, Key.up) ||
|
|
144
|
+
keyData === "\u001b[A" ||
|
|
145
|
+
keyData === "\u001bOA"
|
|
146
|
+
) {
|
|
147
|
+
this.selectedIndex =
|
|
148
|
+
(this.selectedIndex - 1 + actions.length) % actions.length;
|
|
149
|
+
} else if (matchesKey(keyData, Key.enter) || keyData === "\r") {
|
|
150
|
+
this.activate(actions[this.selectedIndex]);
|
|
151
|
+
} else {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
this.options.tui.requestRender();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
render(width: number): string[] {
|
|
158
|
+
const { theme } = this.options;
|
|
159
|
+
const enabled = this.options.currentValue === "On";
|
|
160
|
+
const lines = [
|
|
161
|
+
theme.fg("accent", theme.bold(" Jev consultation filter")),
|
|
162
|
+
"",
|
|
163
|
+
];
|
|
164
|
+
if (this.credentials) {
|
|
165
|
+
lines.push(` Transport: ${transportLabel(this.credentials)}`);
|
|
166
|
+
lines.push(` Filter: ${enabled ? "On" : "Off"}`);
|
|
167
|
+
} else if (this.mode === "verifying") {
|
|
168
|
+
lines.push(" Checking available Jev credentials…");
|
|
169
|
+
} else {
|
|
170
|
+
lines.push(
|
|
171
|
+
" No Jev credentials found. Enter a TypeSafe API key below, or add"
|
|
172
|
+
);
|
|
173
|
+
lines.push(" an OpenRouter login in pi; it is reused automatically.");
|
|
174
|
+
}
|
|
175
|
+
if (this.notice) {
|
|
176
|
+
lines.push("");
|
|
177
|
+
lines.push(theme.fg("warning", ` ${this.notice}`));
|
|
178
|
+
}
|
|
179
|
+
lines.push("");
|
|
180
|
+
if (this.mode === "verifying") {
|
|
181
|
+
lines.push(" Verifying with a live Jev call…");
|
|
182
|
+
} else if (this.mode === "key-entry") {
|
|
183
|
+
lines.push(
|
|
184
|
+
` ${this.maskedInput.render(Math.max(10, width - 4))[0] ?? ""}`
|
|
185
|
+
);
|
|
186
|
+
lines.push(theme.fg("dim", " Enter: verify · Esc: cancel"));
|
|
187
|
+
} else {
|
|
188
|
+
for (const [index, label] of this.labels().entries()) {
|
|
189
|
+
const prefix = index === this.selectedIndex ? "→ " : " ";
|
|
190
|
+
lines.push(`${prefix}${label}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return lines.map((line) => truncateToWidth(line, width));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private actions(): SetupAction[] {
|
|
197
|
+
if (!this.credentials) {
|
|
198
|
+
return ["enter-key", "done"];
|
|
199
|
+
}
|
|
200
|
+
const enabled = this.options.currentValue === "On";
|
|
201
|
+
const actions: SetupAction[] = [enabled ? "verify-again" : "verify-enable"];
|
|
202
|
+
actions.push("disable");
|
|
203
|
+
if (
|
|
204
|
+
this.credentials.source === "bun-secrets" ||
|
|
205
|
+
this.credentials.source === "file"
|
|
206
|
+
) {
|
|
207
|
+
actions.push("disable-clear");
|
|
208
|
+
}
|
|
209
|
+
actions.push("done");
|
|
210
|
+
return actions;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private actionLabels(): Record<SetupAction, string> {
|
|
214
|
+
return {
|
|
215
|
+
disable: "Disable",
|
|
216
|
+
"disable-clear": "Disable and clear stored key",
|
|
217
|
+
done: "Done",
|
|
218
|
+
"enter-key": "Enter a TypeSafe API key",
|
|
219
|
+
"verify-again": "Verify again",
|
|
220
|
+
"verify-enable": "Verify and enable",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private labels(): string[] {
|
|
225
|
+
const labels = this.actionLabels();
|
|
226
|
+
return this.actions().map((action) => labels[action]);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private async refresh(): Promise<void> {
|
|
230
|
+
const wasVerifying = this.mode === "verifying";
|
|
231
|
+
this.mode = "verifying";
|
|
232
|
+
if (!wasVerifying) {
|
|
233
|
+
this.options.tui.requestRender();
|
|
234
|
+
}
|
|
235
|
+
this.credentials = await this.deps.resolveTransport?.();
|
|
236
|
+
this.mode = "menu";
|
|
237
|
+
this.selectedIndex = 0;
|
|
238
|
+
if (this.credentials?.source === "advisor-json") {
|
|
239
|
+
this.notice ??= consumePlaintextKeyWarning();
|
|
240
|
+
}
|
|
241
|
+
this.options.tui.requestRender();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private activate(action: SetupAction): void {
|
|
245
|
+
switch (action) {
|
|
246
|
+
case "done":
|
|
247
|
+
this.options.done();
|
|
248
|
+
return;
|
|
249
|
+
case "enter-key":
|
|
250
|
+
this.mode = "key-entry";
|
|
251
|
+
return;
|
|
252
|
+
case "disable":
|
|
253
|
+
this.notice = undefined;
|
|
254
|
+
this.options.done("Off");
|
|
255
|
+
return;
|
|
256
|
+
case "disable-clear":
|
|
257
|
+
this.disableAndClear().catch(() => undefined);
|
|
258
|
+
return;
|
|
259
|
+
case "verify-again":
|
|
260
|
+
case "verify-enable":
|
|
261
|
+
this.verifyAndEnable().catch(() => undefined);
|
|
262
|
+
return;
|
|
263
|
+
default:
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private async disableAndClear(): Promise<void> {
|
|
269
|
+
const clear = this.deps.clearStoredKey;
|
|
270
|
+
if (!clear) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const result = await clear();
|
|
274
|
+
this.credentials = undefined;
|
|
275
|
+
this.notice = result.message;
|
|
276
|
+
this.options.done("Off");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async verifyAndEnable(): Promise<void> {
|
|
280
|
+
const { credentials } = this;
|
|
281
|
+
if (!(credentials && this.deps.verify)) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
this.mode = "verifying";
|
|
285
|
+
this.notice = undefined;
|
|
286
|
+
this.options.tui.requestRender();
|
|
287
|
+
const outcome = await this.deps.verify(credentials);
|
|
288
|
+
this.mode = "menu";
|
|
289
|
+
if (!outcome.ok) {
|
|
290
|
+
this.notice = `Verification failed: ${outcome.message ?? "unknown error"}`;
|
|
291
|
+
this.options.tui.requestRender();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (
|
|
295
|
+
credentials.transport === "typesafe" &&
|
|
296
|
+
credentials.source === "advisor-json"
|
|
297
|
+
) {
|
|
298
|
+
const stored = await this.deps.writeKey?.(credentials.apiKey);
|
|
299
|
+
if (!stored?.ok) {
|
|
300
|
+
this.notice =
|
|
301
|
+
stored?.message ??
|
|
302
|
+
"Storing the key failed; the plaintext advisor.json key keeps working.";
|
|
303
|
+
this.options.tui.requestRender();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const removed = this.deps.removePlaintextKey?.();
|
|
307
|
+
this.notice = removed?.ok
|
|
308
|
+
? "Key moved from advisor.json into the secure store."
|
|
309
|
+
: `Stored securely, but ${removed?.message ?? "removing the plaintext copy failed; remove it yourself."}`;
|
|
310
|
+
}
|
|
311
|
+
this.options.done("On");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private async submitEnteredKey(value: string): Promise<void> {
|
|
315
|
+
const key = value.trim();
|
|
316
|
+
if (!key) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (!this.deps.verify) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
this.mode = "verifying";
|
|
323
|
+
this.options.tui.requestRender();
|
|
324
|
+
const outcome = await this.deps.verify({
|
|
325
|
+
apiKey: key,
|
|
326
|
+
transport: "typesafe",
|
|
327
|
+
});
|
|
328
|
+
this.mode = "menu";
|
|
329
|
+
if (!outcome.ok) {
|
|
330
|
+
this.notice = `Verification failed: ${outcome.message ?? "unknown error"}`;
|
|
331
|
+
this.options.tui.requestRender();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const stored = await this.deps.writeKey?.(key);
|
|
335
|
+
if (!stored?.ok) {
|
|
336
|
+
this.notice = stored?.message ?? "Storing the key failed.";
|
|
337
|
+
this.options.tui.requestRender();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
this.notice = `${stored.message} Verification succeeded.`;
|
|
341
|
+
await this.refresh();
|
|
342
|
+
this.options.done("On");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
visibleWidth,
|
|
4
4
|
wrapTextWithAnsi,
|
|
5
5
|
} from "@earendil-works/pi-tui";
|
|
6
|
-
import type { GitContextLevel } from "../git.
|
|
7
|
-
import type { ManualAdvisorDialogView } from "./types.
|
|
6
|
+
import type { GitContextLevel } from "../git.ts";
|
|
7
|
+
import type { ManualAdvisorDialogView } from "./types.ts";
|
|
8
8
|
|
|
9
9
|
const MANUAL_GIT_CONTEXT_COPY: Record<
|
|
10
10
|
GitContextLevel,
|
package/src/ui/manual-dialog.ts
CHANGED
|
@@ -12,24 +12,19 @@ import {
|
|
|
12
12
|
clampGitContextLevel,
|
|
13
13
|
GIT_CONTEXT_LEVELS,
|
|
14
14
|
type GitContextLevel,
|
|
15
|
-
} from "../git.
|
|
16
|
-
import { renderManualAdvisorDialog } from "./manual-dialog-render.
|
|
15
|
+
} from "../git.ts";
|
|
16
|
+
import { renderManualAdvisorDialog } from "./manual-dialog-render.ts";
|
|
17
17
|
import type {
|
|
18
18
|
ManualAdvisorDialogOptions,
|
|
19
19
|
ManualAdvisorFocus,
|
|
20
|
-
} from "./types.
|
|
20
|
+
} from "./types.ts";
|
|
21
21
|
|
|
22
22
|
// Keep the keybinding components separate from Socket's URL-string heuristic.
|
|
23
23
|
const TUI_INPUT_TAB: keyof Keybindings = ["tui", "input", "tab"].join(
|
|
24
24
|
"."
|
|
25
25
|
) as keyof Keybindings;
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
* The small, deliberately non-persistent form used by `/advisor-manual`.
|
|
29
|
-
*
|
|
30
|
-
* The dialog owns only local input state. Callers decide when to consume a
|
|
31
|
-
* budget or start a consultation after the submitted request is returned.
|
|
32
|
-
*/
|
|
27
|
+
/** The small, non-persistent /advisor-manual form; callers own budget and consultation decisions. */
|
|
33
28
|
export class ManualAdvisorDialog implements Component, Focusable {
|
|
34
29
|
private readonly options: ManualAdvisorDialogOptions;
|
|
35
30
|
private readonly editor: Editor;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
type Focusable,
|
|
4
|
+
Input,
|
|
5
|
+
truncateToWidth,
|
|
6
|
+
} from "@earendil-works/pi-tui";
|
|
7
|
+
|
|
8
|
+
export interface MaskedInputOptions {
|
|
9
|
+
onEscape?: () => void;
|
|
10
|
+
onSubmit?: (value: string) => void;
|
|
11
|
+
placeholder?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface InputPrivateFields {
|
|
15
|
+
cursor: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Single-line secret input rendering bullets; the value never reaches the screen. */
|
|
19
|
+
export class MaskedInput implements Component, Focusable {
|
|
20
|
+
private readonly input: Input;
|
|
21
|
+
private readonly options: MaskedInputOptions;
|
|
22
|
+
private _focused = true;
|
|
23
|
+
|
|
24
|
+
constructor(options: MaskedInputOptions = {}) {
|
|
25
|
+
this.options = options;
|
|
26
|
+
this.input = new Input({ placeholder: options.placeholder });
|
|
27
|
+
this.input.focused = true;
|
|
28
|
+
this.input.onSubmit = (value) => options.onSubmit?.(value);
|
|
29
|
+
this.input.onEscape = () => options.onEscape?.();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get focused(): boolean {
|
|
33
|
+
return this._focused;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
set focused(value: boolean) {
|
|
37
|
+
this._focused = value;
|
|
38
|
+
this.input.focused = value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getValue(): string {
|
|
42
|
+
return this.input.getValue();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setValue(value: string): void {
|
|
46
|
+
this.input.setValue(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
handleInput(keyData: string): void {
|
|
50
|
+
this.input.handleInput(keyData);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
invalidate(): void {
|
|
54
|
+
this.input.invalidate();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
render(width: number): string[] {
|
|
58
|
+
const value = this.input.getValue();
|
|
59
|
+
// Compatibility-only read for the caret position; Input does not expose it.
|
|
60
|
+
const { cursor } = this.input as unknown as InputPrivateFields;
|
|
61
|
+
const masked = `${"•".repeat(Math.min(cursor, value.length))}█${"•".repeat(Math.max(0, value.length - cursor))}`;
|
|
62
|
+
const placeholder =
|
|
63
|
+
value.length === 0 && this.options.placeholder
|
|
64
|
+
? this.options.placeholder
|
|
65
|
+
: masked;
|
|
66
|
+
return [truncateToWidth(placeholder, Math.max(1, width))];
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/ui/model-selector.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type KeybindingsManager,
|
|
9
9
|
truncateToWidth,
|
|
10
10
|
} from "@earendil-works/pi-tui";
|
|
11
|
-
import type { SearchableModelSelectorOptions } from "./types.
|
|
11
|
+
import type { SearchableModelSelectorOptions } from "./types.ts";
|
|
12
12
|
|
|
13
13
|
export class SearchableModelSelector implements Component, Focusable {
|
|
14
14
|
private readonly tui: SearchableModelSelectorOptions["tui"];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
-
import
|
|
2
|
+
import { DEFAULT_EFFORT_LEVEL } from "../commands/model-options.ts";
|
|
3
|
+
import type { ContextPreset } from "./types.ts";
|
|
3
4
|
|
|
4
|
-
export const DEFAULT_EFFORT_LEVEL = "Default (Model Default)";
|
|
5
5
|
export const TOGGLE_VALUES = ["On", "Off"];
|
|
6
6
|
export const SIMPLE_MODE_GRADIENT_INTERVAL_MS = 100;
|
|
7
7
|
// Purple steps with a moving light highlight, retained from the original UI.
|
package/src/ui/settings-items.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_JEV_DIGEST_MAX_CHARS,
|
|
5
|
+
DEFAULT_JEV_FILTER_NOUL_MARGIN,
|
|
6
|
+
DEFAULT_JEV_FILTER_OVERRIDE_WINDOW,
|
|
7
|
+
DEFAULT_JEV_FILTER_SKIP_CONFIDENCE,
|
|
8
|
+
DEFAULT_JEV_MODEL,
|
|
9
|
+
DEFAULT_JEV_PRICE_PER_MTOK,
|
|
10
|
+
DEFAULT_JEV_TIMEOUT_MS,
|
|
11
|
+
DEFAULT_JEV_TRANSPORT,
|
|
12
|
+
DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD,
|
|
13
|
+
} from "../config/types.ts";
|
|
14
|
+
import { isValidAdvisorToolPolicies } from "../config/validation.ts";
|
|
15
|
+
import { JevSetupSubmenu } from "./jev-setup-submenu.ts";
|
|
4
16
|
import {
|
|
5
17
|
contextDescription,
|
|
6
18
|
currentContextLabel,
|
|
@@ -10,13 +22,13 @@ import {
|
|
|
10
22
|
settingValue,
|
|
11
23
|
TOGGLE_VALUES,
|
|
12
24
|
withCurrentValue,
|
|
13
|
-
} from "./settings-formatting.
|
|
14
|
-
import { TextSettingSubmenu } from "./text-setting-submenu.
|
|
25
|
+
} from "./settings-formatting.ts";
|
|
26
|
+
import { TextSettingSubmenu } from "./text-setting-submenu.ts";
|
|
15
27
|
import type {
|
|
16
28
|
AdvisorSettings,
|
|
17
29
|
ContextPreset,
|
|
18
30
|
RenderRequester,
|
|
19
|
-
} from "./types.
|
|
31
|
+
} from "./types.ts";
|
|
20
32
|
|
|
21
33
|
export interface SettingsItemsOptions {
|
|
22
34
|
effortLevels: string[];
|
|
@@ -40,6 +52,154 @@ const toggle = (
|
|
|
40
52
|
values: TOGGLE_VALUES,
|
|
41
53
|
});
|
|
42
54
|
|
|
55
|
+
const jevItems = (
|
|
56
|
+
settings: AdvisorSettings,
|
|
57
|
+
theme: Theme,
|
|
58
|
+
tui: RenderRequester
|
|
59
|
+
): SettingItem[] => [
|
|
60
|
+
{
|
|
61
|
+
currentValue: settingValue(settings.jevFilterEnabled, false),
|
|
62
|
+
description:
|
|
63
|
+
"Screen low-stakes ask_advisor consultations with Jev; guided setup verifies credentials.",
|
|
64
|
+
id: "jevFilter",
|
|
65
|
+
label: "Jev consultation filter",
|
|
66
|
+
submenu: (currentValue, done) =>
|
|
67
|
+
new JevSetupSubmenu({ currentValue, done, theme, tui }),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
currentValue: String(
|
|
71
|
+
settings.jevFilterSkipConfidence ?? DEFAULT_JEV_FILTER_SKIP_CONFIDENCE
|
|
72
|
+
),
|
|
73
|
+
description:
|
|
74
|
+
"Required probability on negligible stakes before a consultation is skipped.",
|
|
75
|
+
id: "jevFilterSkipConfidence",
|
|
76
|
+
label: "Jev skip confidence",
|
|
77
|
+
values: numericValues(
|
|
78
|
+
settings.jevFilterSkipConfidence ?? DEFAULT_JEV_FILTER_SKIP_CONFIDENCE,
|
|
79
|
+
[0.6, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95]
|
|
80
|
+
),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
currentValue: String(
|
|
84
|
+
settings.jevFilterNoulMargin ?? DEFAULT_JEV_FILTER_NOUL_MARGIN
|
|
85
|
+
),
|
|
86
|
+
description:
|
|
87
|
+
"Extra margin over a coin flip required on self-answerability before skipping.",
|
|
88
|
+
id: "jevFilterNoulMargin",
|
|
89
|
+
label: "Jev Noul margin",
|
|
90
|
+
values: numericValues(
|
|
91
|
+
settings.jevFilterNoulMargin ?? DEFAULT_JEV_FILTER_NOUL_MARGIN,
|
|
92
|
+
[0.1, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45]
|
|
93
|
+
),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
currentValue: `${settings.jevFilterOverrideWindow ?? DEFAULT_JEV_FILTER_OVERRIDE_WINDOW} turns`,
|
|
97
|
+
description:
|
|
98
|
+
"Turns after a skip during which the same question passes automatically.",
|
|
99
|
+
id: "jevFilterOverrideWindow",
|
|
100
|
+
label: "Jev override window",
|
|
101
|
+
values: numericValues(
|
|
102
|
+
settings.jevFilterOverrideWindow ?? DEFAULT_JEV_FILTER_OVERRIDE_WINDOW,
|
|
103
|
+
[0, 3, 5, 10, 20, 50]
|
|
104
|
+
).map((value) => `${value} turns`),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
currentValue: settings.jevModel ?? DEFAULT_JEV_MODEL,
|
|
108
|
+
description: "TypeSafe Jev model used for screening and turn-gate checks.",
|
|
109
|
+
id: "jevModel",
|
|
110
|
+
label: "Jev model",
|
|
111
|
+
submenu: (_currentValue, done) =>
|
|
112
|
+
new TextSettingSubmenu({
|
|
113
|
+
description:
|
|
114
|
+
"Enter a TypeSafe model name (for example jev-latest or jev-1.13.0).",
|
|
115
|
+
initial: settings.jevModel ?? DEFAULT_JEV_MODEL,
|
|
116
|
+
onCancel: done,
|
|
117
|
+
onSubmit: (value) => ({ value: value.trim() || DEFAULT_JEV_MODEL }),
|
|
118
|
+
theme,
|
|
119
|
+
title: "Jev model",
|
|
120
|
+
tui,
|
|
121
|
+
}),
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
currentValue: String(settings.jevTimeoutMs ?? DEFAULT_JEV_TIMEOUT_MS),
|
|
125
|
+
description: "Total wall-time budget for one Jev call, including retries.",
|
|
126
|
+
id: "jevTimeoutMs",
|
|
127
|
+
label: "Jev timeout ms",
|
|
128
|
+
values: numericValues(
|
|
129
|
+
settings.jevTimeoutMs ?? DEFAULT_JEV_TIMEOUT_MS,
|
|
130
|
+
[1000, 2000, 5000, 8000, 15_000, 30_000]
|
|
131
|
+
),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
currentValue: String(
|
|
135
|
+
settings.jevDigestMaxChars ?? DEFAULT_JEV_DIGEST_MAX_CHARS
|
|
136
|
+
),
|
|
137
|
+
description: "Conversation characters sent to Jev as screening evidence.",
|
|
138
|
+
id: "jevDigestMaxChars",
|
|
139
|
+
label: "Jev digest chars",
|
|
140
|
+
values: numericValues(
|
|
141
|
+
settings.jevDigestMaxChars ?? DEFAULT_JEV_DIGEST_MAX_CHARS,
|
|
142
|
+
[0, 1000, 2000, 4000, 8000, 15_000]
|
|
143
|
+
),
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
currentValue: String(
|
|
147
|
+
settings.jevPricePerMtok ?? DEFAULT_JEV_PRICE_PER_MTOK
|
|
148
|
+
),
|
|
149
|
+
description:
|
|
150
|
+
"Assumed TypeSafe price per million input tokens for cost lines.",
|
|
151
|
+
id: "jevPricePerMtok",
|
|
152
|
+
label: "Jev price/Mtok",
|
|
153
|
+
values: numericValues(
|
|
154
|
+
settings.jevPricePerMtok ?? DEFAULT_JEV_PRICE_PER_MTOK,
|
|
155
|
+
[0.01, 0.02, 0.042, 0.05, 0.1]
|
|
156
|
+
),
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
currentValue:
|
|
160
|
+
settings.jevTurnGateEveryTurns === 0 ||
|
|
161
|
+
settings.jevTurnGateEveryTurns === undefined
|
|
162
|
+
? "Off"
|
|
163
|
+
: `every ${settings.jevTurnGateEveryTurns} turns`,
|
|
164
|
+
description:
|
|
165
|
+
"Proactively consult the Advisor every N turns without a consultation (0 = off).",
|
|
166
|
+
id: "jevTurnGateEveryTurns",
|
|
167
|
+
label: "Jev turn gate",
|
|
168
|
+
values: [
|
|
169
|
+
"Off",
|
|
170
|
+
"every 3 turns",
|
|
171
|
+
"every 5 turns",
|
|
172
|
+
"every 10 turns",
|
|
173
|
+
"every 20 turns",
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
currentValue: String(
|
|
178
|
+
settings.jevTurnGateNoulThreshold ?? DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD
|
|
179
|
+
),
|
|
180
|
+
description:
|
|
181
|
+
"Jev confidence required before the turn gate interrupts with advice.",
|
|
182
|
+
id: "jevTurnGateNoulThreshold",
|
|
183
|
+
label: "Jev turn-gate threshold",
|
|
184
|
+
values: numericValues(
|
|
185
|
+
settings.jevTurnGateNoulThreshold ?? DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD,
|
|
186
|
+
[0.6, 0.7, 0.8, 0.85, 0.9, 0.95]
|
|
187
|
+
),
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
currentValue: settings.jevTransport ?? DEFAULT_JEV_TRANSPORT,
|
|
191
|
+
description:
|
|
192
|
+
"How Jev calls travel: auto reuses an OpenRouter login when no TypeSafe key is set.",
|
|
193
|
+
id: "jevTransport",
|
|
194
|
+
label: "Jev transport",
|
|
195
|
+
values: withCurrentValue(settings.jevTransport ?? DEFAULT_JEV_TRANSPORT, [
|
|
196
|
+
"auto",
|
|
197
|
+
"typesafe",
|
|
198
|
+
"openrouter",
|
|
199
|
+
]),
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
|
|
43
203
|
export const createSettingsItems = ({
|
|
44
204
|
effortLevels,
|
|
45
205
|
presets,
|
|
@@ -317,7 +477,8 @@ export const createSettingsItems = ({
|
|
|
317
477
|
"Allow anonymized Advisor outcomes to be logged globally.",
|
|
318
478
|
settings.outcomeLogging,
|
|
319
479
|
false
|
|
320
|
-
)
|
|
480
|
+
),
|
|
481
|
+
...jevItems(settings, theme, tui)
|
|
321
482
|
);
|
|
322
483
|
return items;
|
|
323
484
|
};
|