pi-advisor-flow 0.5.1 → 0.5.2

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 (59) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +3 -2
  3. package/extensions/index.ts +7 -7
  4. package/package.json +2 -1
  5. package/src/commands/activation-preparation.ts +117 -0
  6. package/src/commands/activation.ts +135 -0
  7. package/src/commands/lifecycle.ts +83 -0
  8. package/src/commands/manual-command.ts +107 -0
  9. package/src/commands/manual-consultation.ts +144 -0
  10. package/src/commands/manual-progress.ts +93 -0
  11. package/src/commands/model-commands.ts +66 -0
  12. package/src/commands/model-options.ts +181 -0
  13. package/src/commands/model-picker.ts +110 -0
  14. package/src/commands/registration.ts +27 -0
  15. package/src/commands/renderers.ts +76 -0
  16. package/src/commands/runtime.ts +114 -0
  17. package/src/commands/settings-commands.ts +80 -0
  18. package/src/commands/settings-persistence.ts +81 -0
  19. package/src/commands/types.ts +78 -0
  20. package/src/commands.ts +9 -1259
  21. package/src/config/args.ts +37 -0
  22. package/src/config/defaults.ts +190 -0
  23. package/src/config/state.ts +199 -0
  24. package/src/config/storage.ts +224 -0
  25. package/src/config/types.ts +63 -0
  26. package/src/config/validation.ts +234 -0
  27. package/src/config.ts +104 -814
  28. package/src/conversation.ts +4 -2
  29. package/src/herdr.ts +1 -1
  30. package/src/model-stream.ts +15 -1
  31. package/src/scout-context.ts +2 -2
  32. package/src/scout.ts +1 -1
  33. package/src/tools/consultation.ts +337 -0
  34. package/src/tools/gate-policy.ts +132 -0
  35. package/src/tools/gate-protocol.ts +127 -0
  36. package/src/tools/loop-gate.ts +210 -0
  37. package/src/tools/prompts.ts +159 -0
  38. package/src/tools/register-ask-advisor.ts +225 -0
  39. package/src/tools/register-lifecycle.ts +89 -0
  40. package/src/tools/register-outcome.ts +75 -0
  41. package/src/tools/register-renderers.ts +88 -0
  42. package/src/tools/registration.ts +35 -0
  43. package/src/tools/render-advisor-result.ts +168 -0
  44. package/src/tools/render-common.ts +120 -0
  45. package/src/tools/scout-status.ts +206 -0
  46. package/src/tools/session.ts +3 -0
  47. package/src/tools/types.ts +119 -0
  48. package/src/tools.ts +42 -1865
  49. package/src/ui/manual-dialog-render.ts +150 -0
  50. package/src/ui/manual-dialog.ts +305 -0
  51. package/src/ui/model-selector.ts +166 -0
  52. package/src/ui/settings-formatting.ts +133 -0
  53. package/src/ui/settings-items.ts +323 -0
  54. package/src/ui/settings-list-adapter.ts +103 -0
  55. package/src/ui/settings-mutations.ts +80 -0
  56. package/src/ui/settings-selector.ts +161 -0
  57. package/src/ui/text-setting-submenu.ts +66 -0
  58. package/src/ui/types.ts +111 -0
  59. package/src/ui.ts +9 -1418
