pi-advisor-flow 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +8 -5
- package/package.json +11 -11
- package/src/commands.ts +327 -113
- package/src/config.ts +22 -0
- package/src/model-stream.ts +123 -0
- package/src/session-state.ts +26 -2
- package/src/tools.ts +117 -42
- package/src/ui.ts +1060 -600
package/src/ui.ts
CHANGED
|
@@ -1,16 +1,32 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
getSettingsListTheme,
|
|
3
|
+
type Theme,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
5
|
import {
|
|
3
6
|
type Component,
|
|
7
|
+
Editor,
|
|
8
|
+
type EditorTheme,
|
|
4
9
|
type Focusable,
|
|
5
10
|
fuzzyFilter,
|
|
6
11
|
Input,
|
|
7
12
|
Key,
|
|
8
13
|
type Keybindings,
|
|
9
14
|
type KeybindingsManager,
|
|
15
|
+
type KeyId,
|
|
10
16
|
matchesKey,
|
|
17
|
+
type SettingItem,
|
|
18
|
+
SettingsList,
|
|
19
|
+
type TUI,
|
|
11
20
|
truncateToWidth,
|
|
21
|
+
visibleWidth,
|
|
22
|
+
wrapTextWithAnsi,
|
|
12
23
|
} from "@earendil-works/pi-tui";
|
|
13
24
|
import { isValidAdvisorToolPolicies } from "./config.js";
|
|
25
|
+
import {
|
|
26
|
+
clampGitContextLevel,
|
|
27
|
+
GIT_CONTEXT_LEVELS,
|
|
28
|
+
type GitContextLevel,
|
|
29
|
+
} from "./git.js";
|
|
14
30
|
|
|
15
31
|
interface RenderRequester {
|
|
16
32
|
requestRender: () => void;
|
|
@@ -25,28 +41,6 @@ interface SearchableModelSelectorOptions {
|
|
|
25
41
|
title: string;
|
|
26
42
|
tui: RenderRequester;
|
|
27
43
|
}
|
|
28
|
-
const stepNumericPreset = (
|
|
29
|
-
current: number,
|
|
30
|
-
presets: number[],
|
|
31
|
-
direction: number
|
|
32
|
-
) => {
|
|
33
|
-
const values = presets.includes(current)
|
|
34
|
-
? presets
|
|
35
|
-
: [...presets, current].sort((a, b) => a - b);
|
|
36
|
-
const index = values.indexOf(current);
|
|
37
|
-
return values[Math.max(0, Math.min(values.length - 1, index + direction))];
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
interface AdvisorSettingsSelectorOptions {
|
|
41
|
-
effortLevels: string[];
|
|
42
|
-
initial: AdvisorSettings;
|
|
43
|
-
onCancel: () => void;
|
|
44
|
-
onSave: (settings: AdvisorSettings) => void;
|
|
45
|
-
presets: ContextPreset[];
|
|
46
|
-
theme: Theme;
|
|
47
|
-
tui: RenderRequester;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
44
|
export class SearchableModelSelector implements Component, Focusable {
|
|
51
45
|
private readonly tui: RenderRequester;
|
|
52
46
|
private readonly searchInput: Input;
|
|
@@ -203,6 +197,427 @@ export class SearchableModelSelector implements Component, Focusable {
|
|
|
203
197
|
}
|
|
204
198
|
}
|
|
205
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
|
+
|
|
206
621
|
export interface ContextPreset {
|
|
207
622
|
description: string;
|
|
208
623
|
label: string;
|
|
@@ -230,6 +645,8 @@ export interface AdvisorSettings {
|
|
|
230
645
|
redactSecrets?: boolean;
|
|
231
646
|
scoutEnabled?: boolean;
|
|
232
647
|
sessionSummary?: boolean;
|
|
648
|
+
showUsageDetails?: boolean;
|
|
649
|
+
showUsageFooter?: boolean;
|
|
233
650
|
simpleMode?: boolean;
|
|
234
651
|
toolPolicies?: Record<string, "full" | "summary" | "exclude">;
|
|
235
652
|
toolResultMaxBytes?: number;
|
|
@@ -238,231 +655,644 @@ export interface AdvisorSettings {
|
|
|
238
655
|
untrackedContent?: boolean;
|
|
239
656
|
}
|
|
240
657
|
|
|
241
|
-
type
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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([
|
|
273
755
|
"scoutEnabled",
|
|
756
|
+
"showUsageDetails",
|
|
757
|
+
"showUsageFooter",
|
|
274
758
|
"planGate",
|
|
275
759
|
"failureGate",
|
|
276
760
|
"completionGate",
|
|
277
761
|
"collapseResponses",
|
|
278
|
-
"customRule",
|
|
279
762
|
"blockOnBlocked",
|
|
280
763
|
"autoLoopGate",
|
|
281
|
-
"loopThreshold",
|
|
282
|
-
"maxCallsPerSession",
|
|
283
764
|
"sessionSummary",
|
|
284
|
-
"failureMode",
|
|
285
765
|
"herdrIntegration",
|
|
286
|
-
"toolResultMaxLines",
|
|
287
|
-
"toolResultMaxBytes",
|
|
288
766
|
"redactSecrets",
|
|
289
|
-
"gitContext",
|
|
290
|
-
"gitContextMaxChars",
|
|
291
|
-
"toolPolicies",
|
|
292
767
|
"trackedFileContent",
|
|
293
768
|
"untrackedContent",
|
|
294
769
|
"outcomeLogging",
|
|
295
|
-
];
|
|
770
|
+
]);
|
|
296
771
|
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
[
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
[
|
|
307
|
-
|
|
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";
|
|
308
800
|
|
|
309
801
|
export class AdvisorSettingsSelector implements Component, Focusable {
|
|
310
|
-
private
|
|
311
|
-
private contextIndex: number;
|
|
312
|
-
private effortIndex: number;
|
|
802
|
+
private readonly options: AdvisorSettingsSelectorOptions;
|
|
313
803
|
private readonly settings: AdvisorSettings;
|
|
314
|
-
private readonly
|
|
315
|
-
private
|
|
316
|
-
private editingCustom: boolean;
|
|
317
|
-
private editingPolicies: boolean;
|
|
318
|
-
private policiesError: string | undefined;
|
|
804
|
+
private readonly presets: ContextPreset[];
|
|
805
|
+
private settingsList: SettingsList;
|
|
319
806
|
private simpleModeGradientStartedAt: number | undefined;
|
|
320
807
|
private simpleModeGradientTimer: ReturnType<typeof setInterval> | undefined;
|
|
321
808
|
private _focused = false;
|
|
322
|
-
private readonly options: AdvisorSettingsSelectorOptions;
|
|
323
809
|
|
|
324
810
|
get focused(): boolean {
|
|
325
811
|
return this._focused;
|
|
326
812
|
}
|
|
813
|
+
|
|
327
814
|
set focused(value: boolean) {
|
|
328
815
|
this._focused = value;
|
|
329
|
-
this.
|
|
330
|
-
|
|
816
|
+
const submenu = this.settingsList as unknown as {
|
|
817
|
+
submenuComponent?: Focusable;
|
|
818
|
+
};
|
|
819
|
+
if (submenu.submenuComponent) {
|
|
820
|
+
submenu.submenuComponent.focused = value;
|
|
821
|
+
}
|
|
331
822
|
}
|
|
332
823
|
|
|
333
824
|
constructor(options: AdvisorSettingsSelectorOptions) {
|
|
334
|
-
this.
|
|
335
|
-
this.editingCustom = false;
|
|
336
|
-
this.editingPolicies = false;
|
|
337
|
-
this.policiesError = undefined;
|
|
338
|
-
// Retain every advanced value even when Simple mode hides its controls.
|
|
825
|
+
this.options = options;
|
|
339
826
|
this.settings = { ...options.initial };
|
|
340
827
|
const configuredContext = this.settings.contextMaxChars;
|
|
341
|
-
|
|
828
|
+
this.presets = options.presets.some(
|
|
342
829
|
(preset) => preset.value === configuredContext
|
|
343
830
|
)
|
|
344
831
|
? [...options.presets]
|
|
345
832
|
: [
|
|
346
833
|
...options.presets,
|
|
347
834
|
{
|
|
348
|
-
description: "
|
|
835
|
+
description: "Current custom context limit",
|
|
349
836
|
label: String(configuredContext),
|
|
350
837
|
value: configuredContext,
|
|
351
838
|
},
|
|
352
839
|
].sort((a, b) => a.value - b.value);
|
|
353
|
-
this.options = { ...options, presets };
|
|
354
|
-
this.contextIndex = presets.findIndex(
|
|
355
|
-
(preset) => preset.value === configuredContext
|
|
356
|
-
);
|
|
357
|
-
this.effortIndex = Math.max(
|
|
358
|
-
0,
|
|
359
|
-
options.effortLevels.indexOf(
|
|
360
|
-
this.settings.effort || "Default (Model Default)"
|
|
361
|
-
)
|
|
362
|
-
);
|
|
363
840
|
if (this.settings.simpleMode) {
|
|
364
841
|
this.startSimpleModeGradient();
|
|
365
842
|
}
|
|
366
|
-
this.
|
|
367
|
-
this.settings.customRule = value.trim() || undefined;
|
|
368
|
-
this.editingCustom = false;
|
|
369
|
-
this.customInput.focused = false;
|
|
370
|
-
this.options.tui.requestRender();
|
|
371
|
-
};
|
|
372
|
-
this.customInput.onEscape = () => {
|
|
373
|
-
this.editingCustom = false;
|
|
374
|
-
this.customInput.focused = false;
|
|
375
|
-
this.options.tui.requestRender();
|
|
376
|
-
};
|
|
377
|
-
this.policiesInput.onSubmit = (value) => {
|
|
378
|
-
let parsed: unknown;
|
|
379
|
-
try {
|
|
380
|
-
parsed = JSON.parse(value || "{}");
|
|
381
|
-
} catch {
|
|
382
|
-
this.policiesError = "Enter a valid JSON object.";
|
|
383
|
-
this.options.tui.requestRender();
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
if (!isValidAdvisorToolPolicies(parsed)) {
|
|
387
|
-
this.policiesError =
|
|
388
|
-
"Use non-empty tool names with full, summary, or exclude values.";
|
|
389
|
-
this.options.tui.requestRender();
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
this.settings.toolPolicies = parsed;
|
|
393
|
-
this.policiesError = undefined;
|
|
394
|
-
this.editingPolicies = false;
|
|
395
|
-
this.policiesInput.focused = false;
|
|
396
|
-
this.options.tui.requestRender();
|
|
397
|
-
};
|
|
398
|
-
this.policiesInput.onEscape = () => {
|
|
399
|
-
this.editingPolicies = false;
|
|
400
|
-
this.policiesInput.focused = false;
|
|
401
|
-
this.options.tui.requestRender();
|
|
402
|
-
};
|
|
843
|
+
this.settingsList = this.createSettingsList();
|
|
403
844
|
}
|
|
404
845
|
|
|
405
846
|
invalidate(): void {
|
|
406
|
-
this.
|
|
847
|
+
this.settingsList.invalidate();
|
|
407
848
|
}
|
|
408
849
|
|
|
409
|
-
/** Called by TUI teardown when it supports component disposal. */
|
|
410
850
|
dispose(): void {
|
|
411
851
|
this.stopSimpleModeGradient();
|
|
412
852
|
}
|
|
413
853
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
"save",
|
|
423
|
-
];
|
|
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
|
+
);
|
|
424
862
|
}
|
|
425
863
|
|
|
426
|
-
|
|
427
|
-
|
|
864
|
+
handleInput(keyData: string): void {
|
|
865
|
+
if (!this.changeWithArrow(keyData)) {
|
|
866
|
+
this.settingsList.handleInput(keyData);
|
|
867
|
+
}
|
|
868
|
+
this.options.tui.requestRender();
|
|
428
869
|
}
|
|
429
870
|
|
|
430
|
-
private
|
|
431
|
-
|
|
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;
|
|
432
911
|
}
|
|
433
912
|
|
|
434
|
-
private
|
|
435
|
-
const
|
|
436
|
-
|
|
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 }
|
|
437
930
|
);
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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;
|
|
443
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
|
+
)
|
|
444
1216
|
);
|
|
1217
|
+
return items;
|
|
445
1218
|
}
|
|
446
1219
|
|
|
447
|
-
private
|
|
448
|
-
|
|
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
|
+
};
|
|
449
1234
|
}
|
|
450
1235
|
|
|
451
|
-
private
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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;
|
|
466
1296
|
}
|
|
467
1297
|
|
|
468
1298
|
private rainbowGradient(text: string): string {
|
|
@@ -510,449 +1340,79 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
510
1340
|
this.simpleModeGradientStartedAt = undefined;
|
|
511
1341
|
}
|
|
512
1342
|
|
|
513
|
-
private
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
private cancel(): void {
|
|
523
|
-
this.stopSimpleModeGradient();
|
|
524
|
-
this.options.onCancel();
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/** Disclosure and output-limit rows, split out to keep each builder simple. */
|
|
528
|
-
private disclosureRows(): string[] {
|
|
529
|
-
const onOff = (value: boolean) => (value ? "On" : "Off");
|
|
530
|
-
return [
|
|
531
|
-
this.row(
|
|
532
|
-
"Tool result lines",
|
|
533
|
-
String(this.settings.toolResultMaxLines ?? 2000),
|
|
534
|
-
"toolResultMaxLines"
|
|
535
|
-
),
|
|
536
|
-
this.row(
|
|
537
|
-
"Tool result bytes",
|
|
538
|
-
String(this.settings.toolResultMaxBytes ?? 50 * 1024),
|
|
539
|
-
"toolResultMaxBytes"
|
|
540
|
-
),
|
|
541
|
-
this.row(
|
|
542
|
-
"Redact common secrets",
|
|
543
|
-
onOff(this.settings.redactSecrets ?? false),
|
|
544
|
-
"redactSecrets"
|
|
545
|
-
),
|
|
546
|
-
this.row(
|
|
547
|
-
"Repository context",
|
|
548
|
-
this.settings.gitContext ?? "summary",
|
|
549
|
-
"gitContext"
|
|
550
|
-
),
|
|
551
|
-
this.row(
|
|
552
|
-
"Repository context chars",
|
|
553
|
-
String(this.settings.gitContextMaxChars ?? 20_000),
|
|
554
|
-
"gitContextMaxChars"
|
|
555
|
-
),
|
|
556
|
-
this.row(
|
|
557
|
-
"Tool disclosure policies",
|
|
558
|
-
Object.keys(this.settings.toolPolicies ?? {}).length
|
|
559
|
-
? "Exact names configured"
|
|
560
|
-
: "All tools: full",
|
|
561
|
-
"toolPolicies"
|
|
562
|
-
),
|
|
563
|
-
this.row(
|
|
564
|
-
"Tracked file content",
|
|
565
|
-
onOff(this.settings.trackedFileContent ?? false),
|
|
566
|
-
"trackedFileContent"
|
|
567
|
-
),
|
|
568
|
-
this.row(
|
|
569
|
-
"Untracked file content",
|
|
570
|
-
onOff(this.settings.untrackedContent ?? false),
|
|
571
|
-
"untrackedContent"
|
|
572
|
-
),
|
|
573
|
-
this.row(
|
|
574
|
-
"Outcome logging (global)",
|
|
575
|
-
onOff(this.settings.outcomeLogging ?? false),
|
|
576
|
-
"outcomeLogging"
|
|
577
|
-
),
|
|
578
|
-
];
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
private advancedRows(width: number): string[] {
|
|
582
|
-
const onOff = (value: boolean) => (value ? "On" : "Off");
|
|
583
|
-
const rows = [
|
|
584
|
-
this.row("Advisor reasoning", this.currentEffort(), "effort"),
|
|
585
|
-
this.row(
|
|
586
|
-
"Experimental Advisor Scout",
|
|
587
|
-
onOff(this.settings.scoutEnabled ?? false),
|
|
588
|
-
"scoutEnabled"
|
|
589
|
-
),
|
|
590
|
-
this.row("Plan gate", onOff(this.settings.planGate), "planGate"),
|
|
591
|
-
this.row("Failure gate", onOff(this.settings.failureGate), "failureGate"),
|
|
592
|
-
this.row(
|
|
593
|
-
"Completion gate",
|
|
594
|
-
onOff(this.settings.completionGate),
|
|
595
|
-
"completionGate"
|
|
596
|
-
),
|
|
597
|
-
this.row(
|
|
598
|
-
"Collapse long responses",
|
|
599
|
-
onOff(this.settings.collapseResponses),
|
|
600
|
-
"collapseResponses"
|
|
601
|
-
),
|
|
602
|
-
this.row(
|
|
603
|
-
"Custom invocation",
|
|
604
|
-
this.settings.customRule || "None",
|
|
605
|
-
"customRule"
|
|
606
|
-
),
|
|
607
|
-
this.row(
|
|
608
|
-
"Block on critical advice",
|
|
609
|
-
onOff(this.settings.blockOnBlocked ?? true),
|
|
610
|
-
"blockOnBlocked"
|
|
611
|
-
),
|
|
612
|
-
this.row(
|
|
613
|
-
"Automatic loop gate",
|
|
614
|
-
onOff(this.settings.autoLoopGate ?? true),
|
|
615
|
-
"autoLoopGate"
|
|
616
|
-
),
|
|
617
|
-
this.row(
|
|
618
|
-
"Loop threshold",
|
|
619
|
-
`After ${this.settings.loopThreshold ?? 3} repeats`,
|
|
620
|
-
"loopThreshold"
|
|
621
|
-
),
|
|
622
|
-
this.row(
|
|
623
|
-
"Max Advisor calls/session",
|
|
624
|
-
this.settings.maxCallsPerSession === undefined
|
|
625
|
-
? "∞"
|
|
626
|
-
: String(this.settings.maxCallsPerSession),
|
|
627
|
-
"maxCallsPerSession"
|
|
628
|
-
),
|
|
629
|
-
this.row(
|
|
630
|
-
"Session Advisor Summary",
|
|
631
|
-
onOff(this.settings.sessionSummary ?? false),
|
|
632
|
-
"sessionSummary"
|
|
633
|
-
),
|
|
634
|
-
this.row(
|
|
635
|
-
"Gate failure mode",
|
|
636
|
-
this.settings.failureMode ?? "block-session",
|
|
637
|
-
"failureMode"
|
|
638
|
-
),
|
|
639
|
-
this.row(
|
|
640
|
-
"Herdr integration",
|
|
641
|
-
onOff(this.settings.herdrIntegration ?? true),
|
|
642
|
-
"herdrIntegration"
|
|
643
|
-
),
|
|
644
|
-
...this.disclosureRows(),
|
|
645
|
-
];
|
|
646
|
-
if (this.editingCustom) {
|
|
647
|
-
rows.push(
|
|
648
|
-
` ${this.customInput.render(Math.max(10, width - 6))[0] || ""}`
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
if (this.editingPolicies) {
|
|
652
|
-
rows.push(
|
|
653
|
-
` ${this.policiesInput.render(Math.max(10, width - 6))[0] || ""}`
|
|
654
|
-
);
|
|
655
|
-
if (this.policiesError) {
|
|
656
|
-
rows.push(` ${this.options.theme.fg("error", this.policiesError)}`);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
rows.push(this.row("Save changes", "", "save"));
|
|
660
|
-
return rows;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
render(width: number): string[] {
|
|
664
|
-
const { theme, presets } = this.options;
|
|
665
|
-
const simpleMode = this.settings.simpleMode ?? false;
|
|
666
|
-
const modeRows = [
|
|
667
|
-
this.row("Simple mode", simpleMode ? "On" : "Off", "simpleMode"),
|
|
668
|
-
this.row("Always on", this.settings.alwaysOn ? "On" : "Off", "alwaysOn"),
|
|
669
|
-
];
|
|
670
|
-
const lines = [theme.fg("accent", theme.bold(" Advisor settings")), ""];
|
|
671
|
-
if (simpleMode) {
|
|
672
|
-
const trackWidth = Math.max(24, Math.min(60, width - 4));
|
|
673
|
-
const positions = presets.map((_, index) =>
|
|
674
|
-
Math.round((index * (trackWidth - 1)) / Math.max(1, presets.length - 1))
|
|
675
|
-
);
|
|
676
|
-
const track = Array.from({ length: trackWidth }, () => "─");
|
|
677
|
-
track[positions[this.contextIndex]] = "▲";
|
|
678
|
-
const labels = Array.from({ length: trackWidth }, () => " ");
|
|
679
|
-
for (let index = 0; index < presets.length; index += 1) {
|
|
680
|
-
const { label } = presets[index];
|
|
681
|
-
const start = Math.max(
|
|
682
|
-
0,
|
|
683
|
-
Math.min(
|
|
684
|
-
trackWidth - label.length,
|
|
685
|
-
positions[index] - Math.floor(label.length / 2)
|
|
686
|
-
)
|
|
687
|
-
);
|
|
688
|
-
for (let char = 0; char < label.length; char += 1) {
|
|
689
|
-
labels[start + char] = label[char];
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
lines.push(
|
|
693
|
-
` ${this.row("Context window", this.currentContext().label, "context")}`
|
|
694
|
-
);
|
|
695
|
-
lines.push(` ${theme.fg("muted", "Recent history")}`);
|
|
696
|
-
lines.push(` ${theme.fg("muted", track.join(""))}`);
|
|
697
|
-
lines.push(` ${theme.fg("text", labels.join(""))}`);
|
|
698
|
-
lines.push("");
|
|
699
|
-
lines.push(...modeRows.map((line) => ` ${line}`));
|
|
700
|
-
lines.push(` ${this.row("Save changes", "", "save")}`);
|
|
701
|
-
lines.push("");
|
|
702
|
-
lines.push(
|
|
703
|
-
` ${theme.fg("muted", "↑/↓ select · ←/→ adjust · Enter saves · Esc cancels")}`
|
|
704
|
-
);
|
|
705
|
-
return lines.map((line) => truncateToWidth(line, width));
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
const trackWidth = Math.max(24, Math.min(60, width - 4));
|
|
709
|
-
const positions = presets.map((_, index) =>
|
|
710
|
-
Math.round((index * (trackWidth - 1)) / Math.max(1, presets.length - 1))
|
|
711
|
-
);
|
|
712
|
-
const track = Array.from({ length: trackWidth }, () => "─");
|
|
713
|
-
track[positions[this.contextIndex]] = "▲";
|
|
714
|
-
const labels = Array.from({ length: trackWidth }, () => " ");
|
|
715
|
-
for (let index = 0; index < presets.length; index += 1) {
|
|
716
|
-
const { label } = presets[index];
|
|
717
|
-
const start = Math.max(
|
|
718
|
-
0,
|
|
719
|
-
Math.min(
|
|
720
|
-
trackWidth - label.length,
|
|
721
|
-
positions[index] - Math.floor(label.length / 2)
|
|
722
|
-
)
|
|
723
|
-
);
|
|
724
|
-
for (let char = 0; char < label.length; char += 1) {
|
|
725
|
-
labels[start + char] = label[char];
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
const heading = `Recent history${" ".repeat(Math.max(1, trackWidth - "Recent history".length - "Full branch".length))}Full branch`;
|
|
729
|
-
lines.push(
|
|
730
|
-
` ${this.row("Context window", this.currentContext().label, "context")}`
|
|
731
|
-
);
|
|
732
|
-
lines.push(` ${theme.fg("muted", heading)}`);
|
|
733
|
-
lines.push(` ${theme.fg("muted", track.join(""))}`);
|
|
734
|
-
lines.push(` ${theme.fg("text", labels.join(""))}`);
|
|
735
|
-
lines.push("");
|
|
736
|
-
lines.push(
|
|
737
|
-
...[...modeRows, ...this.advancedRows(width)].map((line) => ` ${line}`)
|
|
738
|
-
);
|
|
739
|
-
lines.push("");
|
|
740
|
-
lines.push(
|
|
741
|
-
` ${theme.fg("muted", "↑/↓ select · ←/→ adjust · Enter edits or saves · Esc cancels")}`
|
|
742
|
-
);
|
|
743
|
-
return lines.map((line) => truncateToWidth(line, width));
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
handleInput(keyData: string): void {
|
|
747
|
-
const { tui } = this.options;
|
|
748
|
-
if (this.editingCustom) {
|
|
749
|
-
this.customInput.handleInput(keyData);
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
if (this.editingPolicies) {
|
|
753
|
-
this.policiesInput.handleInput(keyData);
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
if (matchesKey(keyData, Key.up)) {
|
|
757
|
-
this.selectedRow = Math.max(0, this.selectedRow - 1);
|
|
758
|
-
} else if (matchesKey(keyData, Key.down)) {
|
|
759
|
-
this.selectedRow = Math.min(
|
|
760
|
-
this.visibleRows().length - 1,
|
|
761
|
-
this.selectedRow + 1
|
|
762
|
-
);
|
|
763
|
-
} else if (matchesKey(keyData, Key.left)) {
|
|
764
|
-
this.adjust(-1);
|
|
765
|
-
} else if (matchesKey(keyData, Key.right)) {
|
|
766
|
-
this.adjust(1);
|
|
767
|
-
} else if (matchesKey(keyData, Key.enter)) {
|
|
768
|
-
const row = this.selectedRowId();
|
|
769
|
-
if (row === "customRule") {
|
|
770
|
-
this.editingCustom = true;
|
|
771
|
-
this.customInput.setValue(this.settings.customRule || "");
|
|
772
|
-
this.customInput.focused = this.focused;
|
|
773
|
-
tui.requestRender();
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
if (row === "toolPolicies") {
|
|
777
|
-
this.editingPolicies = true;
|
|
778
|
-
this.policiesError = undefined;
|
|
779
|
-
this.policiesInput.setValue(
|
|
780
|
-
JSON.stringify(this.settings.toolPolicies ?? {})
|
|
781
|
-
);
|
|
782
|
-
this.policiesInput.focused = this.focused;
|
|
783
|
-
tui.requestRender();
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
if (row === "save") {
|
|
787
|
-
this.save();
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
this.adjust(1);
|
|
791
|
-
} else if (matchesKey(keyData, Key.escape)) {
|
|
792
|
-
this.cancel();
|
|
793
|
-
return;
|
|
794
|
-
} else {
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
tui.requestRender();
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one keyboard dispatcher maps each settings row to its bounded adjustment.
|
|
801
|
-
private adjust(direction: number): void {
|
|
802
|
-
switch (this.selectedRowId()) {
|
|
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;
|
|
803
1350
|
case "simpleMode":
|
|
804
|
-
this.settings.simpleMode =
|
|
1351
|
+
this.settings.simpleMode = value === "On";
|
|
805
1352
|
if (this.settings.simpleMode) {
|
|
806
1353
|
this.startSimpleModeGradient();
|
|
807
1354
|
} else {
|
|
808
1355
|
this.stopSimpleModeGradient();
|
|
809
1356
|
}
|
|
810
|
-
// The visible row set changes with the mode, so keep the cursor on the
|
|
811
|
-
// row the user is actually operating rather than on its old index.
|
|
812
|
-
this.focusRow("simpleMode");
|
|
813
1357
|
break;
|
|
814
1358
|
case "alwaysOn":
|
|
815
|
-
this.settings.alwaysOn =
|
|
816
|
-
break;
|
|
817
|
-
case "context":
|
|
818
|
-
this.contextIndex = Math.max(
|
|
819
|
-
0,
|
|
820
|
-
Math.min(
|
|
821
|
-
this.options.presets.length - 1,
|
|
822
|
-
this.contextIndex + direction
|
|
823
|
-
)
|
|
824
|
-
);
|
|
825
|
-
this.settings.contextMaxChars =
|
|
826
|
-
this.options.presets[this.contextIndex].value;
|
|
1359
|
+
this.settings.alwaysOn = value === "On";
|
|
827
1360
|
break;
|
|
828
1361
|
case "effort":
|
|
829
|
-
this.
|
|
830
|
-
0,
|
|
831
|
-
Math.min(
|
|
832
|
-
this.options.effortLevels.length - 1,
|
|
833
|
-
this.effortIndex + direction
|
|
834
|
-
)
|
|
835
|
-
);
|
|
836
|
-
this.settings.effort = this.options.effortLevels[this.effortIndex];
|
|
837
|
-
break;
|
|
838
|
-
case "scoutEnabled":
|
|
839
|
-
this.settings.scoutEnabled = !(this.settings.scoutEnabled ?? false);
|
|
840
|
-
break;
|
|
841
|
-
case "planGate":
|
|
842
|
-
this.settings.planGate = !this.settings.planGate;
|
|
843
|
-
break;
|
|
844
|
-
case "failureGate":
|
|
845
|
-
this.settings.failureGate = !this.settings.failureGate;
|
|
846
|
-
break;
|
|
847
|
-
case "completionGate":
|
|
848
|
-
this.settings.completionGate = !this.settings.completionGate;
|
|
1362
|
+
this.settings.effort = value;
|
|
849
1363
|
break;
|
|
850
|
-
case "
|
|
851
|
-
this.settings.
|
|
1364
|
+
case "customRule":
|
|
1365
|
+
this.settings.customRule = value.trim() || undefined;
|
|
852
1366
|
break;
|
|
853
|
-
case "
|
|
854
|
-
this.settings.
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
this.settings.autoLoopGate = !(this.settings.autoLoopGate ?? true);
|
|
1367
|
+
case "toolPolicies":
|
|
1368
|
+
this.settings.toolPolicies = JSON.parse(
|
|
1369
|
+
value
|
|
1370
|
+
) as AdvisorSettings["toolPolicies"];
|
|
858
1371
|
break;
|
|
859
1372
|
case "loopThreshold":
|
|
860
|
-
this.settings.loopThreshold =
|
|
861
|
-
|
|
862
|
-
(this.settings.loopThreshold ?? 3) + direction
|
|
1373
|
+
this.settings.loopThreshold = Number(
|
|
1374
|
+
value.replace("After ", "").replace(" repeats", "")
|
|
863
1375
|
);
|
|
864
1376
|
break;
|
|
865
|
-
case "maxCallsPerSession":
|
|
866
|
-
const current = this.settings.maxCallsPerSession;
|
|
867
|
-
const numeric = [0, 1, 2, 3, 5, 10, 25, 50];
|
|
868
|
-
const sorted =
|
|
869
|
-
current === undefined || numeric.includes(current)
|
|
870
|
-
? numeric
|
|
871
|
-
: [...numeric, current].sort((a, b) => a - b);
|
|
872
|
-
const values: (number | undefined)[] = [undefined, ...sorted];
|
|
873
|
-
const index = values.indexOf(current);
|
|
1377
|
+
case "maxCallsPerSession":
|
|
874
1378
|
this.settings.maxCallsPerSession =
|
|
875
|
-
|
|
1379
|
+
value === "∞" ? undefined : Number(value);
|
|
876
1380
|
break;
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
this.settings.sessionSummary = !(this.settings.sessionSummary ?? false);
|
|
1381
|
+
case "failureMode":
|
|
1382
|
+
this.settings.failureMode = value as AdvisorSettings["failureMode"];
|
|
880
1383
|
break;
|
|
881
|
-
case "
|
|
882
|
-
|
|
883
|
-
"block-session",
|
|
884
|
-
"block-tool",
|
|
885
|
-
"warn-and-continue",
|
|
886
|
-
];
|
|
887
|
-
const index = Math.max(
|
|
888
|
-
0,
|
|
889
|
-
modes.indexOf(this.settings.failureMode ?? "block-session")
|
|
890
|
-
);
|
|
891
|
-
this.settings.failureMode =
|
|
892
|
-
modes[Math.max(0, Math.min(modes.length - 1, index + direction))];
|
|
1384
|
+
case "gitContext":
|
|
1385
|
+
this.settings.gitContext = value as AdvisorSettings["gitContext"];
|
|
893
1386
|
break;
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
this.settings.herdrIntegration = !(
|
|
897
|
-
this.settings.herdrIntegration ?? true
|
|
898
|
-
);
|
|
899
|
-
break;
|
|
900
|
-
case "redactSecrets":
|
|
901
|
-
this.settings.redactSecrets = !(this.settings.redactSecrets ?? false);
|
|
902
|
-
break;
|
|
903
|
-
case "trackedFileContent":
|
|
904
|
-
this.settings.trackedFileContent = !(
|
|
905
|
-
this.settings.trackedFileContent ?? false
|
|
906
|
-
);
|
|
907
|
-
break;
|
|
908
|
-
case "untrackedContent":
|
|
909
|
-
this.settings.untrackedContent = !(
|
|
910
|
-
this.settings.untrackedContent ?? false
|
|
911
|
-
);
|
|
912
|
-
break;
|
|
913
|
-
case "outcomeLogging":
|
|
914
|
-
this.settings.outcomeLogging = !(this.settings.outcomeLogging ?? false);
|
|
915
|
-
break;
|
|
916
|
-
case "gitContext": {
|
|
917
|
-
const levels: AdvisorSettings["gitContext"][] = [
|
|
918
|
-
"off",
|
|
919
|
-
"summary",
|
|
920
|
-
"full",
|
|
921
|
-
];
|
|
922
|
-
const index = Math.max(
|
|
923
|
-
0,
|
|
924
|
-
levels.indexOf(this.settings.gitContext ?? "summary")
|
|
925
|
-
);
|
|
926
|
-
this.settings.gitContext =
|
|
927
|
-
levels[Math.max(0, Math.min(levels.length - 1, index + direction))];
|
|
928
|
-
break;
|
|
929
|
-
}
|
|
930
|
-
case "gitContextMaxChars": {
|
|
931
|
-
this.settings.gitContextMaxChars = stepNumericPreset(
|
|
932
|
-
this.settings.gitContextMaxChars ?? 20_000,
|
|
933
|
-
[0, 5000, 10_000, 20_000, 50_000, 100_000],
|
|
934
|
-
direction
|
|
935
|
-
);
|
|
1387
|
+
case "toolResultMaxLines":
|
|
1388
|
+
this.settings.toolResultMaxLines = Number(value);
|
|
936
1389
|
break;
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
this.settings.toolResultMaxLines = stepNumericPreset(
|
|
940
|
-
this.settings.toolResultMaxLines ?? 2000,
|
|
941
|
-
[0, 500, 1000, 2000, 5000, 10_000],
|
|
942
|
-
direction
|
|
943
|
-
);
|
|
1390
|
+
case "toolResultMaxBytes":
|
|
1391
|
+
this.settings.toolResultMaxBytes = Number(value);
|
|
944
1392
|
break;
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
this.settings.toolResultMaxBytes = stepNumericPreset(
|
|
948
|
-
this.settings.toolResultMaxBytes ?? 50 * 1024,
|
|
949
|
-
[0, 10 * 1024, 50 * 1024, 100 * 1024, 500 * 1024],
|
|
950
|
-
direction
|
|
951
|
-
);
|
|
1393
|
+
case "gitContextMaxChars":
|
|
1394
|
+
this.settings.gitContextMaxChars = Number(value);
|
|
952
1395
|
break;
|
|
953
|
-
}
|
|
954
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
|
+
}
|
|
955
1402
|
break;
|
|
956
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
|
+
}
|
|
957
1417
|
}
|
|
958
1418
|
}
|