pi-advisor-flow 0.6.0 → 0.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +34 -17
  2. package/README.md +10 -8
  3. package/dist/index.js +2852 -868
  4. package/extensions/index.ts +2 -1
  5. package/package.json +55 -40
  6. package/src/attachments.ts +11 -10
  7. package/src/commands/activation-preparation.ts +1 -0
  8. package/src/commands/activation.ts +2 -0
  9. package/src/commands/lifecycle.ts +1 -0
  10. package/src/commands/manual-command.ts +7 -1
  11. package/src/commands/manual-consultation.ts +103 -79
  12. package/src/commands/manual-progress.ts +7 -5
  13. package/src/commands/model-commands.ts +1 -1
  14. package/src/commands/model-options.ts +14 -3
  15. package/src/commands/model-picker.ts +1 -0
  16. package/src/commands/registration.ts +1 -0
  17. package/src/commands/renderers.ts +67 -55
  18. package/src/commands/runtime.ts +16 -14
  19. package/src/commands/settings-commands.ts +10 -4
  20. package/src/commands/settings-persistence.ts +64 -2
  21. package/src/commands/types.ts +2 -0
  22. package/src/commands.ts +0 -1
  23. package/src/config/args.ts +1 -1
  24. package/src/config/defaults.ts +83 -1
  25. package/src/config/schema.ts +152 -15
  26. package/src/config/state.ts +80 -2
  27. package/src/config/storage.ts +38 -22
  28. package/src/config/types.ts +45 -3
  29. package/src/config/validation.ts +33 -21
  30. package/src/config.ts +3 -0
  31. package/src/content-utils.ts +24 -4
  32. package/src/conversation.ts +16 -15
  33. package/src/git.ts +10 -3
  34. package/src/herdr-block.ts +83 -0
  35. package/src/herdr-shared.ts +108 -0
  36. package/src/herdr.ts +30 -192
  37. package/src/jev/client.ts +338 -0
  38. package/src/jev/failure.ts +19 -0
  39. package/src/jev/key-store.ts +267 -0
  40. package/src/jev/ledger.ts +228 -0
  41. package/src/jev/questions.ts +122 -0
  42. package/src/jev/state.ts +50 -0
  43. package/src/jev/transport.ts +63 -0
  44. package/src/model-stream.ts +35 -37
  45. package/src/outcomes.ts +20 -12
  46. package/src/pi-settings.ts +4 -9
  47. package/src/preferences.ts +4 -2
  48. package/src/redaction.ts +10 -12
  49. package/src/scout-context.ts +59 -60
  50. package/src/scout-curation.ts +4 -13
  51. package/src/scout-groups.ts +178 -175
  52. package/src/scout-protocol.ts +74 -70
  53. package/src/scout-reconstruct.ts +1 -1
  54. package/src/scout.ts +115 -73
  55. package/src/session-state.ts +148 -39
  56. package/src/tool-result-cap.ts +18 -17
  57. package/src/tools/consult-context.ts +7 -15
  58. package/src/tools/consultation.ts +14 -4
  59. package/src/tools/gate-policy.ts +1 -0
  60. package/src/tools/gate-protocol.ts +36 -23
  61. package/src/tools/jev-filter.ts +204 -0
  62. package/src/tools/jev-turn-gate.ts +203 -0
  63. package/src/tools/loop-gate.ts +32 -20
  64. package/src/tools/model-access.ts +46 -0
  65. package/src/tools/outage-notifier.ts +30 -0
  66. package/src/tools/prompts.ts +20 -25
  67. package/src/tools/register-ask-advisor.ts +73 -17
  68. package/src/tools/register-lifecycle.ts +22 -0
  69. package/src/tools/register-outcome.ts +2 -0
  70. package/src/tools/register-renderers.ts +149 -73
  71. package/src/tools/registration.ts +15 -0
  72. package/src/tools/render-advisor-result.ts +77 -35
  73. package/src/tools/render-common.ts +16 -19
  74. package/src/tools/scout-status.ts +42 -27
  75. package/src/tools/types.ts +11 -0
  76. package/src/tools.ts +0 -1
  77. package/src/ui/jev-setup-submenu.ts +350 -0
  78. package/src/ui/manual-dialog-render.ts +1 -0
  79. package/src/ui/manual-dialog.ts +39 -43
  80. package/src/ui/masked-input.ts +62 -0
  81. package/src/ui/model-multi-selector.ts +9 -0
  82. package/src/ui/model-selector-adapter.ts +31 -0
  83. package/src/ui/model-selector.ts +14 -161
  84. package/src/ui/searchable-model-list.ts +230 -0
  85. package/src/ui/settings-formatting.ts +24 -14
  86. package/src/ui/settings-items.ts +200 -2
  87. package/src/ui/settings-list-adapter.ts +15 -12
  88. package/src/ui/settings-mutations.ts +169 -47
  89. package/src/ui/settings-selector.ts +18 -9
  90. package/src/ui/text-setting-submenu.ts +3 -6
  91. package/src/ui/types.ts +25 -0
  92. package/src/ui.ts +0 -1
  93. package/src/usage.ts +29 -20