@@ -0,0 +1,133 @@
1
+ import { visibleWidth } from "@earendil-works/pi-tui";
2
+ import type { ContextPreset } from "./types.js";
3
+
4
+ export const DEFAULT_EFFORT_LEVEL = "Default (Model Default)";
5
+ export const TOGGLE_VALUES = ["On", "Off"];
6
+ export const SIMPLE_MODE_GRADIENT_INTERVAL_MS = 100;
7
+ // Purple steps with a moving light highlight, retained from the original UI.
8
+ const SIMPLE_MODE_GRADIENT_COLORS = [
9
+ [125, 79, 205],
10
+ [143, 96, 218],
11
+ [160, 114, 230],
12
+ [178, 135, 238],
13
+ [195, 157, 245],
14
+ [168, 120, 230],
15
+ [143, 89, 215],
16
+ ] as const;
17
+
18
+ export const withCurrentValue = (current: string, values: string[]) =>
19
+ values.includes(current) ? values : [current, ...values];
20
+
21
+ export const numericValues = (current: number, values: number[]) => {
22
+ const all = values.includes(current) ? values : [...values, current];
23
+ return all.sort((a, b) => a - b).map(String);
24
+ };
25
+
26
+ export const maxCallValues = (current: string) => {
27
+ const values = ["0", "1", "2", "3", "5", "10", "25", "50", "∞"];
28
+ if (values.includes(current)) {
29
+ return values;
30
+ }
31
+ const numeric = Number(current);
32
+ const insertionIndex = values.findIndex(
33
+ (value) => value !== "∞" && Number(value) > numeric
34
+ );
35
+ return insertionIndex === -1
36
+ ? [...values.slice(0, -1), current, "∞"]
37
+ : [
38
+ ...values.slice(0, insertionIndex),
39
+ current,
40
+ ...values.slice(insertionIndex),
41
+ ];
42
+ };
43
+
44
+ export const settingValue = (
45
+ value: boolean | undefined,
46
+ defaultValue: boolean
47
+ ) => ((value ?? defaultValue) ? "On" : "Off");
48
+
49
+ export const currentContextLabel = (
50
+ presets: ContextPreset[],
51
+ contextMaxChars: number
52
+ ): string =>
53
+ presets.find((preset) => preset.value === contextMaxChars)?.label ??
54
+ String(contextMaxChars);
55
+
56
+ export const contextDescription = (
57
+ presets: ContextPreset[],
58
+ contextMaxChars: number
59
+ ): string => {
60
+ const exactIndex = presets.findIndex(
61
+ (preset) => preset.value === contextMaxChars
62
+ );
63
+ const selectedIndex =
64
+ exactIndex >= 0
65
+ ? exactIndex
66
+ : presets.reduce(
67
+ (closestIndex, preset, index) =>
68
+ Math.abs(preset.value - contextMaxChars) <
69
+ Math.abs(presets[closestIndex].value - contextMaxChars)
70
+ ? index
71
+ : closestIndex,
72
+ 0
73
+ );
74
+ const selectedPreset = presets[selectedIndex];
75
+ const isFullContext =
76
+ selectedPreset?.value === Number.MAX_SAFE_INTEGER ||
77
+ selectedPreset?.label.toUpperCase() === "FULL" ||
78
+ selectedPreset?.label.toUpperCase() === "ALL";
79
+ const progress = isFullContext
80
+ ? 1
81
+ : selectedIndex / Math.max(1, presets.length - 1);
82
+ const meterWidth = 20;
83
+ const marker = Math.round(progress * meterWidth);
84
+ const meter = Array.from({ length: meterWidth + 1 }, (_, index) => {
85
+ if (index === marker) {
86
+ return "●";
87
+ }
88
+ return index < marker ? "━" : "─";
89
+ }).join("");
90
+ const label = currentContextLabel(presets, contextMaxChars);
91
+ const labelWidth = visibleWidth(label);
92
+ const meterPrefix = "none ";
93
+ const markerColumn = meterPrefix.length + marker;
94
+ const labelStart = Math.max(
95
+ 0,
96
+ Math.min(
97
+ meterPrefix.length + meter.length + 2 - labelWidth,
98
+ markerColumn - Math.floor((labelWidth - 1) / 2)
99
+ )
100
+ );
101
+ const markerLabel = `${" ".repeat(labelStart)}${label}`;
102
+ const description =
103
+ exactIndex >= 0 ? selectedPreset?.description : "Custom context limit.";
104
+ return `${description ?? "Custom context limit."}\n${meterPrefix}${meter} full\n${markerLabel}`;
105
+ };
106
+
107
+ export const currentEffort = (effort: string | undefined): string =>
108
+ effort || DEFAULT_EFFORT_LEVEL;
109
+
110
+ export const rainbowGradient = (text: string, startedAt: number): string => {
111
+ const frame = Math.floor(
112
+ (Date.now() - startedAt) / SIMPLE_MODE_GRADIENT_INTERVAL_MS
113
+ );
114
+ const shinePosition = frame % (text.length * 2);
115
+ return [...text]
116
+ .map((character, index) => {
117
+ const [baseRed, baseGreen, baseBlue] =
118
+ SIMPLE_MODE_GRADIENT_COLORS[index % SIMPLE_MODE_GRADIENT_COLORS.length];
119
+ const distance = Math.abs(index - shinePosition);
120
+ let brightness = 0;
121
+ if (distance === 0) {
122
+ brightness = 0.7;
123
+ } else if (distance === 1) {
124
+ brightness = 0.35;
125
+ }
126
+ const red = Math.round(baseRed + (255 - baseRed) * brightness);
127
+ const green = Math.round(baseGreen + (255 - baseGreen) * brightness);
128
+ const blue = Math.round(baseBlue + (255 - baseBlue) * brightness);
129
+ return `\x1b[38;2;${red};${green};${blue}m${character}`;
130
+ })
131
+ .join("")
132
+ .concat("\x1b[0m");
133
+ };
@@ -0,0 +1,323 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import type { SettingItem } from "@earendil-works/pi-tui";
3
+ import { isValidAdvisorToolPolicies } from "../config/validation.js";
4
+ import {
5
+ contextDescription,
6
+ currentContextLabel,
7
+ currentEffort,
8
+ maxCallValues,
9
+ numericValues,
10
+ settingValue,
11
+ TOGGLE_VALUES,
12
+ withCurrentValue,
13
+ } from "./settings-formatting.js";
14
+ import { TextSettingSubmenu } from "./text-setting-submenu.js";
15
+ import type {
16
+ AdvisorSettings,
17
+ ContextPreset,
18
+ RenderRequester,
19
+ } from "./types.js";
20
+
21
+ export interface SettingsItemsOptions {
22
+ effortLevels: string[];
23
+ presets: ContextPreset[];
24
+ settings: AdvisorSettings;
25
+ theme: Theme;
26
+ tui: RenderRequester;
27
+ }
28
+
29
+ const toggle = (
30
+ id: string,
31
+ label: string,
32
+ description: string,
33
+ value: boolean | undefined,
34
+ defaultValue: boolean
35
+ ): SettingItem => ({
36
+ currentValue: settingValue(value, defaultValue),
37
+ description,
38
+ id,
39
+ label,
40
+ values: TOGGLE_VALUES,
41
+ });
42
+
43
+ export const createSettingsItems = ({
44
+ effortLevels,
45
+ presets,
46
+ settings,
47
+ theme,
48
+ tui,
49
+ }: SettingsItemsOptions): SettingItem[] => {
50
+ const items: SettingItem[] = [
51
+ {
52
+ currentValue: currentContextLabel(presets, settings.contextMaxChars),
53
+ description: contextDescription(presets, settings.contextMaxChars),
54
+ id: "context",
55
+ label: "Context window",
56
+ values: presets.map((preset) => preset.label),
57
+ },
58
+ {
59
+ currentValue: settingValue(settings.simpleMode, false),
60
+ description:
61
+ "Keep the Advisor available on demand without automatic gates or blocks.",
62
+ id: "simpleMode",
63
+ label: "Simple mode",
64
+ values: TOGGLE_VALUES,
65
+ },
66
+ {
67
+ currentValue: settingValue(settings.alwaysOn, false),
68
+ description:
69
+ "Run the Advisor flow automatically for supported agent turns.",
70
+ id: "alwaysOn",
71
+ label: "Always on",
72
+ values: TOGGLE_VALUES,
73
+ },
74
+ ];
75
+ if (settings.simpleMode) {
76
+ return items;
77
+ }
78
+
79
+ items.push(
80
+ {
81
+ currentValue: currentEffort(settings.effort),
82
+ description: "Reasoning level used for Advisor calls.",
83
+ id: "effort",
84
+ label: "Advisor reasoning",
85
+ values: withCurrentValue(currentEffort(settings.effort), effortLevels),
86
+ },
87
+ toggle(
88
+ "scoutEnabled",
89
+ "Experimental Advisor Scout",
90
+ "Enable the experimental Scout before Advisor calls.",
91
+ settings.scoutEnabled,
92
+ false
93
+ ),
94
+ toggle(
95
+ "showUsageDetails",
96
+ "Show usage and cost details",
97
+ "Show token usage and cost details in Advisor responses.",
98
+ settings.showUsageDetails,
99
+ true
100
+ ),
101
+ toggle(
102
+ "showUsageFooter",
103
+ "Show usage in footer",
104
+ "Show the current Advisor usage summary in the footer.",
105
+ settings.showUsageFooter,
106
+ false
107
+ ),
108
+ toggle(
109
+ "planGate",
110
+ "Plan gate",
111
+ "Ask the Advisor to review implementation plans.",
112
+ settings.planGate,
113
+ true
114
+ ),
115
+ toggle(
116
+ "failureGate",
117
+ "Failure gate",
118
+ "Ask the Advisor to review repeated failures.",
119
+ settings.failureGate,
120
+ true
121
+ ),
122
+ toggle(
123
+ "completionGate",
124
+ "Completion gate",
125
+ "Ask the Advisor to review work before declaring success.",
126
+ settings.completionGate,
127
+ true
128
+ ),
129
+ toggle(
130
+ "collapseResponses",
131
+ "Collapse long responses",
132
+ "Collapse long Advisor responses in the transcript.",
133
+ settings.collapseResponses,
134
+ false
135
+ ),
136
+ {
137
+ currentValue: settings.customRule || "None",
138
+ description: "Add a rule that triggers Advisor involvement.",
139
+ id: "customRule",
140
+ label: "Custom invocation",
141
+ submenu: (_currentValue, done) =>
142
+ new TextSettingSubmenu({
143
+ description: "Enter a custom invocation rule.",
144
+ initial: settings.customRule || "",
145
+ onCancel: done,
146
+ onSubmit: (value) => ({ value: value.trim() }),
147
+ theme,
148
+ title: "Custom invocation",
149
+ tui,
150
+ }),
151
+ },
152
+ toggle(
153
+ "blockOnBlocked",
154
+ "Block on critical advice",
155
+ "Block the agent when the Advisor returns a critical decision.",
156
+ settings.blockOnBlocked,
157
+ true
158
+ ),
159
+ toggle(
160
+ "autoLoopGate",
161
+ "Automatic loop gate",
162
+ "Ask the Advisor to review repeated equivalent attempts.",
163
+ settings.autoLoopGate,
164
+ true
165
+ ),
166
+ {
167
+ currentValue: `After ${settings.loopThreshold ?? 3} repeats`,
168
+ description: "Number of equivalent attempts before automatic review.",
169
+ id: "loopThreshold",
170
+ label: "Loop threshold",
171
+ values: numericValues(
172
+ settings.loopThreshold ?? 3,
173
+ Array.from({ length: 99 }, (_, index) => index + 2)
174
+ ).map((value) => `After ${value} repeats`),
175
+ },
176
+ {
177
+ currentValue:
178
+ settings.maxCallsPerSession === undefined
179
+ ? "∞"
180
+ : String(settings.maxCallsPerSession),
181
+ description: "Limit automatic Advisor calls in one session.",
182
+ id: "maxCallsPerSession",
183
+ label: "Max Advisor calls/session",
184
+ values: maxCallValues(
185
+ settings.maxCallsPerSession === undefined
186
+ ? "∞"
187
+ : String(settings.maxCallsPerSession)
188
+ ),
189
+ },
190
+ toggle(
191
+ "sessionSummary",
192
+ "Session Advisor Summary",
193
+ "Show a local summary when the session ends.",
194
+ settings.sessionSummary,
195
+ false
196
+ ),
197
+ {
198
+ currentValue: settings.failureMode ?? "block-session",
199
+ description: "Choose what happens when an Advisor gate fails.",
200
+ id: "failureMode",
201
+ label: "Gate failure mode",
202
+ values: withCurrentValue(settings.failureMode ?? "block-session", [
203
+ "block-session",
204
+ "block-tool",
205
+ "warn-and-continue",
206
+ ]),
207
+ },
208
+ toggle(
209
+ "herdrIntegration",
210
+ "Herdr integration",
211
+ "Send Advisor activity to the optional Herdr integration.",
212
+ settings.herdrIntegration,
213
+ true
214
+ ),
215
+ {
216
+ currentValue: String(settings.toolResultMaxLines ?? 2000),
217
+ description: "Maximum lines included from a tool result.",
218
+ id: "toolResultMaxLines",
219
+ label: "Tool result lines",
220
+ values: numericValues(
221
+ settings.toolResultMaxLines ?? 2000,
222
+ [0, 500, 1000, 2000, 5000, 10_000]
223
+ ),
224
+ },
225
+ {
226
+ currentValue: String(settings.toolResultMaxBytes ?? 50 * 1024),
227
+ description: "Maximum bytes included from a tool result.",
228
+ id: "toolResultMaxBytes",
229
+ label: "Tool result bytes",
230
+ values: numericValues(settings.toolResultMaxBytes ?? 50 * 1024, [
231
+ 0,
232
+ 10 * 1024,
233
+ 50 * 1024,
234
+ 100 * 1024,
235
+ 500 * 1024,
236
+ ]),
237
+ },
238
+ toggle(
239
+ "redactSecrets",
240
+ "Redact common secrets",
241
+ "Redact common credential patterns before Advisor calls.",
242
+ settings.redactSecrets,
243
+ false
244
+ ),
245
+ {
246
+ currentValue: settings.gitContext ?? "summary",
247
+ description: "How much repository context is shared with the Advisor.",
248
+ id: "gitContext",
249
+ label: "Repository context",
250
+ values: withCurrentValue(settings.gitContext ?? "summary", [
251
+ "off",
252
+ "summary",
253
+ "full",
254
+ ]),
255
+ },
256
+ {
257
+ currentValue: String(settings.gitContextMaxChars ?? 20_000),
258
+ description: "Maximum repository context characters included.",
259
+ id: "gitContextMaxChars",
260
+ label: "Repository context chars",
261
+ values: numericValues(
262
+ settings.gitContextMaxChars ?? 20_000,
263
+ [0, 5000, 10_000, 20_000, 50_000, 100_000]
264
+ ),
265
+ },
266
+ {
267
+ currentValue: Object.keys(settings.toolPolicies ?? {}).length
268
+ ? "Configured"
269
+ : "All tools: full",
270
+ description:
271
+ "Choose which tools are shared in full, summarized, or excluded.",
272
+ id: "toolPolicies",
273
+ label: "Tool disclosure policies",
274
+ submenu: (_currentValue, done) =>
275
+ new TextSettingSubmenu({
276
+ description:
277
+ 'Enter a JSON object with "full", "summary", or "exclude" values.',
278
+ initial: JSON.stringify(settings.toolPolicies ?? {}),
279
+ onCancel: done,
280
+ onSubmit: (value) => {
281
+ let parsed: unknown;
282
+ try {
283
+ parsed = JSON.parse(value || "{}");
284
+ } catch {
285
+ return { error: "Enter a valid JSON object." };
286
+ }
287
+ if (!isValidAdvisorToolPolicies(parsed)) {
288
+ return {
289
+ error:
290
+ "Use non-empty tool names with full, summary, or exclude values.",
291
+ };
292
+ }
293
+ return { value: JSON.stringify(parsed) };
294
+ },
295
+ theme,
296
+ title: "Tool disclosure policies",
297
+ tui,
298
+ }),
299
+ },
300
+ toggle(
301
+ "trackedFileContent",
302
+ "Tracked file content",
303
+ "Allow tracked file contents to be sent with Advisor context.",
304
+ settings.trackedFileContent,
305
+ false
306
+ ),
307
+ toggle(
308
+ "untrackedContent",
309
+ "Untracked file content",
310
+ "Allow untracked file contents to be sent with Advisor context.",
311
+ settings.untrackedContent,
312
+ false
313
+ ),
314
+ toggle(
315
+ "outcomeLogging",
316
+ "Outcome logging (global)",
317
+ "Allow anonymized Advisor outcomes to be logged globally.",
318
+ settings.outcomeLogging,
319
+ false
320
+ )
321
+ );
322
+ return items;
323
+ };
@@ -0,0 +1,103 @@
1
+ import {
2
+ type Component,
3
+ type Focusable,
4
+ type Input,
5
+ Key,
6
+ matchesKey,
7
+ type SettingItem,
8
+ type SettingsList,
9
+ } from "@earendil-works/pi-tui";
10
+
11
+ interface SettingsListPrivateFields {
12
+ filteredItems: SettingItem[];
13
+ searchInput?: Input;
14
+ selectedIndex: number;
15
+ submenuComponent?: Component | null;
16
+ }
17
+
18
+ /**
19
+ * Contains the narrow compatibility access needed for pi-tui's SettingsList.
20
+ * No other UI module should depend on its private fields.
21
+ */
22
+ export class SettingsListAdapter {
23
+ private readonly list: SettingsList;
24
+
25
+ constructor(list: SettingsList) {
26
+ this.list = list;
27
+ }
28
+
29
+ invalidate(): void {
30
+ this.list.invalidate();
31
+ }
32
+
33
+ render(width: number): string[] {
34
+ return this.list.render(width);
35
+ }
36
+
37
+ handleInput(keyData: string): void {
38
+ this.list.handleInput(keyData);
39
+ }
40
+
41
+ /** Compatibility-only view for existing component integration seams. */
42
+ get submenuComponent(): Component | null | undefined {
43
+ return this.privateFields().submenuComponent;
44
+ }
45
+
46
+ setFocused(value: boolean): void {
47
+ const fields = this.privateFields();
48
+ const submenu = fields.submenuComponent as Focusable | undefined;
49
+ if (submenu) {
50
+ submenu.focused = value;
51
+ }
52
+ }
53
+
54
+ setSelectedId(items: SettingItem[], selectedId: string): void {
55
+ const selectedIndex = items.findIndex((item) => item.id === selectedId);
56
+ if (selectedIndex >= 0) {
57
+ this.privateFields().selectedIndex = selectedIndex;
58
+ }
59
+ }
60
+
61
+ changeWithArrow(
62
+ keyData: string,
63
+ onChange: (id: string, value: string) => void
64
+ ): boolean {
65
+ let direction = 0;
66
+ if (matchesKey(keyData, Key.left) || keyData === "\u001b[D") {
67
+ direction = -1;
68
+ } else if (matchesKey(keyData, Key.right) || keyData === "\u001b[C") {
69
+ direction = 1;
70
+ }
71
+ if (direction === 0) {
72
+ return false;
73
+ }
74
+
75
+ const fields = this.privateFields();
76
+ if (fields.submenuComponent || fields.searchInput?.getValue()) {
77
+ return false;
78
+ }
79
+ const item = fields.filteredItems[fields.selectedIndex];
80
+ if (!item?.values?.length) {
81
+ return false;
82
+ }
83
+ const currentIndex = item.values.indexOf(item.currentValue);
84
+ let nextIndex: number;
85
+ if (currentIndex === -1) {
86
+ nextIndex = direction > 0 ? 0 : item.values.length - 1;
87
+ } else {
88
+ nextIndex =
89
+ (currentIndex + direction + item.values.length) % item.values.length;
90
+ }
91
+ const nextValue = item.values[nextIndex];
92
+ if (nextValue === undefined) {
93
+ return false;
94
+ }
95
+ item.currentValue = nextValue;
96
+ onChange(item.id, nextValue);
97
+ return true;
98
+ }
99
+
100
+ private privateFields(): SettingsListPrivateFields {
101
+ return this.list as unknown as SettingsListPrivateFields;
102
+ }
103
+ }
@@ -0,0 +1,80 @@
1
+ import type { AdvisorSettings, ContextPreset, SettingValue } from "./types.js";
2
+
3
+ export const BOOLEAN_SETTING_IDS = new Set([
4
+ "scoutEnabled",
5
+ "showUsageDetails",
6
+ "showUsageFooter",
7
+ "planGate",
8
+ "failureGate",
9
+ "completionGate",
10
+ "collapseResponses",
11
+ "blockOnBlocked",
12
+ "autoLoopGate",
13
+ "sessionSummary",
14
+ "herdrIntegration",
15
+ "redactSecrets",
16
+ "trackedFileContent",
17
+ "untrackedContent",
18
+ "outcomeLogging",
19
+ ]);
20
+
21
+ export const mutateAdvisorSettings = (
22
+ settings: AdvisorSettings,
23
+ id: string,
24
+ value: string,
25
+ presets: ContextPreset[]
26
+ ): void => {
27
+ switch (id) {
28
+ case "context":
29
+ settings.contextMaxChars =
30
+ presets.find((preset) => preset.label === value)?.value ??
31
+ settings.contextMaxChars;
32
+ break;
33
+ case "simpleMode":
34
+ settings.simpleMode = value === "On";
35
+ break;
36
+ case "alwaysOn":
37
+ settings.alwaysOn = value === "On";
38
+ break;
39
+ case "effort":
40
+ settings.effort = value;
41
+ break;
42
+ case "customRule":
43
+ settings.customRule = value.trim() || undefined;
44
+ break;
45
+ case "toolPolicies":
46
+ settings.toolPolicies = JSON.parse(
47
+ value
48
+ ) as AdvisorSettings["toolPolicies"];
49
+ break;
50
+ case "loopThreshold":
51
+ settings.loopThreshold = Number(
52
+ value.replace("After ", "").replace(" repeats", "")
53
+ );
54
+ break;
55
+ case "maxCallsPerSession":
56
+ settings.maxCallsPerSession = value === "∞" ? undefined : Number(value);
57
+ break;
58
+ case "failureMode":
59
+ settings.failureMode = value as AdvisorSettings["failureMode"];
60
+ break;
61
+ case "gitContext":
62
+ settings.gitContext = value as AdvisorSettings["gitContext"];
63
+ break;
64
+ case "toolResultMaxLines":
65
+ settings.toolResultMaxLines = Number(value);
66
+ break;
67
+ case "toolResultMaxBytes":
68
+ settings.toolResultMaxBytes = Number(value);
69
+ break;
70
+ case "gitContextMaxChars":
71
+ 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;
79
+ }
80
+ };