pi-advisor-flow 0.6.0 → 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.
@@ -8,6 +8,8 @@ import type {
8
8
  GateTrigger,
9
9
  } from "../session-state.ts";
10
10
  import type { consultAdvisor, runAdvisorGate } from "./consultation.ts";
11
+ import type { JevSkipKind, screenConsultation } from "./jev-filter.ts";
12
+ import type { JevTurnGateDeps } from "./jev-turn-gate.ts";
11
13
  import type { ScoutStatusManager } from "./scout-status.ts";
12
14
 
13
15
  export type {
@@ -79,6 +81,11 @@ export interface AdvisorToolDetails {
79
81
  adviceId?: string;
80
82
  advisor?: string;
81
83
  draftBytes?: number;
84
+ jev?: {
85
+ kind: JevSkipKind;
86
+ reason: string;
87
+ skipped: boolean;
88
+ };
82
89
  preferenceBytes?: number;
83
90
  question?: string;
84
91
  scout?: ScoutToolDetails;
@@ -108,6 +115,7 @@ export interface ToolRegistrationContext {
108
115
  reservedCalls: Set<string>;
109
116
  runGate: typeof runAdvisorGate;
110
117
  scoutStatus: ScoutStatusManager;
118
+ screen: typeof screenConsultation;
111
119
  session: AdvisorSessionState;
112
120
  }
113
121
 
@@ -115,5 +123,7 @@ export interface ToolRegistrationDependencies {
115
123
  appendOutcome?: typeof appendOutcome;
116
124
  consult?: typeof consultAdvisor;
117
125
  runGate?: typeof runAdvisorGate;
126
+ screen?: typeof screenConsultation;
118
127
  statusManager?: ScoutStatusManager;
128
+ turnGateDeps?: JevTurnGateDeps;
119
129
  }
@@ -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
+ }
@@ -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
+ }
@@ -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 {
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";
3
14
  import { isValidAdvisorToolPolicies } from "../config/validation.ts";
15
+ import { JevSetupSubmenu } from "./jev-setup-submenu.ts";
4
16
  import {
5
17
  contextDescription,
6
18
  currentContextLabel,
@@ -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
  };
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_JEV_MODEL } from "../config/types.ts";
1
2
  import type { AdvisorSettings, ContextPreset, SettingValue } from "./types.ts";
2
3
 
3
4
  const BOOLEAN_SETTING_IDS = new Set([
@@ -70,6 +71,40 @@ export const mutateAdvisorSettings = (
70
71
  case "gitContextMaxChars":
71
72
  settings.gitContextMaxChars = Number(value);
72
73
  break;
74
+ case "jevFilter":
75
+ settings.jevFilterEnabled = value === "On";
76
+ break;
77
+ case "jevFilterSkipConfidence":
78
+ settings.jevFilterSkipConfidence = Number(value);
79
+ break;
80
+ case "jevFilterNoulMargin":
81
+ settings.jevFilterNoulMargin = Number(value);
82
+ break;
83
+ case "jevFilterOverrideWindow":
84
+ settings.jevFilterOverrideWindow = Number(value.replace(" turns", ""));
85
+ break;
86
+ case "jevTurnGateEveryTurns":
87
+ settings.jevTurnGateEveryTurns =
88
+ value === "Off" ? 0 : Number(value.replace(/[^0-9]/g, ""));
89
+ break;
90
+ case "jevTurnGateNoulThreshold":
91
+ settings.jevTurnGateNoulThreshold = Number(value);
92
+ break;
93
+ case "jevModel":
94
+ settings.jevModel = value.trim() || DEFAULT_JEV_MODEL;
95
+ break;
96
+ case "jevTimeoutMs":
97
+ settings.jevTimeoutMs = Number(value);
98
+ break;
99
+ case "jevDigestMaxChars":
100
+ settings.jevDigestMaxChars = Number(value);
101
+ break;
102
+ case "jevPricePerMtok":
103
+ settings.jevPricePerMtok = Number(value);
104
+ break;
105
+ case "jevTransport":
106
+ settings.jevTransport = value as AdvisorSettings["jevTransport"];
107
+ break;
73
108
  default:
74
109
  if (BOOLEAN_SETTING_IDS.has(id)) {
75
110
  (settings as unknown as Record<string, SettingValue | boolean>)[id] =
package/src/ui/types.ts CHANGED
@@ -69,6 +69,17 @@ export interface AdvisorSettings {
69
69
  gitContext?: GitContextLevel;
70
70
  gitContextMaxChars?: number;
71
71
  herdrIntegration?: boolean;
72
+ jevDigestMaxChars?: number;
73
+ jevFilterEnabled?: boolean;
74
+ jevFilterNoulMargin?: number;
75
+ jevFilterOverrideWindow?: number;
76
+ jevFilterSkipConfidence?: number;
77
+ jevModel?: string;
78
+ jevPricePerMtok?: number;
79
+ jevTimeoutMs?: number;
80
+ jevTransport?: "auto" | "typesafe" | "openrouter";
81
+ jevTurnGateEveryTurns?: number;
82
+ jevTurnGateNoulThreshold?: number;
72
83
  loopThreshold?: number;
73
84
  maxCallsPerSession?: number;
74
85
  outcomeLogging?: boolean;