@@ -1,6 +1,21 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import type { SettingItem } from "@earendil-works/pi-tui";
2
+ import { getKeybindings } from "@earendil-works/pi-tui";
3
+ import type { KeybindingsManager, SettingItem } from "@earendil-works/pi-tui";
4
+
5
+ import {
6
+ DEFAULT_JEV_DIGEST_MAX_CHARS,
7
+ DEFAULT_JEV_FILTER_NOUL_MARGIN,
8
+ DEFAULT_JEV_FILTER_OVERRIDE_WINDOW,
9
+ DEFAULT_JEV_FILTER_SKIP_CONFIDENCE,
10
+ DEFAULT_JEV_MODEL,
11
+ DEFAULT_JEV_PRICE_PER_MTOK,
12
+ DEFAULT_JEV_TIMEOUT_MS,
13
+ DEFAULT_JEV_TRANSPORT,
14
+ DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD,
15
+ } from "../config/types.ts";
3
16
  import { isValidAdvisorToolPolicies } from "../config/validation.ts";
17
+ import { JevSetupSubmenu } from "./jev-setup-submenu.ts";
18
+ import { SearchableModelMultiSelector } from "./model-multi-selector.ts";
4
19
  import {
5
20
  contextDescription,
6
21
  currentContextLabel,
@@ -20,6 +35,7 @@ import type {
20
35
 
21
36
  export interface SettingsItemsOptions {
22
37
  effortLevels: string[];
38
+ modelWhitelist: SettingItem;
23
39
  presets: ContextPreset[];
24
40
  settings: AdvisorSettings;
25
41
  theme: Theme;
@@ -40,8 +56,187 @@ const toggle = (
40
56
  values: TOGGLE_VALUES,
41
57
  });
42
58
 
59
+ const jevItems = (
60
+ settings: AdvisorSettings,
61
+ theme: Theme,
62
+ tui: RenderRequester
63
+ ): SettingItem[] => [
64
+ {
65
+ currentValue: settingValue(settings.jevFilterEnabled, false),
66
+ description:
67
+ "Screen low-stakes ask_advisor consultations with Jev; guided setup verifies credentials.",
68
+ id: "jevFilter",
69
+ label: "Jev consultation filter",
70
+ submenu: (currentValue, done) =>
71
+ new JevSetupSubmenu({ currentValue, done, theme, tui }),
72
+ },
73
+ {
74
+ currentValue: String(
75
+ settings.jevFilterSkipConfidence ?? DEFAULT_JEV_FILTER_SKIP_CONFIDENCE
76
+ ),
77
+ description:
78
+ "Required probability on negligible stakes before a consultation is skipped.",
79
+ id: "jevFilterSkipConfidence",
80
+ label: "Jev skip confidence",
81
+ values: numericValues(
82
+ settings.jevFilterSkipConfidence ?? DEFAULT_JEV_FILTER_SKIP_CONFIDENCE,
83
+ [0.6, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95]
84
+ ),
85
+ },
86
+ {
87
+ currentValue: String(
88
+ settings.jevFilterNoulMargin ?? DEFAULT_JEV_FILTER_NOUL_MARGIN
89
+ ),
90
+ description:
91
+ "Extra margin over a coin flip required on self-answerability before skipping.",
92
+ id: "jevFilterNoulMargin",
93
+ label: "Jev Noul margin",
94
+ values: numericValues(
95
+ settings.jevFilterNoulMargin ?? DEFAULT_JEV_FILTER_NOUL_MARGIN,
96
+ [0.1, 0.2, 0.25, 0.3, 0.35, 0.4, 0.45]
97
+ ),
98
+ },
99
+ {
100
+ currentValue: `${settings.jevFilterOverrideWindow ?? DEFAULT_JEV_FILTER_OVERRIDE_WINDOW} turns`,
101
+ description:
102
+ "Turns after a skip during which the same question passes automatically.",
103
+ id: "jevFilterOverrideWindow",
104
+ label: "Jev override window",
105
+ values: numericValues(
106
+ settings.jevFilterOverrideWindow ?? DEFAULT_JEV_FILTER_OVERRIDE_WINDOW,
107
+ [0, 3, 5, 10, 20, 50]
108
+ ).map((value) => `${value} turns`),
109
+ },
110
+ {
111
+ currentValue: settings.jevModel ?? DEFAULT_JEV_MODEL,
112
+ description: "TypeSafe Jev model used for screening and turn-gate checks.",
113
+ id: "jevModel",
114
+ label: "Jev model",
115
+ submenu: (_currentValue, done) =>
116
+ new TextSettingSubmenu({
117
+ description:
118
+ "Enter a TypeSafe model name (for example jev-latest or jev-1.13.0).",
119
+ initial: settings.jevModel ?? DEFAULT_JEV_MODEL,
120
+ onCancel: done,
121
+ onSubmit: (value) => ({ value: value.trim() || DEFAULT_JEV_MODEL }),
122
+ theme,
123
+ title: "Jev model",
124
+ tui,
125
+ }),
126
+ },
127
+ {
128
+ currentValue: String(settings.jevTimeoutMs ?? DEFAULT_JEV_TIMEOUT_MS),
129
+ description: "Total wall-time budget for one Jev call, including retries.",
130
+ id: "jevTimeoutMs",
131
+ label: "Jev timeout ms",
132
+ values: numericValues(
133
+ settings.jevTimeoutMs ?? DEFAULT_JEV_TIMEOUT_MS,
134
+ [1000, 2000, 5000, 8000, 15_000, 30_000]
135
+ ),
136
+ },
137
+ {
138
+ currentValue: String(
139
+ settings.jevDigestMaxChars ?? DEFAULT_JEV_DIGEST_MAX_CHARS
140
+ ),
141
+ description: "Conversation characters sent to Jev as screening evidence.",
142
+ id: "jevDigestMaxChars",
143
+ label: "Jev digest chars",
144
+ values: numericValues(
145
+ settings.jevDigestMaxChars ?? DEFAULT_JEV_DIGEST_MAX_CHARS,
146
+ [0, 1000, 2000, 4000, 8000, 15_000]
147
+ ),
148
+ },
149
+ {
150
+ currentValue: String(
151
+ settings.jevPricePerMtok ?? DEFAULT_JEV_PRICE_PER_MTOK
152
+ ),
153
+ description:
154
+ "Assumed TypeSafe price per million input tokens for cost lines.",
155
+ id: "jevPricePerMtok",
156
+ label: "Jev price/Mtok",
157
+ values: numericValues(
158
+ settings.jevPricePerMtok ?? DEFAULT_JEV_PRICE_PER_MTOK,
159
+ [0.01, 0.02, 0.042, 0.05, 0.1]
160
+ ),
161
+ },
162
+ {
163
+ currentValue:
164
+ settings.jevTurnGateEveryTurns === 0 ||
165
+ settings.jevTurnGateEveryTurns === undefined
166
+ ? "Off"
167
+ : `every ${settings.jevTurnGateEveryTurns} turns`,
168
+ description:
169
+ "Proactively consult the Advisor every N turns without a consultation (0 = off).",
170
+ id: "jevTurnGateEveryTurns",
171
+ label: "Jev turn gate",
172
+ values: [
173
+ "Off",
174
+ "every 3 turns",
175
+ "every 5 turns",
176
+ "every 10 turns",
177
+ "every 20 turns",
178
+ ],
179
+ },
180
+ {
181
+ currentValue: String(
182
+ settings.jevTurnGateNoulThreshold ?? DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD
183
+ ),
184
+ description:
185
+ "Jev confidence required before the turn gate interrupts with advice.",
186
+ id: "jevTurnGateNoulThreshold",
187
+ label: "Jev turn-gate threshold",
188
+ values: numericValues(
189
+ settings.jevTurnGateNoulThreshold ?? DEFAULT_JEV_TURN_GATE_NOUL_THRESHOLD,
190
+ [0.6, 0.7, 0.8, 0.85, 0.9, 0.95]
191
+ ),
192
+ },
193
+ {
194
+ currentValue: settings.jevTransport ?? DEFAULT_JEV_TRANSPORT,
195
+ description:
196
+ "How Jev calls travel: auto reuses an OpenRouter login when no TypeSafe key is set.",
197
+ id: "jevTransport",
198
+ label: "Jev transport",
199
+ values: withCurrentValue(settings.jevTransport ?? DEFAULT_JEV_TRANSPORT, [
200
+ "auto",
201
+ "typesafe",
202
+ "openrouter",
203
+ ]),
204
+ },
205
+ ];
206
+
207
+ export const advisorModelWhitelistItem = (
208
+ settings: AdvisorSettings,
209
+ modelRefs: string[] | undefined,
210
+ keybindings: KeybindingsManager | undefined,
211
+ theme: Theme,
212
+ tui: RenderRequester
213
+ ): SettingItem => ({
214
+ currentValue: settings.modelWhitelist?.length
215
+ ? settings.modelWhitelist.join(", ")
216
+ : "Any model",
217
+ description:
218
+ "Only the exact provider/model references listed here may call the Advisor; an empty list allows every model.",
219
+ id: "modelWhitelist",
220
+ label: "Advisor model whitelist",
221
+ submenu: (_currentValue, done) =>
222
+ new SearchableModelMultiSelector({
223
+ allOptions: [
224
+ ...new Set([...(modelRefs ?? []), ...(settings.modelWhitelist ?? [])]),
225
+ ],
226
+ currentOptions: settings.modelWhitelist ?? [],
227
+ keybindings: keybindings ?? getKeybindings(),
228
+ multiSelect: true,
229
+ onCancel: done,
230
+ onSelect: (values) => done(values.join(",")),
231
+ theme,
232
+ title: "Advisor model whitelist",
233
+ tui,
234
+ }),
235
+ });
236
+
43
237
  export const createSettingsItems = ({
44
238
  effortLevels,
239
+ modelWhitelist,
45
240
  presets,
46
241
  settings,
47
242
  theme,
@@ -73,6 +268,7 @@ export const createSettingsItems = ({
73
268
  },
74
269
  ];
75
270
  if (settings.simpleMode) {
271
+ items.push(modelWhitelist);
76
272
  return items;
77
273
  }
78
274
 
@@ -84,6 +280,7 @@ export const createSettingsItems = ({
84
280
  label: "Advisor reasoning",
85
281
  values: withCurrentValue(currentEffort(settings.effort), effortLevels),
86
282
  },
283
+ modelWhitelist,
87
284
  toggle(
88
285
  "scoutEnabled",
89
286
  "Experimental Advisor Scout",
@@ -317,7 +514,8 @@ export const createSettingsItems = ({
317
514
  "Allow anonymized Advisor outcomes to be logged globally.",
318
515
  settings.outcomeLogging,
319
516
  false
320
- )
517
+ ),
518
+ ...jevItems(settings, theme, tui)
321
519
  );
322
520
  return items;
323
521
  };
@@ -1,11 +1,10 @@
1
- import {
2
- type Component,
3
- type Focusable,
4
- type Input,
5
- Key,
6
- matchesKey,
7
- type SettingItem,
8
- type SettingsList,
1
+ import { Key, matchesKey } from "@earendil-works/pi-tui";
2
+ import type {
3
+ Component,
4
+ Focusable,
5
+ Input,
6
+ SettingItem,
7
+ SettingsList,
9
8
  } from "@earendil-works/pi-tui";
10
9
 
11
10
  interface SettingsListPrivateFields {
@@ -15,6 +14,8 @@ interface SettingsListPrivateFields {
15
14
  submenuComponent?: Component | null;
16
15
  }
17
16
 
17
+ type SettingsListWithPrivateFields = SettingsList & SettingsListPrivateFields;
18
+
18
19
  /**
19
20
  * Contains the narrow compatibility access needed for pi-tui's SettingsList.
20
21
  * No other UI module should depend on its private fields.
@@ -45,6 +46,7 @@ export class SettingsListAdapter {
45
46
 
46
47
  setFocused(value: boolean): void {
47
48
  const fields = this.privateFields();
49
+ // SAFETY: a SettingsList submenu component is registered only when focusable.
48
50
  const submenu = fields.submenuComponent as Focusable | undefined;
49
51
  if (submenu) {
50
52
  submenu.focused = value;
@@ -53,7 +55,7 @@ export class SettingsListAdapter {
53
55
 
54
56
  setSelectedId(items: SettingItem[], selectedId: string): void {
55
57
  const selectedIndex = items.findIndex((item) => item.id === selectedId);
56
- if (selectedIndex >= 0) {
58
+ if (selectedIndex !== -1) {
57
59
  this.privateFields().selectedIndex = selectedIndex;
58
60
  }
59
61
  }
@@ -63,9 +65,9 @@ export class SettingsListAdapter {
63
65
  onChange: (id: string, value: string) => void
64
66
  ): boolean {
65
67
  let direction = 0;
66
- if (matchesKey(keyData, Key.left) || keyData === "\u001b[D") {
68
+ if (matchesKey(keyData, Key.left) || keyData === "\u001B[D") {
67
69
  direction = -1;
68
- } else if (matchesKey(keyData, Key.right) || keyData === "\u001b[C") {
70
+ } else if (matchesKey(keyData, Key.right) || keyData === "\u001B[C") {
69
71
  direction = 1;
70
72
  }
71
73
  if (direction === 0) {
@@ -98,6 +100,7 @@ export class SettingsListAdapter {
98
100
  }
99
101
 
100
102
  private privateFields(): SettingsListPrivateFields {
101
- return this.list as unknown as SettingsListPrivateFields;
103
+ // SAFETY: compatibility-only read of SettingsList's private layout fields.
104
+ return this.list as SettingsListWithPrivateFields;
102
105
  }
103
106
  }
@@ -1,80 +1,202 @@
1
- import type { AdvisorSettings, ContextPreset, SettingValue } from "./types.ts";
1
+ import { DEFAULT_JEV_MODEL } from "../config/types.ts";
2
+ import type { AdvisorSettings, ContextPreset } from "./types.ts";
2
3
 
3
- const BOOLEAN_SETTING_IDS = new Set([
4
- "scoutEnabled",
5
- "showUsageDetails",
6
- "showUsageFooter",
7
- "planGate",
8
- "failureGate",
9
- "completionGate",
10
- "collapseResponses",
11
- "blockOnBlocked",
4
+ const BOOLEAN_SETTING_FIELDS = [
12
5
  "autoLoopGate",
13
- "sessionSummary",
6
+ "blockOnBlocked",
7
+ "collapseResponses",
8
+ "completionGate",
9
+ "failureGate",
14
10
  "herdrIntegration",
11
+ "outcomeLogging",
12
+ "planGate",
15
13
  "redactSecrets",
14
+ "scoutEnabled",
15
+ "sessionSummary",
16
+ "showUsageDetails",
17
+ "showUsageFooter",
16
18
  "trackedFileContent",
17
19
  "untrackedContent",
18
- "outcomeLogging",
19
- ]);
20
+ ] as const;
20
21
 
21
- export const mutateAdvisorSettings = (
22
+ const BOOLEAN_SETTING_IDS = new Set<string>(BOOLEAN_SETTING_FIELDS);
23
+
24
+ type BooleanSettingField = (typeof BOOLEAN_SETTING_FIELDS)[number];
25
+
26
+ const parseModelWhitelist = (value: string) => [
27
+ ...new Set(
28
+ value
29
+ .split(",")
30
+ .map((model) => model.trim())
31
+ .filter(Boolean)
32
+ ),
33
+ ];
34
+
35
+ const applyCoreMutation = (
22
36
  settings: AdvisorSettings,
23
37
  id: string,
24
38
  value: string,
25
39
  presets: ContextPreset[]
26
- ): void => {
40
+ ): boolean => {
27
41
  switch (id) {
28
- case "context":
42
+ case "context": {
29
43
  settings.contextMaxChars =
30
44
  presets.find((preset) => preset.label === value)?.value ??
31
45
  settings.contextMaxChars;
32
- break;
33
- case "simpleMode":
46
+ return true;
47
+ }
48
+ case "simpleMode": {
34
49
  settings.simpleMode = value === "On";
35
- break;
36
- case "alwaysOn":
50
+ return true;
51
+ }
52
+ case "alwaysOn": {
37
53
  settings.alwaysOn = value === "On";
38
- break;
39
- case "effort":
54
+ return true;
55
+ }
56
+ case "effort": {
40
57
  settings.effort = value;
41
- break;
42
- case "customRule":
58
+ return true;
59
+ }
60
+ case "customRule": {
43
61
  settings.customRule = value.trim() || undefined;
44
- break;
45
- case "toolPolicies":
62
+ return true;
63
+ }
64
+ case "toolPolicies": {
65
+ // SAFETY: the value originates from the settings submenu's previously validated JSON editor.
46
66
  settings.toolPolicies = JSON.parse(
47
67
  value
48
68
  ) as AdvisorSettings["toolPolicies"];
49
- break;
50
- case "loopThreshold":
69
+ return true;
70
+ }
71
+ case "loopThreshold": {
51
72
  settings.loopThreshold = Number(
52
73
  value.replace("After ", "").replace(" repeats", "")
53
74
  );
54
- break;
55
- case "maxCallsPerSession":
75
+ return true;
76
+ }
77
+ case "maxCallsPerSession": {
56
78
  settings.maxCallsPerSession = value === "∞" ? undefined : Number(value);
57
- break;
58
- case "failureMode":
79
+ return true;
80
+ }
81
+ case "modelWhitelist": {
82
+ settings.modelWhitelist = parseModelWhitelist(value);
83
+ return true;
84
+ }
85
+ case "failureMode": {
86
+ // SAFETY: value comes from the fixed failureMode options of the settings list.
59
87
  settings.failureMode = value as AdvisorSettings["failureMode"];
60
- break;
61
- case "gitContext":
88
+ return true;
89
+ }
90
+ case "gitContext": {
91
+ // SAFETY: value comes from the fixed GitContextLevel options of the settings list.
62
92
  settings.gitContext = value as AdvisorSettings["gitContext"];
63
- break;
64
- case "toolResultMaxLines":
93
+ return true;
94
+ }
95
+ default: {
96
+ return false;
97
+ }
98
+ }
99
+ };
100
+
101
+ const applyNumericMutation = (
102
+ settings: AdvisorSettings,
103
+ id: string,
104
+ value: string
105
+ ): boolean => {
106
+ switch (id) {
107
+ case "toolResultMaxLines": {
65
108
  settings.toolResultMaxLines = Number(value);
66
- break;
67
- case "toolResultMaxBytes":
109
+ return true;
110
+ }
111
+ case "toolResultMaxBytes": {
68
112
  settings.toolResultMaxBytes = Number(value);
69
- break;
70
- case "gitContextMaxChars":
113
+ return true;
114
+ }
115
+ case "gitContextMaxChars": {
71
116
  settings.gitContextMaxChars = Number(value);
72
- break;
73
- default:
74
- if (BOOLEAN_SETTING_IDS.has(id)) {
75
- (settings as unknown as Record<string, SettingValue | boolean>)[id] =
76
- value === "On";
77
- }
78
- break;
117
+ return true;
118
+ }
119
+ default: {
120
+ return false;
121
+ }
122
+ }
123
+ };
124
+
125
+ const applyJevMutation = (
126
+ settings: AdvisorSettings,
127
+ id: string,
128
+ value: string
129
+ ): boolean => {
130
+ switch (id) {
131
+ case "jevFilter": {
132
+ settings.jevFilterEnabled = value === "On";
133
+ return true;
134
+ }
135
+ case "jevFilterSkipConfidence": {
136
+ settings.jevFilterSkipConfidence = Number(value);
137
+ return true;
138
+ }
139
+ case "jevFilterNoulMargin": {
140
+ settings.jevFilterNoulMargin = Number(value);
141
+ return true;
142
+ }
143
+ case "jevFilterOverrideWindow": {
144
+ settings.jevFilterOverrideWindow = Number(value.replace(" turns", ""));
145
+ return true;
146
+ }
147
+ case "jevTurnGateEveryTurns": {
148
+ settings.jevTurnGateEveryTurns =
149
+ value === "Off" ? 0 : Number(value.replaceAll(/[^0-9]/gu, ""));
150
+ return true;
151
+ }
152
+ case "jevTurnGateNoulThreshold": {
153
+ settings.jevTurnGateNoulThreshold = Number(value);
154
+ return true;
155
+ }
156
+ case "jevModel": {
157
+ settings.jevModel = value.trim() || DEFAULT_JEV_MODEL;
158
+ return true;
159
+ }
160
+ case "jevTimeoutMs": {
161
+ settings.jevTimeoutMs = Number(value);
162
+ return true;
163
+ }
164
+ case "jevDigestMaxChars": {
165
+ settings.jevDigestMaxChars = Number(value);
166
+ return true;
167
+ }
168
+ case "jevPricePerMtok": {
169
+ settings.jevPricePerMtok = Number(value);
170
+ return true;
171
+ }
172
+ case "jevTransport": {
173
+ // SAFETY: value comes from the fixed jevTransport options of the settings list.
174
+ settings.jevTransport = value as AdvisorSettings["jevTransport"];
175
+ return true;
176
+ }
177
+ default: {
178
+ return false;
179
+ }
180
+ }
181
+ };
182
+
183
+ export const mutateAdvisorSettings = (
184
+ settings: AdvisorSettings,
185
+ id: string,
186
+ value: string,
187
+ presets: ContextPreset[]
188
+ ): void => {
189
+ if (applyCoreMutation(settings, id, value, presets)) {
190
+ return;
191
+ }
192
+ if (applyNumericMutation(settings, id, value)) {
193
+ return;
194
+ }
195
+ if (applyJevMutation(settings, id, value)) {
196
+ return;
197
+ }
198
+ if (BOOLEAN_SETTING_IDS.has(id)) {
199
+ // SAFETY: BOOLEAN_SETTING_IDS only contains AdvisorSettings boolean fields.
200
+ settings[id as BooleanSettingField] = value === "On";
79
201
  }
80
202
  };
@@ -1,15 +1,15 @@
1
1
  import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
2
- import {
3
- type Component,
4
- type Focusable,
5
- SettingsList,
6
- truncateToWidth,
7
- } from "@earendil-works/pi-tui";
2
+ import { SettingsList, truncateToWidth } from "@earendil-works/pi-tui";
3
+ import type { Component, Focusable } from "@earendil-works/pi-tui";
4
+
8
5
  import {
9
6
  rainbowGradient,
10
7
  SIMPLE_MODE_GRADIENT_INTERVAL_MS,
11
8
  } from "./settings-formatting.ts";
12
- import { createSettingsItems } from "./settings-items.ts";
9
+ import {
10
+ advisorModelWhitelistItem,
11
+ createSettingsItems,
12
+ } from "./settings-items.ts";
13
13
  import { SettingsListAdapter } from "./settings-list-adapter.ts";
14
14
  import { mutateAdvisorSettings } from "./settings-mutations.ts";
15
15
  import type {
@@ -51,7 +51,7 @@ export class AdvisorSettingsSelector implements Component, Focusable {
51
51
  label: String(configuredContext),
52
52
  value: configuredContext,
53
53
  },
54
- ].sort((a, b) => a.value - b.value);
54
+ ].toSorted((a, b) => a.value - b.value);
55
55
  if (this.settings.simpleMode) {
56
56
  this.startSimpleModeGradient();
57
57
  }
@@ -96,8 +96,16 @@ export class AdvisorSettingsSelector implements Component, Focusable {
96
96
  }
97
97
  return defaultLabel(text, selected);
98
98
  };
99
+ const modelWhitelist = advisorModelWhitelistItem(
100
+ this.settings,
101
+ this.options.modelRefs,
102
+ this.options.keybindings,
103
+ this.options.theme,
104
+ this.options.tui
105
+ );
99
106
  const items = createSettingsItems({
100
107
  effortLevels: this.options.effortLevels,
108
+ modelWhitelist,
101
109
  presets: this.presets,
102
110
  settings: this.settings,
103
111
  theme: this.options.theme,
@@ -147,12 +155,13 @@ export class AdvisorSettingsSelector implements Component, Focusable {
147
155
  (this.options.onChange ?? this.options.onSave)?.({
148
156
  ...this.settings,
149
157
  showUsageDetails: this.settings.showUsageDetails ?? true,
150
- toolPolicies: { ...(this.settings.toolPolicies ?? {}) },
158
+ toolPolicies: { ...this.settings.toolPolicies },
151
159
  });
152
160
  if (
153
161
  id === "context" ||
154
162
  id === "simpleMode" ||
155
163
  id === "customRule" ||
164
+ id === "modelWhitelist" ||
156
165
  id === "toolPolicies"
157
166
  ) {
158
167
  this.settingsList = this.createSettingsList(id);
@@ -1,9 +1,6 @@
1
- import {
2
- type Component,
3
- type Focusable,
4
- Input,
5
- truncateToWidth,
6
- } from "@earendil-works/pi-tui";
1
+ import { Input, truncateToWidth } from "@earendil-works/pi-tui";
2
+ import type { Component, Focusable } from "@earendil-works/pi-tui";
3
+
7
4
  import type { TextSettingSubmenuOptions } from "./types.ts";
8
5
 
9
6
  /** Small inline editor used by SettingsList for the two free-form settings. */
package/src/ui/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import type { KeybindingsManager, TUI } from "@earendil-works/pi-tui";
3
+
3
4
  import type { GateFailureMode } from "../config/types.ts";
4
5
  import type { GitContextLevel } from "../git.ts";
5
6
 
@@ -11,6 +12,7 @@ export interface SearchableModelSelectorOptions {
11
12
  allOptions: string[];
12
13
  currentOption?: string;
13
14
  keybindings: KeybindingsManager;
15
+ multiSelect?: false;
14
16
  onCancel: () => void;
15
17
  onSelect: (value: string) => void;
16
18
  theme: Theme;
@@ -18,6 +20,15 @@ export interface SearchableModelSelectorOptions {
18
20
  tui: RenderRequester;
19
21
  }
20
22
 
23
+ export interface SearchableModelMultiSelectorOptions extends Omit<
24
+ SearchableModelSelectorOptions,
25
+ "currentOption" | "multiSelect" | "onSelect"
26
+ > {
27
+ currentOptions: string[];
28
+ multiSelect: true;
29
+ onSelect: (values: string[]) => void;
30
+ }
31
+
21
32
  export interface ManualAdvisorRequest {
22
33
  /** The repository disclosure level selected for this consultation. */
23
34
  gitContext: GitContextLevel;
@@ -69,8 +80,20 @@ export interface AdvisorSettings {
69
80
  gitContext?: GitContextLevel;
70
81
  gitContextMaxChars?: number;
71
82
  herdrIntegration?: boolean;
83
+ jevDigestMaxChars?: number;
84
+ jevFilterEnabled?: boolean;
85
+ jevFilterNoulMargin?: number;
86
+ jevFilterOverrideWindow?: number;
87
+ jevFilterSkipConfidence?: number;
88
+ jevModel?: string;
89
+ jevPricePerMtok?: number;
90
+ jevTimeoutMs?: number;
91
+ jevTransport?: "auto" | "typesafe" | "openrouter";
92
+ jevTurnGateEveryTurns?: number;
93
+ jevTurnGateNoulThreshold?: number;
72
94
  loopThreshold?: number;
73
95
  maxCallsPerSession?: number;
96
+ modelWhitelist?: string[];
74
97
  outcomeLogging?: boolean;
75
98
  planGate: boolean;
76
99
  redactSecrets?: boolean;
@@ -89,6 +112,8 @@ export interface AdvisorSettings {
89
112
  export interface AdvisorSettingsSelectorOptions {
90
113
  effortLevels: string[];
91
114
  initial: AdvisorSettings;
115
+ keybindings?: KeybindingsManager;
116
+ modelRefs?: string[];
92
117
  onCancel: () => void;
93
118
  onChange?: (settings: AdvisorSettings) => void;
94
119
  /** @deprecated Use onChange; retained for extensions embedding this component. */
package/src/ui.ts CHANGED
@@ -1,4 +1,3 @@
1
- // biome-ignore lint/performance/noBarrelFile: this facade intentionally preserves the public UI module contract.
2
1
  export { ManualAdvisorDialog } from "./ui/manual-dialog.ts";
3
2
  export { SearchableModelSelector } from "./ui/model-selector.ts";
4
3
  export { AdvisorSettingsSelector } from "./ui/settings-selector.ts";