pi-advisor-flow 0.5.1 → 0.5.3
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 +21 -0
- package/README.md +3 -2
- package/extensions/index.ts +7 -7
- package/package.json +2 -1
- package/src/commands/activation-preparation.ts +117 -0
- package/src/commands/activation.ts +135 -0
- package/src/commands/lifecycle.ts +83 -0
- package/src/commands/manual-command.ts +107 -0
- package/src/commands/manual-consultation.ts +144 -0
- package/src/commands/manual-progress.ts +93 -0
- package/src/commands/model-commands.ts +66 -0
- package/src/commands/model-options.ts +181 -0
- package/src/commands/model-picker.ts +110 -0
- package/src/commands/registration.ts +27 -0
- package/src/commands/renderers.ts +76 -0
- package/src/commands/runtime.ts +114 -0
- package/src/commands/settings-commands.ts +80 -0
- package/src/commands/settings-persistence.ts +81 -0
- package/src/commands/types.ts +78 -0
- package/src/commands.ts +9 -1259
- package/src/config/args.ts +37 -0
- package/src/config/defaults.ts +190 -0
- package/src/config/state.ts +199 -0
- package/src/config/storage.ts +224 -0
- package/src/config/types.ts +63 -0
- package/src/config/validation.ts +234 -0
- package/src/config.ts +104 -814
- package/src/conversation.ts +4 -2
- package/src/herdr.ts +1 -1
- package/src/model-stream.ts +15 -1
- package/src/scout-context.ts +2 -2
- package/src/scout.ts +1 -1
- package/src/tools/consultation.ts +337 -0
- package/src/tools/gate-policy.ts +132 -0
- package/src/tools/gate-protocol.ts +127 -0
- package/src/tools/loop-gate.ts +210 -0
- package/src/tools/prompts.ts +159 -0
- package/src/tools/register-ask-advisor.ts +247 -0
- package/src/tools/register-lifecycle.ts +119 -0
- package/src/tools/register-outcome.ts +75 -0
- package/src/tools/register-renderers.ts +88 -0
- package/src/tools/registration.ts +35 -0
- package/src/tools/render-advisor-result.ts +168 -0
- package/src/tools/render-common.ts +120 -0
- package/src/tools/scout-status.ts +206 -0
- package/src/tools/session.ts +3 -0
- package/src/tools/types.ts +119 -0
- package/src/tools.ts +42 -1865
- package/src/ui/manual-dialog-render.ts +150 -0
- package/src/ui/manual-dialog.ts +305 -0
- package/src/ui/model-selector.ts +166 -0
- package/src/ui/settings-formatting.ts +133 -0
- package/src/ui/settings-items.ts +323 -0
- package/src/ui/settings-list-adapter.ts +103 -0
- package/src/ui/settings-mutations.ts +80 -0
- package/src/ui/settings-selector.ts +161 -0
- package/src/ui/text-setting-submenu.ts +66 -0
- package/src/ui/types.ts +111 -0
- package/src/ui.ts +9 -1418
package/src/ui.ts
CHANGED
|
@@ -1,1418 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
fuzzyFilter,
|
|
11
|
-
Input,
|
|
12
|
-
Key,
|
|
13
|
-
type Keybindings,
|
|
14
|
-
type KeybindingsManager,
|
|
15
|
-
type KeyId,
|
|
16
|
-
matchesKey,
|
|
17
|
-
type SettingItem,
|
|
18
|
-
SettingsList,
|
|
19
|
-
type TUI,
|
|
20
|
-
truncateToWidth,
|
|
21
|
-
visibleWidth,
|
|
22
|
-
wrapTextWithAnsi,
|
|
23
|
-
} from "@earendil-works/pi-tui";
|
|
24
|
-
import { isValidAdvisorToolPolicies } from "./config.js";
|
|
25
|
-
import {
|
|
26
|
-
clampGitContextLevel,
|
|
27
|
-
GIT_CONTEXT_LEVELS,
|
|
28
|
-
type GitContextLevel,
|
|
29
|
-
} from "./git.js";
|
|
30
|
-
|
|
31
|
-
interface RenderRequester {
|
|
32
|
-
requestRender: () => void;
|
|
33
|
-
}
|
|
34
|
-
interface SearchableModelSelectorOptions {
|
|
35
|
-
allOptions: string[];
|
|
36
|
-
currentOption?: string;
|
|
37
|
-
keybindings: KeybindingsManager;
|
|
38
|
-
onCancel: () => void;
|
|
39
|
-
onSelect: (value: string) => void;
|
|
40
|
-
theme: Theme;
|
|
41
|
-
title: string;
|
|
42
|
-
tui: RenderRequester;
|
|
43
|
-
}
|
|
44
|
-
export class SearchableModelSelector implements Component, Focusable {
|
|
45
|
-
private readonly tui: RenderRequester;
|
|
46
|
-
private readonly searchInput: Input;
|
|
47
|
-
private readonly allOptions: string[];
|
|
48
|
-
private readonly currentOption: string | undefined;
|
|
49
|
-
private filteredOptions: string[];
|
|
50
|
-
private selectedIndex = 0;
|
|
51
|
-
private readonly title: string;
|
|
52
|
-
private readonly onSelect: (value: string) => void;
|
|
53
|
-
private readonly onCancel: () => void;
|
|
54
|
-
private readonly theme: Theme;
|
|
55
|
-
private readonly keybindings: KeybindingsManager;
|
|
56
|
-
private _focused = false;
|
|
57
|
-
|
|
58
|
-
get focused(): boolean {
|
|
59
|
-
return this._focused;
|
|
60
|
-
}
|
|
61
|
-
set focused(val: boolean) {
|
|
62
|
-
this._focused = val;
|
|
63
|
-
this.searchInput.focused = val;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
constructor(options: SearchableModelSelectorOptions) {
|
|
67
|
-
this.tui = options.tui;
|
|
68
|
-
this.title = options.title;
|
|
69
|
-
this.currentOption =
|
|
70
|
-
options.currentOption &&
|
|
71
|
-
options.allOptions.includes(options.currentOption)
|
|
72
|
-
? options.currentOption
|
|
73
|
-
: undefined;
|
|
74
|
-
this.allOptions = this.currentOption
|
|
75
|
-
? [
|
|
76
|
-
this.currentOption,
|
|
77
|
-
...options.allOptions.filter((item) => item !== this.currentOption),
|
|
78
|
-
]
|
|
79
|
-
: options.allOptions;
|
|
80
|
-
this.theme = options.theme;
|
|
81
|
-
this.keybindings = options.keybindings;
|
|
82
|
-
this.onSelect = options.onSelect;
|
|
83
|
-
this.onCancel = options.onCancel;
|
|
84
|
-
this.searchInput = new Input();
|
|
85
|
-
this.filteredOptions = this.allOptions;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
invalidate(): void {
|
|
89
|
-
this.searchInput.invalidate();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
render(width: number): string[] {
|
|
93
|
-
const lines: string[] = ["═".repeat(width)];
|
|
94
|
-
lines.push(` ${this.theme.fg("accent", this.theme.bold(this.title))}`);
|
|
95
|
-
const inputLines = this.searchInput.render(width - 12);
|
|
96
|
-
lines.push(
|
|
97
|
-
` ${this.theme.fg("accent", "Search: ")}${inputLines[0] || ""}`
|
|
98
|
-
);
|
|
99
|
-
lines.push("");
|
|
100
|
-
|
|
101
|
-
const query = this.searchInput.getValue().trim();
|
|
102
|
-
this.filteredOptions = query
|
|
103
|
-
? fuzzyFilter(this.allOptions, query, (item) => item)
|
|
104
|
-
: this.allOptions;
|
|
105
|
-
this.selectedIndex = Math.min(
|
|
106
|
-
this.selectedIndex,
|
|
107
|
-
Math.max(0, this.filteredOptions.length - 1)
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
const maxVisible = 10;
|
|
111
|
-
const total = this.filteredOptions.length;
|
|
112
|
-
if (total === 0) {
|
|
113
|
-
lines.push(` ${this.theme.fg("muted", "No matching models found.")}`);
|
|
114
|
-
} else {
|
|
115
|
-
const startIndex = Math.max(
|
|
116
|
-
0,
|
|
117
|
-
Math.min(
|
|
118
|
-
this.selectedIndex - Math.floor(maxVisible / 2),
|
|
119
|
-
total - maxVisible
|
|
120
|
-
)
|
|
121
|
-
);
|
|
122
|
-
const endIndex = Math.min(startIndex + maxVisible, total);
|
|
123
|
-
for (let i = startIndex; i < endIndex; i += 1) {
|
|
124
|
-
const item = this.filteredOptions[i];
|
|
125
|
-
const tick = item === this.currentOption ? "✓ " : " ";
|
|
126
|
-
if (i === this.selectedIndex) {
|
|
127
|
-
lines.push(
|
|
128
|
-
` ${this.theme.fg("accent", "→ ")}${this.theme.fg("accent", `${tick}${item}`)}`
|
|
129
|
-
);
|
|
130
|
-
} else {
|
|
131
|
-
lines.push(` ${this.theme.fg("text", `${tick}${item}`)}`);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
if (total > maxVisible) {
|
|
135
|
-
lines.push(
|
|
136
|
-
" " +
|
|
137
|
-
this.theme.fg("muted", ` (${this.selectedIndex + 1}/${total})`)
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
lines.push("");
|
|
142
|
-
lines.push(
|
|
143
|
-
` ${this.theme.fg("muted", "Type to search · ↑↓: navigate · Enter: select · Esc: cancel")}`
|
|
144
|
-
);
|
|
145
|
-
lines.push("═".repeat(width));
|
|
146
|
-
return lines;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
handleInput(keyData: string): void {
|
|
150
|
-
if (this.matchesAction(keyData, "tui.select.up", "\u001b[A")) {
|
|
151
|
-
this.moveSelection(-1);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
if (this.matchesAction(keyData, "tui.select.down", "\u001b[B")) {
|
|
155
|
-
this.moveSelection(1);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
if (
|
|
159
|
-
this.matchesAction(keyData, "tui.select.confirm", "\n") ||
|
|
160
|
-
keyData === "\r"
|
|
161
|
-
) {
|
|
162
|
-
if (this.filteredOptions.length > 0) {
|
|
163
|
-
this.onSelect(this.filteredOptions[this.selectedIndex]);
|
|
164
|
-
}
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (this.matchesAction(keyData, "tui.select.cancel", "\u001b")) {
|
|
168
|
-
this.onCancel();
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
this.searchInput.handleInput(keyData);
|
|
172
|
-
this.selectedIndex = 0;
|
|
173
|
-
this.tui.requestRender();
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
private matchesAction(
|
|
177
|
-
keyData: string,
|
|
178
|
-
action: keyof Keybindings,
|
|
179
|
-
fallback: string
|
|
180
|
-
) {
|
|
181
|
-
return this.keybindings.matches(keyData, action) || keyData === fallback;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
private moveSelection(direction: -1 | 1) {
|
|
185
|
-
if (this.filteredOptions.length > 0) {
|
|
186
|
-
const lastIndex = this.filteredOptions.length - 1;
|
|
187
|
-
const nextIndex = this.selectedIndex + direction;
|
|
188
|
-
if (nextIndex < 0) {
|
|
189
|
-
this.selectedIndex = lastIndex;
|
|
190
|
-
} else if (nextIndex > lastIndex) {
|
|
191
|
-
this.selectedIndex = 0;
|
|
192
|
-
} else {
|
|
193
|
-
this.selectedIndex = nextIndex;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
this.tui.requestRender();
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export interface ManualAdvisorRequest {
|
|
201
|
-
/** The repository disclosure level selected for this consultation. */
|
|
202
|
-
gitContext: GitContextLevel;
|
|
203
|
-
/** The unnormalized message entered in the dialog, when one was supplied. */
|
|
204
|
-
message?: string;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
interface ManualAdvisorDialogOptions {
|
|
208
|
-
gitContext: GitContextLevel;
|
|
209
|
-
initialMessage?: string;
|
|
210
|
-
keybindings: KeybindingsManager;
|
|
211
|
-
onCancel: () => void;
|
|
212
|
-
onSubmit: (request: ManualAdvisorRequest) => void;
|
|
213
|
-
theme: Theme;
|
|
214
|
-
tui: TUI;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
type ManualAdvisorFocus = "editor" | "git" | "actions";
|
|
218
|
-
|
|
219
|
-
const MANUAL_GIT_CONTEXT_COPY: Record<
|
|
220
|
-
GitContextLevel,
|
|
221
|
-
{ description: string; label: string }
|
|
222
|
-
> = {
|
|
223
|
-
full: { description: "summary plus patch", label: "Full" },
|
|
224
|
-
off: { description: "no Git data", label: "None" },
|
|
225
|
-
summary: { description: "changed paths and status", label: "Summary" },
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* The small, deliberately non-persistent form used by `/advisor-manual`.
|
|
230
|
-
*
|
|
231
|
-
* The dialog owns only local input state. Callers decide when to consume a
|
|
232
|
-
* budget or start a consultation after the submitted request is returned.
|
|
233
|
-
*/
|
|
234
|
-
export class ManualAdvisorDialog implements Component, Focusable {
|
|
235
|
-
private readonly options: ManualAdvisorDialogOptions;
|
|
236
|
-
private readonly editor: Editor;
|
|
237
|
-
private readonly gitLevels: GitContextLevel[];
|
|
238
|
-
private gitIndex: number;
|
|
239
|
-
private actionIndex = 0;
|
|
240
|
-
private focusTarget: ManualAdvisorFocus = "editor";
|
|
241
|
-
private readonly state: { completed: boolean; focused: boolean } = {
|
|
242
|
-
completed: false,
|
|
243
|
-
focused: false,
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
get focused(): boolean {
|
|
247
|
-
return this.state.focused;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
set focused(value: boolean) {
|
|
251
|
-
this.state.focused = value;
|
|
252
|
-
this.updateEditorFocus();
|
|
253
|
-
this.options.tui.requestRender();
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
constructor(options: ManualAdvisorDialogOptions) {
|
|
257
|
-
this.options = options;
|
|
258
|
-
// Show the configured default first, followed by progressively narrower
|
|
259
|
-
// choices. That makes Down do the useful thing from the initial selection
|
|
260
|
-
// while the configured level remains the hard ceiling.
|
|
261
|
-
this.gitLevels = GIT_CONTEXT_LEVELS.filter(
|
|
262
|
-
(level) => clampGitContextLevel(level, options.gitContext) === level
|
|
263
|
-
).reverse();
|
|
264
|
-
this.gitIndex = Math.max(0, this.gitLevels.indexOf(options.gitContext));
|
|
265
|
-
|
|
266
|
-
const editorTheme: EditorTheme = {
|
|
267
|
-
borderColor: (text) => options.theme.fg("border", text),
|
|
268
|
-
selectList: {
|
|
269
|
-
description: (text) => options.theme.fg("muted", text),
|
|
270
|
-
noMatch: (text) => options.theme.fg("warning", text),
|
|
271
|
-
scrollInfo: (text) => options.theme.fg("dim", text),
|
|
272
|
-
selectedPrefix: (text) => options.theme.fg("accent", text),
|
|
273
|
-
selectedText: (text) => options.theme.fg("accent", text),
|
|
274
|
-
},
|
|
275
|
-
};
|
|
276
|
-
this.editor = new Editor(options.tui, editorTheme);
|
|
277
|
-
this.editor.setText(options.initialMessage ?? "");
|
|
278
|
-
this.editor.onChange = () => options.tui.requestRender();
|
|
279
|
-
this.editor.onSubmit = (message) => this.submit(message);
|
|
280
|
-
this.updateEditorFocus();
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
invalidate(): void {
|
|
284
|
-
this.editor.invalidate();
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
dispose(): void {
|
|
288
|
-
this.state.completed = true;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
handleInput(keyData: string): void {
|
|
292
|
-
if (this.state.completed) {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
if (this.isCancel(keyData)) {
|
|
296
|
-
this.cancel();
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
if (this.isShiftTab(keyData)) {
|
|
300
|
-
this.changeFocus(-1);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
if (this.isTab(keyData)) {
|
|
304
|
-
this.changeFocus(1);
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
switch (this.focusTarget) {
|
|
309
|
-
case "editor":
|
|
310
|
-
// Editor owns Enter/Shift+Enter and all cursor/editing semantics. Its
|
|
311
|
-
// onSubmit callback above is the sole editor submission path. At the
|
|
312
|
-
// edge of the message, a directional key also moves to the adjacent
|
|
313
|
-
// form control so the Git selector is reachable without Tab.
|
|
314
|
-
this.handleEditorInput(keyData);
|
|
315
|
-
return;
|
|
316
|
-
case "git":
|
|
317
|
-
this.handleGitInput(keyData);
|
|
318
|
-
return;
|
|
319
|
-
case "actions":
|
|
320
|
-
this.handleActionInput(keyData);
|
|
321
|
-
return;
|
|
322
|
-
default:
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
render(width: number): string[] {
|
|
328
|
-
const renderWidth = Math.max(1, Math.floor(width));
|
|
329
|
-
const innerWidth = Math.max(0, renderWidth - 2);
|
|
330
|
-
const horizontalPadding = renderWidth >= 4 ? 1 : 0;
|
|
331
|
-
const contentWidth = Math.max(0, innerWidth - horizontalPadding * 2);
|
|
332
|
-
const lines: string[] = [];
|
|
333
|
-
|
|
334
|
-
const focusMarker = (target: ManualAdvisorFocus) =>
|
|
335
|
-
this.focusTarget === target
|
|
336
|
-
? this.options.theme.fg("accent", "▸ ")
|
|
337
|
-
: " ";
|
|
338
|
-
const addLine = (text: string) => {
|
|
339
|
-
if (renderWidth < 2) {
|
|
340
|
-
lines.push(truncateToWidth(text, renderWidth, ""));
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
lines.push(
|
|
344
|
-
`${this.options.theme.fg("border", "│")}${" ".repeat(
|
|
345
|
-
horizontalPadding
|
|
346
|
-
)}${truncateToWidth(text, contentWidth, "", true)}${" ".repeat(
|
|
347
|
-
horizontalPadding
|
|
348
|
-
)}${this.options.theme.fg("border", "│")}`
|
|
349
|
-
);
|
|
350
|
-
};
|
|
351
|
-
const addWrapped = (
|
|
352
|
-
text: string,
|
|
353
|
-
color: "accent" | "text" | "muted" | "dim" = "text"
|
|
354
|
-
) => {
|
|
355
|
-
const content = this.options.theme.fg(color, text);
|
|
356
|
-
const wrapped = wrapTextWithAnsi(content, Math.max(1, contentWidth - 2));
|
|
357
|
-
for (const line of wrapped.length > 0 ? wrapped : [""]) {
|
|
358
|
-
addLine(` ${line}`);
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
if (renderWidth >= 2) {
|
|
363
|
-
const title = truncateToWidth(
|
|
364
|
-
" Ask Advisor ",
|
|
365
|
-
Math.max(0, innerWidth),
|
|
366
|
-
"",
|
|
367
|
-
false
|
|
368
|
-
);
|
|
369
|
-
const titleWidth = visibleWidth(title);
|
|
370
|
-
const remaining = Math.max(0, innerWidth - titleWidth);
|
|
371
|
-
const left = Math.floor(remaining / 2);
|
|
372
|
-
const right = remaining - left;
|
|
373
|
-
lines.push(
|
|
374
|
-
this.options.theme.fg("border", `╭${"─".repeat(left)}`) +
|
|
375
|
-
this.options.theme.fg("accent", title) +
|
|
376
|
-
this.options.theme.fg("border", `${"─".repeat(right)}╮`)
|
|
377
|
-
);
|
|
378
|
-
addLine("");
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
addLine(
|
|
382
|
-
`${focusMarker("editor")}${this.options.theme.bold(
|
|
383
|
-
"Message for Advisor (optional)"
|
|
384
|
-
)}`
|
|
385
|
-
);
|
|
386
|
-
const editorWidth = Math.max(1, contentWidth);
|
|
387
|
-
for (const editorLine of this.editor.render(editorWidth)) {
|
|
388
|
-
addLine(editorLine);
|
|
389
|
-
}
|
|
390
|
-
addLine("");
|
|
391
|
-
addWrapped(
|
|
392
|
-
"Automatic context follows your settings: conversation history, tool disclosure, project preferences, redaction, and configured limits remain unchanged.",
|
|
393
|
-
"muted"
|
|
394
|
-
);
|
|
395
|
-
addWrapped(
|
|
396
|
-
"This dialog does not expose drafts or explicit file handoff controls.",
|
|
397
|
-
"dim"
|
|
398
|
-
);
|
|
399
|
-
addWrapped(
|
|
400
|
-
"Git None only withholds repository data; it does not remove configured conversation history.",
|
|
401
|
-
"dim"
|
|
402
|
-
);
|
|
403
|
-
addLine("");
|
|
404
|
-
const gitCeiling = MANUAL_GIT_CONTEXT_COPY[this.options.gitContext].label;
|
|
405
|
-
addLine(
|
|
406
|
-
`${focusMarker("git")}${this.options.theme.bold(
|
|
407
|
-
`Git repository context (max: ${gitCeiling}; ↑/↓ or Space to choose)`
|
|
408
|
-
)}`
|
|
409
|
-
);
|
|
410
|
-
for (let index = 0; index < this.gitLevels.length; index += 1) {
|
|
411
|
-
const level = this.gitLevels[index];
|
|
412
|
-
const copy = MANUAL_GIT_CONTEXT_COPY[level];
|
|
413
|
-
const selected = index === this.gitIndex;
|
|
414
|
-
const marker = selected ? "●" : "○";
|
|
415
|
-
const optionFocus = this.focusTarget === "git" && selected ? "▸ " : " ";
|
|
416
|
-
const color = selected ? "accent" : "text";
|
|
417
|
-
addWrapped(
|
|
418
|
-
`${optionFocus}${marker} ${copy.label} — ${copy.description}`,
|
|
419
|
-
color
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
addLine("");
|
|
423
|
-
|
|
424
|
-
let interactionHint =
|
|
425
|
-
"Enter submit · Shift+Enter newline · Tab/Shift+Tab focus · Esc cancel";
|
|
426
|
-
if (this.focusTarget === "git") {
|
|
427
|
-
interactionHint =
|
|
428
|
-
"↑/↓ or Space choose · Enter next · Tab/Shift+Tab focus · Esc cancel";
|
|
429
|
-
} else if (this.focusTarget === "actions") {
|
|
430
|
-
interactionHint =
|
|
431
|
-
"←/→ choose · Enter/Space activate · Tab/Shift+Tab focus · Esc cancel";
|
|
432
|
-
}
|
|
433
|
-
const submit = this.focusTarget === "actions" && this.actionIndex === 0;
|
|
434
|
-
const submitLabel = submit
|
|
435
|
-
? this.options.theme.fg("accent", "[Submit]")
|
|
436
|
-
: this.options.theme.fg("text", "[Submit]");
|
|
437
|
-
const cancel = this.focusTarget === "actions" && this.actionIndex === 1;
|
|
438
|
-
const cancelLabel = cancel
|
|
439
|
-
? this.options.theme.fg("accent", "[Cancel]")
|
|
440
|
-
: this.options.theme.fg("text", "[Cancel]");
|
|
441
|
-
addLine(
|
|
442
|
-
`${focusMarker("actions")} ${submitLabel} ${cancelLabel}`
|
|
443
|
-
);
|
|
444
|
-
addWrapped(interactionHint, "dim");
|
|
445
|
-
addLine("");
|
|
446
|
-
|
|
447
|
-
if (renderWidth >= 2) {
|
|
448
|
-
lines.push(
|
|
449
|
-
this.options.theme.fg(
|
|
450
|
-
"border",
|
|
451
|
-
`╰${"─".repeat(Math.max(0, renderWidth - 2))}╯`
|
|
452
|
-
)
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
return lines.map((line) => truncateToWidth(line, renderWidth, ""));
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
private handleEditorInput(keyData: string): void {
|
|
459
|
-
let direction: -1 | 1 | undefined;
|
|
460
|
-
if (this.matches(keyData, "tui.editor.cursorDown", Key.down)) {
|
|
461
|
-
direction = 1;
|
|
462
|
-
} else if (this.matches(keyData, "tui.editor.cursorUp", Key.up)) {
|
|
463
|
-
direction = -1;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
const beforeCursor =
|
|
467
|
-
direction === undefined ? undefined : this.editor.getCursor();
|
|
468
|
-
const beforeText =
|
|
469
|
-
direction === undefined ? undefined : this.editor.getText();
|
|
470
|
-
this.editor.handleInput(keyData);
|
|
471
|
-
|
|
472
|
-
if (direction !== undefined) {
|
|
473
|
-
const afterCursor = this.editor.getCursor();
|
|
474
|
-
if (
|
|
475
|
-
beforeCursor?.line === afterCursor.line &&
|
|
476
|
-
beforeCursor.col === afterCursor.col &&
|
|
477
|
-
beforeText === this.editor.getText()
|
|
478
|
-
) {
|
|
479
|
-
this.changeFocus(direction);
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
this.options.tui.requestRender();
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
private handleGitInput(keyData: string): void {
|
|
487
|
-
// Accept both the selector bindings and the editor arrow bindings. The
|
|
488
|
-
// latter keeps the form usable with user keymaps that customize selector
|
|
489
|
-
// actions, while the raw-key fallback keeps terminal arrow sequences
|
|
490
|
-
// working across Pi/pi-tui versions.
|
|
491
|
-
const up =
|
|
492
|
-
this.matches(keyData, "tui.select.up", Key.up) ||
|
|
493
|
-
this.matches(keyData, "tui.editor.cursorUp", Key.up);
|
|
494
|
-
const down =
|
|
495
|
-
this.matches(keyData, "tui.select.down", Key.down) ||
|
|
496
|
-
this.matches(keyData, "tui.editor.cursorDown", Key.down);
|
|
497
|
-
if (up) {
|
|
498
|
-
if (this.gitIndex === 0) {
|
|
499
|
-
this.changeFocus(-1);
|
|
500
|
-
} else {
|
|
501
|
-
this.moveGit(-1);
|
|
502
|
-
}
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
if (down) {
|
|
506
|
-
if (this.gitIndex === this.gitLevels.length - 1) {
|
|
507
|
-
this.changeFocus(1);
|
|
508
|
-
} else {
|
|
509
|
-
this.moveGit(1);
|
|
510
|
-
}
|
|
511
|
-
return;
|
|
512
|
-
}
|
|
513
|
-
if (matchesKey(keyData, Key.space)) {
|
|
514
|
-
this.moveGit(1);
|
|
515
|
-
return;
|
|
516
|
-
}
|
|
517
|
-
if (this.matches(keyData, "tui.input.submit", Key.enter)) {
|
|
518
|
-
this.changeFocus(1);
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
this.options.tui.requestRender();
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
private handleActionInput(keyData: string): void {
|
|
525
|
-
if (this.matches(keyData, "tui.editor.cursorUp", Key.up)) {
|
|
526
|
-
this.changeFocus(-1);
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
if (this.matches(keyData, "tui.editor.cursorDown", Key.down)) {
|
|
530
|
-
this.changeFocus(1);
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
if (matchesKey(keyData, Key.left) || matchesKey(keyData, Key.right)) {
|
|
534
|
-
this.actionIndex = this.actionIndex === 0 ? 1 : 0;
|
|
535
|
-
this.options.tui.requestRender();
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
if (
|
|
539
|
-
this.matches(keyData, "tui.input.submit", Key.enter) ||
|
|
540
|
-
matchesKey(keyData, Key.space)
|
|
541
|
-
) {
|
|
542
|
-
if (this.actionIndex === 0) {
|
|
543
|
-
this.submit();
|
|
544
|
-
} else {
|
|
545
|
-
this.cancel();
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
private changeFocus(direction: -1 | 1): void {
|
|
551
|
-
const targets: ManualAdvisorFocus[] = ["editor", "git", "actions"];
|
|
552
|
-
const current = targets.indexOf(this.focusTarget);
|
|
553
|
-
this.focusTarget =
|
|
554
|
-
targets[(current + direction + targets.length) % targets.length];
|
|
555
|
-
this.updateEditorFocus();
|
|
556
|
-
this.options.tui.requestRender();
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
private moveGit(direction: -1 | 1): void {
|
|
560
|
-
if (this.gitLevels.length > 0) {
|
|
561
|
-
this.gitIndex =
|
|
562
|
-
(this.gitIndex + direction + this.gitLevels.length) %
|
|
563
|
-
this.gitLevels.length;
|
|
564
|
-
}
|
|
565
|
-
this.options.tui.requestRender();
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
private updateEditorFocus(): void {
|
|
569
|
-
this.editor.focused = this.state.focused && this.focusTarget === "editor";
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
private isTab(keyData: string): boolean {
|
|
573
|
-
return (
|
|
574
|
-
this.matches(keyData, "tui.input.tab", Key.tab) &&
|
|
575
|
-
!matchesKey(keyData, Key.shift("tab"))
|
|
576
|
-
);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
private isShiftTab(keyData: string): boolean {
|
|
580
|
-
return matchesKey(keyData, Key.shift("tab"));
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
private isCancel(keyData: string): boolean {
|
|
584
|
-
return (
|
|
585
|
-
matchesKey(keyData, Key.escape) ||
|
|
586
|
-
this.options.keybindings.matches(keyData, "tui.select.cancel")
|
|
587
|
-
);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
private matches(
|
|
591
|
-
keyData: string,
|
|
592
|
-
action: keyof Keybindings,
|
|
593
|
-
fallback: KeyId
|
|
594
|
-
): boolean {
|
|
595
|
-
return (
|
|
596
|
-
this.options.keybindings.matches(keyData, action) ||
|
|
597
|
-
matchesKey(keyData, fallback)
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
private submit(message = this.editor.getText()): void {
|
|
602
|
-
if (this.state.completed) {
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
this.state.completed = true;
|
|
606
|
-
this.options.onSubmit({
|
|
607
|
-
gitContext: this.gitLevels[this.gitIndex] ?? "off",
|
|
608
|
-
...(message ? { message } : {}),
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
private cancel(): void {
|
|
613
|
-
if (this.state.completed) {
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
this.state.completed = true;
|
|
617
|
-
this.options.onCancel();
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
export interface ContextPreset {
|
|
622
|
-
description: string;
|
|
623
|
-
label: string;
|
|
624
|
-
value: number;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
export interface AdvisorSettings {
|
|
628
|
-
alwaysOn?: boolean;
|
|
629
|
-
autoLoopGate?: boolean;
|
|
630
|
-
blockOnBlocked?: boolean;
|
|
631
|
-
collapseResponses: boolean;
|
|
632
|
-
completionGate: boolean;
|
|
633
|
-
contextMaxChars: number;
|
|
634
|
-
customRule?: string;
|
|
635
|
-
effort?: string;
|
|
636
|
-
failureGate: boolean;
|
|
637
|
-
failureMode?: "block-session" | "block-tool" | "warn-and-continue";
|
|
638
|
-
gitContext?: "off" | "summary" | "full";
|
|
639
|
-
gitContextMaxChars?: number;
|
|
640
|
-
herdrIntegration?: boolean;
|
|
641
|
-
loopThreshold?: number;
|
|
642
|
-
maxCallsPerSession?: number;
|
|
643
|
-
outcomeLogging?: boolean;
|
|
644
|
-
planGate: boolean;
|
|
645
|
-
redactSecrets?: boolean;
|
|
646
|
-
scoutEnabled?: boolean;
|
|
647
|
-
sessionSummary?: boolean;
|
|
648
|
-
showUsageDetails?: boolean;
|
|
649
|
-
showUsageFooter?: boolean;
|
|
650
|
-
simpleMode?: boolean;
|
|
651
|
-
toolPolicies?: Record<string, "full" | "summary" | "exclude">;
|
|
652
|
-
toolResultMaxBytes?: number;
|
|
653
|
-
toolResultMaxLines?: number;
|
|
654
|
-
trackedFileContent?: boolean;
|
|
655
|
-
untrackedContent?: boolean;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
type SettingValue = string | undefined;
|
|
659
|
-
|
|
660
|
-
interface AdvisorSettingsSelectorOptions {
|
|
661
|
-
effortLevels: string[];
|
|
662
|
-
initial: AdvisorSettings;
|
|
663
|
-
onCancel: () => void;
|
|
664
|
-
onChange?: (settings: AdvisorSettings) => void;
|
|
665
|
-
/** @deprecated Use onChange; retained for extensions embedding this component. */
|
|
666
|
-
onSave?: (settings: AdvisorSettings) => void;
|
|
667
|
-
presets: ContextPreset[];
|
|
668
|
-
theme: Theme;
|
|
669
|
-
tui: RenderRequester;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
interface TextSettingSubmenuOptions {
|
|
673
|
-
description: string;
|
|
674
|
-
initial: string;
|
|
675
|
-
onCancel: (value?: string) => void;
|
|
676
|
-
onSubmit: (value: string) => { error?: string; value?: string };
|
|
677
|
-
theme: Theme;
|
|
678
|
-
title: string;
|
|
679
|
-
tui: RenderRequester;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
/** Small inline editor used by SettingsList for the two free-form settings. */
|
|
683
|
-
class TextSettingSubmenu implements Component, Focusable {
|
|
684
|
-
private readonly input = new Input();
|
|
685
|
-
private readonly options: TextSettingSubmenuOptions;
|
|
686
|
-
private _focused = true;
|
|
687
|
-
private error: string | undefined;
|
|
688
|
-
|
|
689
|
-
get focused(): boolean {
|
|
690
|
-
return this._focused;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
set focused(value: boolean) {
|
|
694
|
-
this._focused = value;
|
|
695
|
-
this.input.focused = value;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
constructor(options: TextSettingSubmenuOptions) {
|
|
699
|
-
this.options = options;
|
|
700
|
-
this.input.setValue(options.initial);
|
|
701
|
-
this.input.focused = true;
|
|
702
|
-
this.input.onSubmit = (value) => {
|
|
703
|
-
const result = options.onSubmit(value);
|
|
704
|
-
if (result.error) {
|
|
705
|
-
this.error = result.error;
|
|
706
|
-
options.tui.requestRender();
|
|
707
|
-
return;
|
|
708
|
-
}
|
|
709
|
-
this.error = undefined;
|
|
710
|
-
options.onCancel(result.value);
|
|
711
|
-
};
|
|
712
|
-
this.input.onEscape = () => options.onCancel();
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
invalidate(): void {
|
|
716
|
-
this.input.invalidate();
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
render(width: number): string[] {
|
|
720
|
-
const { theme } = this.options;
|
|
721
|
-
const input = this.input.render(Math.max(10, width - 4))[0] || "";
|
|
722
|
-
const lines = [
|
|
723
|
-
theme.fg("accent", theme.bold(` ${this.options.title}`)),
|
|
724
|
-
"",
|
|
725
|
-
theme.fg("muted", ` ${this.options.description}`),
|
|
726
|
-
"",
|
|
727
|
-
` ${input}`,
|
|
728
|
-
];
|
|
729
|
-
if (this.error) {
|
|
730
|
-
lines.push(theme.fg("error", ` ${this.error}`));
|
|
731
|
-
}
|
|
732
|
-
lines.push("", theme.fg("dim", " Enter: apply · Esc: cancel"));
|
|
733
|
-
return lines.map((line) => truncateToWidth(line, width));
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
handleInput(keyData: string): void {
|
|
737
|
-
this.input.handleInput(keyData);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
const DEFAULT_EFFORT_LEVEL = "Default (Model Default)";
|
|
742
|
-
const TOGGLE_VALUES = ["On", "Off"];
|
|
743
|
-
const SIMPLE_MODE_GRADIENT_INTERVAL_MS = 100;
|
|
744
|
-
// Purple steps with a moving light highlight, retained from the original UI.
|
|
745
|
-
const SIMPLE_MODE_GRADIENT_COLORS = [
|
|
746
|
-
[125, 79, 205],
|
|
747
|
-
[143, 96, 218],
|
|
748
|
-
[160, 114, 230],
|
|
749
|
-
[178, 135, 238],
|
|
750
|
-
[195, 157, 245],
|
|
751
|
-
[168, 120, 230],
|
|
752
|
-
[143, 89, 215],
|
|
753
|
-
] as const;
|
|
754
|
-
const BOOLEAN_SETTING_IDS = new Set([
|
|
755
|
-
"scoutEnabled",
|
|
756
|
-
"showUsageDetails",
|
|
757
|
-
"showUsageFooter",
|
|
758
|
-
"planGate",
|
|
759
|
-
"failureGate",
|
|
760
|
-
"completionGate",
|
|
761
|
-
"collapseResponses",
|
|
762
|
-
"blockOnBlocked",
|
|
763
|
-
"autoLoopGate",
|
|
764
|
-
"sessionSummary",
|
|
765
|
-
"herdrIntegration",
|
|
766
|
-
"redactSecrets",
|
|
767
|
-
"trackedFileContent",
|
|
768
|
-
"untrackedContent",
|
|
769
|
-
"outcomeLogging",
|
|
770
|
-
]);
|
|
771
|
-
|
|
772
|
-
const withCurrentValue = (current: string, values: string[]) =>
|
|
773
|
-
values.includes(current) ? values : [current, ...values];
|
|
774
|
-
|
|
775
|
-
const numericValues = (current: number, values: number[]) => {
|
|
776
|
-
const all = values.includes(current) ? values : [...values, current];
|
|
777
|
-
return all.sort((a, b) => a - b).map(String);
|
|
778
|
-
};
|
|
779
|
-
|
|
780
|
-
const maxCallValues = (current: string) => {
|
|
781
|
-
const values = ["0", "1", "2", "3", "5", "10", "25", "50", "∞"];
|
|
782
|
-
if (values.includes(current)) {
|
|
783
|
-
return values;
|
|
784
|
-
}
|
|
785
|
-
const numeric = Number(current);
|
|
786
|
-
const insertionIndex = values.findIndex(
|
|
787
|
-
(value) => value !== "∞" && Number(value) > numeric
|
|
788
|
-
);
|
|
789
|
-
return insertionIndex === -1
|
|
790
|
-
? [...values.slice(0, -1), current, "∞"]
|
|
791
|
-
: [
|
|
792
|
-
...values.slice(0, insertionIndex),
|
|
793
|
-
current,
|
|
794
|
-
...values.slice(insertionIndex),
|
|
795
|
-
];
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
const settingValue = (value: boolean | undefined, defaultValue: boolean) =>
|
|
799
|
-
(value ?? defaultValue) ? "On" : "Off";
|
|
800
|
-
|
|
801
|
-
export class AdvisorSettingsSelector implements Component, Focusable {
|
|
802
|
-
private readonly options: AdvisorSettingsSelectorOptions;
|
|
803
|
-
private readonly settings: AdvisorSettings;
|
|
804
|
-
private readonly presets: ContextPreset[];
|
|
805
|
-
private settingsList: SettingsList;
|
|
806
|
-
private simpleModeGradientStartedAt: number | undefined;
|
|
807
|
-
private simpleModeGradientTimer: ReturnType<typeof setInterval> | undefined;
|
|
808
|
-
private _focused = false;
|
|
809
|
-
|
|
810
|
-
get focused(): boolean {
|
|
811
|
-
return this._focused;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
set focused(value: boolean) {
|
|
815
|
-
this._focused = value;
|
|
816
|
-
const submenu = this.settingsList as unknown as {
|
|
817
|
-
submenuComponent?: Focusable;
|
|
818
|
-
};
|
|
819
|
-
if (submenu.submenuComponent) {
|
|
820
|
-
submenu.submenuComponent.focused = value;
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
constructor(options: AdvisorSettingsSelectorOptions) {
|
|
825
|
-
this.options = options;
|
|
826
|
-
this.settings = { ...options.initial };
|
|
827
|
-
const configuredContext = this.settings.contextMaxChars;
|
|
828
|
-
this.presets = options.presets.some(
|
|
829
|
-
(preset) => preset.value === configuredContext
|
|
830
|
-
)
|
|
831
|
-
? [...options.presets]
|
|
832
|
-
: [
|
|
833
|
-
...options.presets,
|
|
834
|
-
{
|
|
835
|
-
description: "Current custom context limit",
|
|
836
|
-
label: String(configuredContext),
|
|
837
|
-
value: configuredContext,
|
|
838
|
-
},
|
|
839
|
-
].sort((a, b) => a.value - b.value);
|
|
840
|
-
if (this.settings.simpleMode) {
|
|
841
|
-
this.startSimpleModeGradient();
|
|
842
|
-
}
|
|
843
|
-
this.settingsList = this.createSettingsList();
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
invalidate(): void {
|
|
847
|
-
this.settingsList.invalidate();
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
dispose(): void {
|
|
851
|
-
this.stopSimpleModeGradient();
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
render(width: number): string[] {
|
|
855
|
-
const border = this.options.theme.fg(
|
|
856
|
-
"border",
|
|
857
|
-
"─".repeat(Math.max(1, width))
|
|
858
|
-
);
|
|
859
|
-
return [border, ...this.settingsList.render(width), border].map((line) =>
|
|
860
|
-
truncateToWidth(line, width)
|
|
861
|
-
);
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
handleInput(keyData: string): void {
|
|
865
|
-
if (!this.changeWithArrow(keyData)) {
|
|
866
|
-
this.settingsList.handleInput(keyData);
|
|
867
|
-
}
|
|
868
|
-
this.options.tui.requestRender();
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
private changeWithArrow(keyData: string): boolean {
|
|
872
|
-
let direction = 0;
|
|
873
|
-
if (matchesKey(keyData, Key.left) || keyData === "\u001b[D") {
|
|
874
|
-
direction = -1;
|
|
875
|
-
} else if (matchesKey(keyData, Key.right) || keyData === "\u001b[C") {
|
|
876
|
-
direction = 1;
|
|
877
|
-
}
|
|
878
|
-
if (direction === 0) {
|
|
879
|
-
return false;
|
|
880
|
-
}
|
|
881
|
-
// SettingsList has no public selection/value-adjustment API. Keep this
|
|
882
|
-
// compatibility shim isolated to the native component's current fields.
|
|
883
|
-
const list = this.settingsList as unknown as {
|
|
884
|
-
filteredItems: SettingItem[];
|
|
885
|
-
searchInput?: Input;
|
|
886
|
-
selectedIndex: number;
|
|
887
|
-
submenuComponent?: Component | null;
|
|
888
|
-
};
|
|
889
|
-
if (list.submenuComponent || list.searchInput?.getValue()) {
|
|
890
|
-
return false;
|
|
891
|
-
}
|
|
892
|
-
const item = list.filteredItems[list.selectedIndex];
|
|
893
|
-
if (!item?.values?.length) {
|
|
894
|
-
return false;
|
|
895
|
-
}
|
|
896
|
-
const currentIndex = item.values.indexOf(item.currentValue);
|
|
897
|
-
let nextIndex: number;
|
|
898
|
-
if (currentIndex === -1) {
|
|
899
|
-
nextIndex = direction > 0 ? 0 : item.values.length - 1;
|
|
900
|
-
} else {
|
|
901
|
-
nextIndex =
|
|
902
|
-
(currentIndex + direction + item.values.length) % item.values.length;
|
|
903
|
-
}
|
|
904
|
-
const nextValue = item.values[nextIndex];
|
|
905
|
-
if (nextValue === undefined) {
|
|
906
|
-
return false;
|
|
907
|
-
}
|
|
908
|
-
item.currentValue = nextValue;
|
|
909
|
-
this.change(item.id, nextValue);
|
|
910
|
-
return true;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
private createSettingsList(selectedId?: string): SettingsList {
|
|
914
|
-
const listTheme = getSettingsListTheme();
|
|
915
|
-
const defaultLabel = listTheme.label;
|
|
916
|
-
listTheme.label = (text, selected) => {
|
|
917
|
-
if (this.settings.simpleMode && text.startsWith("Simple mode")) {
|
|
918
|
-
return `${this.rainbowGradient("Simple mode")}${text.slice("Simple mode".length)}`;
|
|
919
|
-
}
|
|
920
|
-
return defaultLabel(text, selected);
|
|
921
|
-
};
|
|
922
|
-
const items = this.items();
|
|
923
|
-
const list = new SettingsList(
|
|
924
|
-
items,
|
|
925
|
-
10,
|
|
926
|
-
listTheme,
|
|
927
|
-
(id, value) => this.change(id, value),
|
|
928
|
-
this.options.onCancel,
|
|
929
|
-
{ enableSearch: true }
|
|
930
|
-
);
|
|
931
|
-
if (selectedId) {
|
|
932
|
-
const selectedIndex = items.findIndex((item) => item.id === selectedId);
|
|
933
|
-
if (selectedIndex >= 0) {
|
|
934
|
-
(list as unknown as { selectedIndex: number }).selectedIndex =
|
|
935
|
-
selectedIndex;
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
return list;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
private items(): SettingItem[] {
|
|
942
|
-
const items: SettingItem[] = [
|
|
943
|
-
{
|
|
944
|
-
currentValue: this.currentContextLabel(),
|
|
945
|
-
description: this.contextDescription(),
|
|
946
|
-
id: "context",
|
|
947
|
-
label: "Context window",
|
|
948
|
-
values: this.presets.map((preset) => preset.label),
|
|
949
|
-
},
|
|
950
|
-
{
|
|
951
|
-
currentValue: settingValue(this.settings.simpleMode, false),
|
|
952
|
-
description:
|
|
953
|
-
"Keep the Advisor available on demand without automatic gates or blocks.",
|
|
954
|
-
id: "simpleMode",
|
|
955
|
-
label: "Simple mode",
|
|
956
|
-
values: TOGGLE_VALUES,
|
|
957
|
-
},
|
|
958
|
-
{
|
|
959
|
-
currentValue: settingValue(this.settings.alwaysOn, false),
|
|
960
|
-
description:
|
|
961
|
-
"Run the Advisor flow automatically for supported agent turns.",
|
|
962
|
-
id: "alwaysOn",
|
|
963
|
-
label: "Always on",
|
|
964
|
-
values: TOGGLE_VALUES,
|
|
965
|
-
},
|
|
966
|
-
];
|
|
967
|
-
if (this.settings.simpleMode) {
|
|
968
|
-
return items;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
items.push(
|
|
972
|
-
{
|
|
973
|
-
currentValue: this.currentEffort(),
|
|
974
|
-
description: "Reasoning level used for Advisor calls.",
|
|
975
|
-
id: "effort",
|
|
976
|
-
label: "Advisor reasoning",
|
|
977
|
-
values: withCurrentValue(
|
|
978
|
-
this.currentEffort(),
|
|
979
|
-
this.options.effortLevels
|
|
980
|
-
),
|
|
981
|
-
},
|
|
982
|
-
this.toggle(
|
|
983
|
-
"scoutEnabled",
|
|
984
|
-
"Experimental Advisor Scout",
|
|
985
|
-
"Enable the experimental Scout before Advisor calls.",
|
|
986
|
-
this.settings.scoutEnabled,
|
|
987
|
-
false
|
|
988
|
-
),
|
|
989
|
-
this.toggle(
|
|
990
|
-
"showUsageDetails",
|
|
991
|
-
"Show usage and cost details",
|
|
992
|
-
"Show token usage and cost details in Advisor responses.",
|
|
993
|
-
this.settings.showUsageDetails,
|
|
994
|
-
true
|
|
995
|
-
),
|
|
996
|
-
this.toggle(
|
|
997
|
-
"showUsageFooter",
|
|
998
|
-
"Show usage in footer",
|
|
999
|
-
"Show the current Advisor usage summary in the footer.",
|
|
1000
|
-
this.settings.showUsageFooter,
|
|
1001
|
-
false
|
|
1002
|
-
),
|
|
1003
|
-
this.toggle(
|
|
1004
|
-
"planGate",
|
|
1005
|
-
"Plan gate",
|
|
1006
|
-
"Ask the Advisor to review implementation plans.",
|
|
1007
|
-
this.settings.planGate,
|
|
1008
|
-
true
|
|
1009
|
-
),
|
|
1010
|
-
this.toggle(
|
|
1011
|
-
"failureGate",
|
|
1012
|
-
"Failure gate",
|
|
1013
|
-
"Ask the Advisor to review repeated failures.",
|
|
1014
|
-
this.settings.failureGate,
|
|
1015
|
-
true
|
|
1016
|
-
),
|
|
1017
|
-
this.toggle(
|
|
1018
|
-
"completionGate",
|
|
1019
|
-
"Completion gate",
|
|
1020
|
-
"Ask the Advisor to review work before declaring success.",
|
|
1021
|
-
this.settings.completionGate,
|
|
1022
|
-
true
|
|
1023
|
-
),
|
|
1024
|
-
this.toggle(
|
|
1025
|
-
"collapseResponses",
|
|
1026
|
-
"Collapse long responses",
|
|
1027
|
-
"Collapse long Advisor responses in the transcript.",
|
|
1028
|
-
this.settings.collapseResponses,
|
|
1029
|
-
false
|
|
1030
|
-
),
|
|
1031
|
-
{
|
|
1032
|
-
currentValue: this.settings.customRule || "None",
|
|
1033
|
-
description: "Add a rule that triggers Advisor involvement.",
|
|
1034
|
-
id: "customRule",
|
|
1035
|
-
label: "Custom invocation",
|
|
1036
|
-
submenu: (_currentValue, done) =>
|
|
1037
|
-
new TextSettingSubmenu({
|
|
1038
|
-
description: "Enter a custom invocation rule.",
|
|
1039
|
-
initial: this.settings.customRule || "",
|
|
1040
|
-
onCancel: done,
|
|
1041
|
-
onSubmit: (value) => ({ value: value.trim() }),
|
|
1042
|
-
theme: this.options.theme,
|
|
1043
|
-
title: "Custom invocation",
|
|
1044
|
-
tui: this.options.tui,
|
|
1045
|
-
}),
|
|
1046
|
-
},
|
|
1047
|
-
this.toggle(
|
|
1048
|
-
"blockOnBlocked",
|
|
1049
|
-
"Block on critical advice",
|
|
1050
|
-
"Block the agent when the Advisor returns a critical decision.",
|
|
1051
|
-
this.settings.blockOnBlocked,
|
|
1052
|
-
true
|
|
1053
|
-
),
|
|
1054
|
-
this.toggle(
|
|
1055
|
-
"autoLoopGate",
|
|
1056
|
-
"Automatic loop gate",
|
|
1057
|
-
"Ask the Advisor to review repeated equivalent attempts.",
|
|
1058
|
-
this.settings.autoLoopGate,
|
|
1059
|
-
true
|
|
1060
|
-
),
|
|
1061
|
-
{
|
|
1062
|
-
currentValue: `After ${this.settings.loopThreshold ?? 3} repeats`,
|
|
1063
|
-
description: "Number of equivalent attempts before automatic review.",
|
|
1064
|
-
id: "loopThreshold",
|
|
1065
|
-
label: "Loop threshold",
|
|
1066
|
-
values: numericValues(
|
|
1067
|
-
this.settings.loopThreshold ?? 3,
|
|
1068
|
-
Array.from({ length: 99 }, (_, index) => index + 2)
|
|
1069
|
-
).map((value) => `After ${value} repeats`),
|
|
1070
|
-
},
|
|
1071
|
-
{
|
|
1072
|
-
currentValue:
|
|
1073
|
-
this.settings.maxCallsPerSession === undefined
|
|
1074
|
-
? "∞"
|
|
1075
|
-
: String(this.settings.maxCallsPerSession),
|
|
1076
|
-
description: "Limit automatic Advisor calls in one session.",
|
|
1077
|
-
id: "maxCallsPerSession",
|
|
1078
|
-
label: "Max Advisor calls/session",
|
|
1079
|
-
values: maxCallValues(
|
|
1080
|
-
this.settings.maxCallsPerSession === undefined
|
|
1081
|
-
? "∞"
|
|
1082
|
-
: String(this.settings.maxCallsPerSession)
|
|
1083
|
-
),
|
|
1084
|
-
},
|
|
1085
|
-
this.toggle(
|
|
1086
|
-
"sessionSummary",
|
|
1087
|
-
"Session Advisor Summary",
|
|
1088
|
-
"Show a local summary when the session ends.",
|
|
1089
|
-
this.settings.sessionSummary,
|
|
1090
|
-
false
|
|
1091
|
-
),
|
|
1092
|
-
{
|
|
1093
|
-
currentValue: this.settings.failureMode ?? "block-session",
|
|
1094
|
-
description: "Choose what happens when an Advisor gate fails.",
|
|
1095
|
-
id: "failureMode",
|
|
1096
|
-
label: "Gate failure mode",
|
|
1097
|
-
values: withCurrentValue(this.settings.failureMode ?? "block-session", [
|
|
1098
|
-
"block-session",
|
|
1099
|
-
"block-tool",
|
|
1100
|
-
"warn-and-continue",
|
|
1101
|
-
]),
|
|
1102
|
-
},
|
|
1103
|
-
this.toggle(
|
|
1104
|
-
"herdrIntegration",
|
|
1105
|
-
"Herdr integration",
|
|
1106
|
-
"Send Advisor activity to the optional Herdr integration.",
|
|
1107
|
-
this.settings.herdrIntegration,
|
|
1108
|
-
true
|
|
1109
|
-
),
|
|
1110
|
-
{
|
|
1111
|
-
currentValue: String(this.settings.toolResultMaxLines ?? 2000),
|
|
1112
|
-
description: "Maximum lines included from a tool result.",
|
|
1113
|
-
id: "toolResultMaxLines",
|
|
1114
|
-
label: "Tool result lines",
|
|
1115
|
-
values: numericValues(
|
|
1116
|
-
this.settings.toolResultMaxLines ?? 2000,
|
|
1117
|
-
[0, 500, 1000, 2000, 5000, 10_000]
|
|
1118
|
-
),
|
|
1119
|
-
},
|
|
1120
|
-
{
|
|
1121
|
-
currentValue: String(this.settings.toolResultMaxBytes ?? 50 * 1024),
|
|
1122
|
-
description: "Maximum bytes included from a tool result.",
|
|
1123
|
-
id: "toolResultMaxBytes",
|
|
1124
|
-
label: "Tool result bytes",
|
|
1125
|
-
values: numericValues(this.settings.toolResultMaxBytes ?? 50 * 1024, [
|
|
1126
|
-
0,
|
|
1127
|
-
10 * 1024,
|
|
1128
|
-
50 * 1024,
|
|
1129
|
-
100 * 1024,
|
|
1130
|
-
500 * 1024,
|
|
1131
|
-
]),
|
|
1132
|
-
},
|
|
1133
|
-
this.toggle(
|
|
1134
|
-
"redactSecrets",
|
|
1135
|
-
"Redact common secrets",
|
|
1136
|
-
"Redact common credential patterns before Advisor calls.",
|
|
1137
|
-
this.settings.redactSecrets,
|
|
1138
|
-
false
|
|
1139
|
-
),
|
|
1140
|
-
{
|
|
1141
|
-
currentValue: this.settings.gitContext ?? "summary",
|
|
1142
|
-
description: "How much repository context is shared with the Advisor.",
|
|
1143
|
-
id: "gitContext",
|
|
1144
|
-
label: "Repository context",
|
|
1145
|
-
values: withCurrentValue(this.settings.gitContext ?? "summary", [
|
|
1146
|
-
"off",
|
|
1147
|
-
"summary",
|
|
1148
|
-
"full",
|
|
1149
|
-
]),
|
|
1150
|
-
},
|
|
1151
|
-
{
|
|
1152
|
-
currentValue: String(this.settings.gitContextMaxChars ?? 20_000),
|
|
1153
|
-
description: "Maximum repository context characters included.",
|
|
1154
|
-
id: "gitContextMaxChars",
|
|
1155
|
-
label: "Repository context chars",
|
|
1156
|
-
values: numericValues(
|
|
1157
|
-
this.settings.gitContextMaxChars ?? 20_000,
|
|
1158
|
-
[0, 5000, 10_000, 20_000, 50_000, 100_000]
|
|
1159
|
-
),
|
|
1160
|
-
},
|
|
1161
|
-
{
|
|
1162
|
-
currentValue: Object.keys(this.settings.toolPolicies ?? {}).length
|
|
1163
|
-
? "Configured"
|
|
1164
|
-
: "All tools: full",
|
|
1165
|
-
description:
|
|
1166
|
-
"Choose which tools are shared in full, summarized, or excluded.",
|
|
1167
|
-
id: "toolPolicies",
|
|
1168
|
-
label: "Tool disclosure policies",
|
|
1169
|
-
submenu: (_currentValue, done) =>
|
|
1170
|
-
new TextSettingSubmenu({
|
|
1171
|
-
description:
|
|
1172
|
-
'Enter a JSON object with "full", "summary", or "exclude" values.',
|
|
1173
|
-
initial: JSON.stringify(this.settings.toolPolicies ?? {}),
|
|
1174
|
-
onCancel: done,
|
|
1175
|
-
onSubmit: (value) => {
|
|
1176
|
-
let parsed: unknown;
|
|
1177
|
-
try {
|
|
1178
|
-
parsed = JSON.parse(value || "{}");
|
|
1179
|
-
} catch {
|
|
1180
|
-
return { error: "Enter a valid JSON object." };
|
|
1181
|
-
}
|
|
1182
|
-
if (!isValidAdvisorToolPolicies(parsed)) {
|
|
1183
|
-
return {
|
|
1184
|
-
error:
|
|
1185
|
-
"Use non-empty tool names with full, summary, or exclude values.",
|
|
1186
|
-
};
|
|
1187
|
-
}
|
|
1188
|
-
return { value: JSON.stringify(parsed) };
|
|
1189
|
-
},
|
|
1190
|
-
theme: this.options.theme,
|
|
1191
|
-
title: "Tool disclosure policies",
|
|
1192
|
-
tui: this.options.tui,
|
|
1193
|
-
}),
|
|
1194
|
-
},
|
|
1195
|
-
this.toggle(
|
|
1196
|
-
"trackedFileContent",
|
|
1197
|
-
"Tracked file content",
|
|
1198
|
-
"Allow tracked file contents to be sent with Advisor context.",
|
|
1199
|
-
this.settings.trackedFileContent,
|
|
1200
|
-
false
|
|
1201
|
-
),
|
|
1202
|
-
this.toggle(
|
|
1203
|
-
"untrackedContent",
|
|
1204
|
-
"Untracked file content",
|
|
1205
|
-
"Allow untracked file contents to be sent with Advisor context.",
|
|
1206
|
-
this.settings.untrackedContent,
|
|
1207
|
-
false
|
|
1208
|
-
),
|
|
1209
|
-
this.toggle(
|
|
1210
|
-
"outcomeLogging",
|
|
1211
|
-
"Outcome logging (global)",
|
|
1212
|
-
"Allow anonymized Advisor outcomes to be logged globally.",
|
|
1213
|
-
this.settings.outcomeLogging,
|
|
1214
|
-
false
|
|
1215
|
-
)
|
|
1216
|
-
);
|
|
1217
|
-
return items;
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
private toggle(
|
|
1221
|
-
id: string,
|
|
1222
|
-
label: string,
|
|
1223
|
-
description: string,
|
|
1224
|
-
value: boolean | undefined,
|
|
1225
|
-
defaultValue: boolean
|
|
1226
|
-
): SettingItem {
|
|
1227
|
-
return {
|
|
1228
|
-
currentValue: settingValue(value, defaultValue),
|
|
1229
|
-
description,
|
|
1230
|
-
id,
|
|
1231
|
-
label,
|
|
1232
|
-
values: TOGGLE_VALUES,
|
|
1233
|
-
};
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
private currentContextLabel(): string {
|
|
1237
|
-
return (
|
|
1238
|
-
this.presets.find(
|
|
1239
|
-
(preset) => preset.value === this.settings.contextMaxChars
|
|
1240
|
-
)?.label ?? String(this.settings.contextMaxChars)
|
|
1241
|
-
);
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
private contextDescription(): string {
|
|
1245
|
-
const exactIndex = this.presets.findIndex(
|
|
1246
|
-
(preset) => preset.value === this.settings.contextMaxChars
|
|
1247
|
-
);
|
|
1248
|
-
const selectedIndex =
|
|
1249
|
-
exactIndex >= 0
|
|
1250
|
-
? exactIndex
|
|
1251
|
-
: this.presets.reduce(
|
|
1252
|
-
(closestIndex, preset, index) =>
|
|
1253
|
-
Math.abs(preset.value - this.settings.contextMaxChars) <
|
|
1254
|
-
Math.abs(
|
|
1255
|
-
this.presets[closestIndex].value - this.settings.contextMaxChars
|
|
1256
|
-
)
|
|
1257
|
-
? index
|
|
1258
|
-
: closestIndex,
|
|
1259
|
-
0
|
|
1260
|
-
);
|
|
1261
|
-
const selectedPreset = this.presets[selectedIndex];
|
|
1262
|
-
const isFullContext =
|
|
1263
|
-
selectedPreset?.value === Number.MAX_SAFE_INTEGER ||
|
|
1264
|
-
selectedPreset?.label.toUpperCase() === "FULL" ||
|
|
1265
|
-
selectedPreset?.label.toUpperCase() === "ALL";
|
|
1266
|
-
const progress = isFullContext
|
|
1267
|
-
? 1
|
|
1268
|
-
: selectedIndex / Math.max(1, this.presets.length - 1);
|
|
1269
|
-
const meterWidth = 20;
|
|
1270
|
-
const marker = Math.round(progress * meterWidth);
|
|
1271
|
-
const meter = Array.from({ length: meterWidth + 1 }, (_, index) => {
|
|
1272
|
-
if (index === marker) {
|
|
1273
|
-
return "●";
|
|
1274
|
-
}
|
|
1275
|
-
return index < marker ? "━" : "─";
|
|
1276
|
-
}).join("");
|
|
1277
|
-
const label = this.currentContextLabel();
|
|
1278
|
-
const labelWidth = visibleWidth(label);
|
|
1279
|
-
const meterPrefix = "none ";
|
|
1280
|
-
const markerColumn = meterPrefix.length + marker;
|
|
1281
|
-
const labelStart = Math.max(
|
|
1282
|
-
0,
|
|
1283
|
-
Math.min(
|
|
1284
|
-
meterPrefix.length + meter.length + 2 - labelWidth,
|
|
1285
|
-
markerColumn - Math.floor((labelWidth - 1) / 2)
|
|
1286
|
-
)
|
|
1287
|
-
);
|
|
1288
|
-
const markerLabel = `${" ".repeat(labelStart)}${label}`;
|
|
1289
|
-
const description =
|
|
1290
|
-
exactIndex >= 0 ? selectedPreset?.description : "Custom context limit.";
|
|
1291
|
-
return `${description ?? "Custom context limit."}\n${meterPrefix}${meter} full\n${markerLabel}`;
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
private currentEffort(): string {
|
|
1295
|
-
return this.settings.effort || DEFAULT_EFFORT_LEVEL;
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
private rainbowGradient(text: string): string {
|
|
1299
|
-
const frame = Math.floor(
|
|
1300
|
-
(Date.now() - (this.simpleModeGradientStartedAt ?? 0)) /
|
|
1301
|
-
SIMPLE_MODE_GRADIENT_INTERVAL_MS
|
|
1302
|
-
);
|
|
1303
|
-
const shinePosition = frame % (text.length * 2);
|
|
1304
|
-
return [...text]
|
|
1305
|
-
.map((character, index) => {
|
|
1306
|
-
const [baseRed, baseGreen, baseBlue] =
|
|
1307
|
-
SIMPLE_MODE_GRADIENT_COLORS[
|
|
1308
|
-
index % SIMPLE_MODE_GRADIENT_COLORS.length
|
|
1309
|
-
];
|
|
1310
|
-
const distance = Math.abs(index - shinePosition);
|
|
1311
|
-
let brightness = 0;
|
|
1312
|
-
if (distance === 0) {
|
|
1313
|
-
brightness = 0.7;
|
|
1314
|
-
} else if (distance === 1) {
|
|
1315
|
-
brightness = 0.35;
|
|
1316
|
-
}
|
|
1317
|
-
const red = Math.round(baseRed + (255 - baseRed) * brightness);
|
|
1318
|
-
const green = Math.round(baseGreen + (255 - baseGreen) * brightness);
|
|
1319
|
-
const blue = Math.round(baseBlue + (255 - baseBlue) * brightness);
|
|
1320
|
-
return `\x1b[38;2;${red};${green};${blue}m${character}`;
|
|
1321
|
-
})
|
|
1322
|
-
.join("")
|
|
1323
|
-
.concat("\x1b[0m");
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
private startSimpleModeGradient(): void {
|
|
1327
|
-
this.stopSimpleModeGradient();
|
|
1328
|
-
this.simpleModeGradientStartedAt = Date.now();
|
|
1329
|
-
this.simpleModeGradientTimer = setInterval(() => {
|
|
1330
|
-
this.options.tui.requestRender();
|
|
1331
|
-
}, SIMPLE_MODE_GRADIENT_INTERVAL_MS);
|
|
1332
|
-
this.simpleModeGradientTimer.unref?.();
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
private stopSimpleModeGradient(): void {
|
|
1336
|
-
if (this.simpleModeGradientTimer) {
|
|
1337
|
-
clearInterval(this.simpleModeGradientTimer);
|
|
1338
|
-
this.simpleModeGradientTimer = undefined;
|
|
1339
|
-
}
|
|
1340
|
-
this.simpleModeGradientStartedAt = undefined;
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
private change(id: string, value: string): void {
|
|
1344
|
-
switch (id) {
|
|
1345
|
-
case "context":
|
|
1346
|
-
this.settings.contextMaxChars =
|
|
1347
|
-
this.presets.find((preset) => preset.label === value)?.value ??
|
|
1348
|
-
this.settings.contextMaxChars;
|
|
1349
|
-
break;
|
|
1350
|
-
case "simpleMode":
|
|
1351
|
-
this.settings.simpleMode = value === "On";
|
|
1352
|
-
if (this.settings.simpleMode) {
|
|
1353
|
-
this.startSimpleModeGradient();
|
|
1354
|
-
} else {
|
|
1355
|
-
this.stopSimpleModeGradient();
|
|
1356
|
-
}
|
|
1357
|
-
break;
|
|
1358
|
-
case "alwaysOn":
|
|
1359
|
-
this.settings.alwaysOn = value === "On";
|
|
1360
|
-
break;
|
|
1361
|
-
case "effort":
|
|
1362
|
-
this.settings.effort = value;
|
|
1363
|
-
break;
|
|
1364
|
-
case "customRule":
|
|
1365
|
-
this.settings.customRule = value.trim() || undefined;
|
|
1366
|
-
break;
|
|
1367
|
-
case "toolPolicies":
|
|
1368
|
-
this.settings.toolPolicies = JSON.parse(
|
|
1369
|
-
value
|
|
1370
|
-
) as AdvisorSettings["toolPolicies"];
|
|
1371
|
-
break;
|
|
1372
|
-
case "loopThreshold":
|
|
1373
|
-
this.settings.loopThreshold = Number(
|
|
1374
|
-
value.replace("After ", "").replace(" repeats", "")
|
|
1375
|
-
);
|
|
1376
|
-
break;
|
|
1377
|
-
case "maxCallsPerSession":
|
|
1378
|
-
this.settings.maxCallsPerSession =
|
|
1379
|
-
value === "∞" ? undefined : Number(value);
|
|
1380
|
-
break;
|
|
1381
|
-
case "failureMode":
|
|
1382
|
-
this.settings.failureMode = value as AdvisorSettings["failureMode"];
|
|
1383
|
-
break;
|
|
1384
|
-
case "gitContext":
|
|
1385
|
-
this.settings.gitContext = value as AdvisorSettings["gitContext"];
|
|
1386
|
-
break;
|
|
1387
|
-
case "toolResultMaxLines":
|
|
1388
|
-
this.settings.toolResultMaxLines = Number(value);
|
|
1389
|
-
break;
|
|
1390
|
-
case "toolResultMaxBytes":
|
|
1391
|
-
this.settings.toolResultMaxBytes = Number(value);
|
|
1392
|
-
break;
|
|
1393
|
-
case "gitContextMaxChars":
|
|
1394
|
-
this.settings.gitContextMaxChars = Number(value);
|
|
1395
|
-
break;
|
|
1396
|
-
default:
|
|
1397
|
-
if (BOOLEAN_SETTING_IDS.has(id)) {
|
|
1398
|
-
(this.settings as unknown as Record<string, SettingValue | boolean>)[
|
|
1399
|
-
id
|
|
1400
|
-
] = value === "On";
|
|
1401
|
-
}
|
|
1402
|
-
break;
|
|
1403
|
-
}
|
|
1404
|
-
(this.options.onChange ?? this.options.onSave)?.({
|
|
1405
|
-
...this.settings,
|
|
1406
|
-
showUsageDetails: this.settings.showUsageDetails ?? true,
|
|
1407
|
-
toolPolicies: { ...(this.settings.toolPolicies ?? {}) },
|
|
1408
|
-
});
|
|
1409
|
-
if (
|
|
1410
|
-
id === "context" ||
|
|
1411
|
-
id === "simpleMode" ||
|
|
1412
|
-
id === "customRule" ||
|
|
1413
|
-
id === "toolPolicies"
|
|
1414
|
-
) {
|
|
1415
|
-
this.settingsList = this.createSettingsList(id);
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1
|
+
// biome-ignore lint/performance/noBarrelFile: this facade intentionally preserves the public UI module contract.
|
|
2
|
+
export { ManualAdvisorDialog } from "./ui/manual-dialog.js";
|
|
3
|
+
export { SearchableModelSelector } from "./ui/model-selector.js";
|
|
4
|
+
export { AdvisorSettingsSelector } from "./ui/settings-selector.js";
|
|
5
|
+
export type {
|
|
6
|
+
AdvisorSettings,
|
|
7
|
+
ContextPreset,
|
|
8
|
+
ManualAdvisorRequest,
|
|
9
|
+
} from "./ui/types.js";
|