@dreb/coding-agent 2.45.4 → 2.47.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/README.md +10 -4
- package/dist/core/agent-session.d.ts +10 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +94 -23
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/context-buffer.d.ts +2 -0
- package/dist/core/context-buffer.d.ts.map +1 -1
- package/dist/core/context-buffer.js +8 -0
- package/dist/core/context-buffer.js.map +1 -1
- package/dist/core/dispatch-arbiter.d.ts +94 -0
- package/dist/core/dispatch-arbiter.d.ts.map +1 -0
- package/dist/core/dispatch-arbiter.js +316 -0
- package/dist/core/dispatch-arbiter.js.map +1 -0
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +7 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +10 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +35 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/git-repo-state.d.ts +2 -0
- package/dist/core/git-repo-state.d.ts.map +1 -1
- package/dist/core/git-repo-state.js +19 -0
- package/dist/core/git-repo-state.js.map +1 -1
- package/dist/core/model-routing-guide.d.ts +15 -0
- package/dist/core/model-routing-guide.d.ts.map +1 -0
- package/dist/core/model-routing-guide.js +186 -0
- package/dist/core/model-routing-guide.js.map +1 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -0
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +38 -2
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +18 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +33 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/thinking.d.ts +14 -0
- package/dist/core/thinking.d.ts.map +1 -1
- package/dist/core/thinking.js +35 -2
- package/dist/core/thinking.js.map +1 -1
- package/dist/core/tools/ask-user.d.ts +45 -0
- package/dist/core/tools/ask-user.d.ts.map +1 -0
- package/dist/core/tools/ask-user.js +210 -0
- package/dist/core/tools/ask-user.js.map +1 -0
- package/dist/core/tools/index.d.ts +18 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +8 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/subagent.d.ts +53 -6
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +343 -53
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/modes/interactive/components/ask-user.d.ts +59 -0
- package/dist/modes/interactive/components/ask-user.d.ts.map +1 -0
- package/dist/modes/interactive/components/ask-user.js +240 -0
- package/dist/modes/interactive/components/ask-user.js.map +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts +4 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +99 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +13 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +167 -6
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +18 -3
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +177 -59
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +32 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/agent-models.md +33 -1
- package/docs/dashboard.md +9 -3
- package/docs/extensions.md +33 -3
- package/docs/json.md +31 -5
- package/docs/rpc.md +105 -15
- package/docs/session.md +8 -2
- package/docs/settings.md +33 -1
- package/docs/skills.md +38 -0
- package/package.json +1 -1
- package/skills/model-routing-guide/SKILL.md +182 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ask_user question component.
|
|
3
|
+
*
|
|
4
|
+
* Renders a clarifying question with optional single- or multi-select options
|
|
5
|
+
* and an optional free-text field, matching the approved cross-surface design.
|
|
6
|
+
*
|
|
7
|
+
* Keyboard model (settled in issue 396 discussion):
|
|
8
|
+
* ↑/↓ move a single cursor through the options and, last, the free-text field
|
|
9
|
+
* Space toggle the highlighted checkbox (multi-select only)
|
|
10
|
+
* Enter single-select: pick the highlighted option and submit;
|
|
11
|
+
* free-text field: submit the typed answer;
|
|
12
|
+
* multi-select: submit all checked options plus any free text
|
|
13
|
+
* Shift+Enter insert a newline in the multiline free-text field
|
|
14
|
+
* Esc stop the current agent turn
|
|
15
|
+
*/
|
|
16
|
+
import { Container, type Focusable, type TUI } from "@dreb/tui";
|
|
17
|
+
import type { AskRequest, AskResult } from "../../../core/extensions/types.js";
|
|
18
|
+
export interface AskUserComponentOptions {
|
|
19
|
+
tui?: TUI;
|
|
20
|
+
timeout?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare class AskUserComponent extends Container implements Focusable {
|
|
23
|
+
private options;
|
|
24
|
+
private allowFreeText;
|
|
25
|
+
private multiSelect;
|
|
26
|
+
private multiline;
|
|
27
|
+
/** Cursor over [options..., freeTextRow?]. */
|
|
28
|
+
private cursorIndex;
|
|
29
|
+
/** Checkbox state for multi-select, aligned to options. */
|
|
30
|
+
private checked;
|
|
31
|
+
private onSubmitCallback;
|
|
32
|
+
private onStopCallback;
|
|
33
|
+
private titleText;
|
|
34
|
+
private baseTitle;
|
|
35
|
+
private optionsContainer;
|
|
36
|
+
private fieldLabel;
|
|
37
|
+
private input;
|
|
38
|
+
private editor;
|
|
39
|
+
private countdown;
|
|
40
|
+
private submitted;
|
|
41
|
+
private _focused;
|
|
42
|
+
get focused(): boolean;
|
|
43
|
+
set focused(value: boolean);
|
|
44
|
+
private get freeTextRow();
|
|
45
|
+
private get lastRow();
|
|
46
|
+
private cursorOnField;
|
|
47
|
+
constructor(request: AskRequest, onSubmit: (result: AskResult) => void, onStop: () => void, opts?: AskUserComponentOptions);
|
|
48
|
+
private buildHint;
|
|
49
|
+
private renderRows;
|
|
50
|
+
private syncFieldFocus;
|
|
51
|
+
private moveCursor;
|
|
52
|
+
private fieldText;
|
|
53
|
+
private currentAnswer;
|
|
54
|
+
private submit;
|
|
55
|
+
private stop;
|
|
56
|
+
handleInput(keyData: string): void;
|
|
57
|
+
dispose(): void;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=ask-user.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ask-user.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/ask-user.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,SAAS,EAAU,KAAK,SAAS,EAAiD,KAAK,GAAG,EAAE,MAAM,WAAW,CAAC;AACvH,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAM/E,MAAM,WAAW,uBAAuB;IACvC,GAAG,CAAC,EAAE,GAAG,CAAC;IACV,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,gBAAiB,SAAQ,SAAU,YAAW,SAAS;IACnE,OAAO,CAAC,OAAO,CAAW;IAC1B,OAAO,CAAC,aAAa,CAAU;IAC/B,OAAO,CAAC,WAAW,CAAU;IAC7B,OAAO,CAAC,SAAS,CAAU;IAE3B,8CAA8C;IAC9C,OAAO,CAAC,WAAW,CAAK;IACxB,2DAA2D;IAC3D,OAAO,CAAC,OAAO,CAAY;IAE3B,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,cAAc,CAAa;IAEnC,OAAO,CAAC,SAAS,CAAO;IACxB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,gBAAgB,CAAY;IACpC,OAAO,CAAC,UAAU,CAAmB;IACrC,OAAO,CAAC,KAAK,CAAoB;IACjC,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,SAAS,CAA6B;IAC9C,OAAO,CAAC,SAAS,CAAS;IAE1B,OAAO,CAAC,QAAQ,CAAS;IACzB,IAAI,OAAO,IAAI,OAAO,CAErB;IACD,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAGzB;IAED,OAAO,KAAK,WAAW,GAEtB;IAED,OAAO,KAAK,OAAO,GAElB;IAED,OAAO,CAAC,aAAa;IAIrB,YACC,OAAO,EAAE,UAAU,EACnB,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,EACrC,MAAM,EAAE,MAAM,IAAI,EAClB,IAAI,CAAC,EAAE,uBAAuB,EA8D9B;IAED,OAAO,CAAC,SAAS;IASjB,OAAO,CAAC,UAAU;IAkBlB,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,MAAM;IAQd,OAAO,CAAC,IAAI;IAMZ,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CA8CjC;IAED,OAAO,IAAI,IAAI,CAEd;CACD","sourcesContent":["/**\n * ask_user question component.\n *\n * Renders a clarifying question with optional single- or multi-select options\n * and an optional free-text field, matching the approved cross-surface design.\n *\n * Keyboard model (settled in issue 396 discussion):\n * ↑/↓ move a single cursor through the options and, last, the free-text field\n * Space toggle the highlighted checkbox (multi-select only)\n * Enter single-select: pick the highlighted option and submit;\n * free-text field: submit the typed answer;\n * multi-select: submit all checked options plus any free text\n * Shift+Enter insert a newline in the multiline free-text field\n * Esc stop the current agent turn\n */\n\nimport { Container, Editor, type Focusable, getKeybindings, Input, Markdown, Spacer, Text, type TUI } from \"@dreb/tui\";\nimport type { AskRequest, AskResult } from \"../../../core/extensions/types.js\";\nimport { getEditorTheme, getMarkdownTheme, theme } from \"../theme/theme.js\";\nimport { CountdownTimer } from \"./countdown-timer.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\nimport { keyHint, rawKeyHint } from \"./keybinding-hints.js\";\n\nexport interface AskUserComponentOptions {\n\ttui?: TUI;\n\ttimeout?: number;\n}\n\nexport class AskUserComponent extends Container implements Focusable {\n\tprivate options: string[];\n\tprivate allowFreeText: boolean;\n\tprivate multiSelect: boolean;\n\tprivate multiline: boolean;\n\n\t/** Cursor over [options..., freeTextRow?]. */\n\tprivate cursorIndex = 0;\n\t/** Checkbox state for multi-select, aligned to options. */\n\tprivate checked: boolean[];\n\n\tprivate onSubmitCallback: (result: AskResult) => void;\n\tprivate onStopCallback: () => void;\n\n\tprivate titleText: Text;\n\tprivate baseTitle: string;\n\tprivate optionsContainer: Container;\n\tprivate fieldLabel: Text | undefined;\n\tprivate input: Input | undefined;\n\tprivate editor: Editor | undefined;\n\tprivate countdown: CountdownTimer | undefined;\n\tprivate submitted = false;\n\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.syncFieldFocus();\n\t}\n\n\tprivate get freeTextRow(): number {\n\t\treturn this.allowFreeText ? this.options.length : -1;\n\t}\n\n\tprivate get lastRow(): number {\n\t\treturn this.allowFreeText ? this.options.length : this.options.length - 1;\n\t}\n\n\tprivate cursorOnField(): boolean {\n\t\treturn this.allowFreeText && this.cursorIndex === this.freeTextRow;\n\t}\n\n\tconstructor(\n\t\trequest: AskRequest,\n\t\tonSubmit: (result: AskResult) => void,\n\t\tonStop: () => void,\n\t\topts?: AskUserComponentOptions,\n\t) {\n\t\tsuper();\n\n\t\tthis.options = request.options ?? [];\n\t\tthis.allowFreeText = request.allowFreeText !== false;\n\t\tthis.multiSelect = request.multiSelect === true && this.options.length > 0;\n\t\tthis.multiline = request.multiline === true;\n\t\tthis.checked = this.options.map(() => false);\n\t\tthis.onSubmitCallback = onSubmit;\n\t\tthis.onStopCallback = onStop;\n\t\tthis.baseTitle = request.title?.trim() || \"Question\";\n\n\t\t// Start the cursor on the first option, or the free-text field when there\n\t\t// are no options.\n\t\tthis.cursorIndex = this.options.length > 0 ? 0 : this.freeTextRow;\n\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.titleText = new Text(theme.fg(\"accent\", theme.bold(this.baseTitle)), 1, 0);\n\t\tthis.addChild(this.titleText);\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.addChild(new Markdown(request.question, 1, 0, getMarkdownTheme(), undefined, true));\n\t\tthis.addChild(new Spacer(1));\n\n\t\tif (opts?.timeout && opts.timeout > 0 && opts.tui) {\n\t\t\tthis.countdown = new CountdownTimer(\n\t\t\t\topts.timeout,\n\t\t\t\topts.tui,\n\t\t\t\t(s) => this.titleText.setText(theme.fg(\"accent\", theme.bold(`${this.baseTitle} (${s}s)`))),\n\t\t\t\t() => this.stop(),\n\t\t\t);\n\t\t}\n\n\t\tthis.optionsContainer = new Container();\n\t\tthis.addChild(this.optionsContainer);\n\n\t\tif (this.allowFreeText) {\n\t\t\tthis.addChild(new Spacer(1));\n\t\t\tthis.fieldLabel = new Text(\"\", 1, 0);\n\t\t\tthis.addChild(this.fieldLabel);\n\t\t\tif (this.multiline && opts?.tui) {\n\t\t\t\tthis.editor = new Editor(opts.tui, getEditorTheme(), {});\n\t\t\t\t// The Editor clears its own state before invoking onSubmit, so we must\n\t\t\t\t// use the text it hands us rather than re-reading the now-empty editor.\n\t\t\t\tthis.editor.onSubmit = (text) => this.submit(text);\n\t\t\t\tthis.addChild(this.editor);\n\t\t\t} else {\n\t\t\t\tthis.input = new Input();\n\t\t\t\tthis.addChild(this.input);\n\t\t\t}\n\t\t}\n\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(this.buildHint(), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\n\t\tthis.renderRows();\n\t\tthis.syncFieldFocus();\n\t}\n\n\tprivate buildHint(): string {\n\t\tconst parts = [rawKeyHint(\"↑↓\", \"move\")];\n\t\tif (this.multiSelect) parts.push(rawKeyHint(\"Space\", \"toggle\"));\n\t\tparts.push(keyHint(\"tui.select.confirm\", \"submit\"));\n\t\tif (this.multiline) parts.push(keyHint(\"tui.input.newLine\", \"newline\"));\n\t\tparts.push(keyHint(\"tui.select.cancel\", \"stop agent\"));\n\t\treturn parts.join(\" \");\n\t}\n\n\tprivate renderRows(): void {\n\t\tthis.optionsContainer.clear();\n\t\tfor (let i = 0; i < this.options.length; i++) {\n\t\t\tconst focused = this.cursorIndex === i;\n\t\t\tconst cursor = focused ? theme.fg(\"accent\", \"→ \") : \" \";\n\t\t\tconst glyph = this.multiSelect ? (this.checked[i] ? \"[x]\" : \"[ ]\") : focused ? \"(•)\" : \"( )\";\n\t\t\tconst label = focused ? theme.fg(\"accent\", this.options[i]) : theme.fg(\"text\", this.options[i]);\n\t\t\tconst glyphColored = this.checked[i] || (!this.multiSelect && focused) ? theme.fg(\"accent\", glyph) : glyph;\n\t\t\tthis.optionsContainer.addChild(new Text(`${cursor}${glyphColored} ${label}`, 1, 0));\n\t\t}\n\t\tif (this.fieldLabel) {\n\t\t\tconst focused = this.cursorOnField();\n\t\t\tconst prefix = focused ? theme.fg(\"accent\", \"→ \") : \" \";\n\t\t\tconst labelText = this.options.length > 0 ? \"Or type your own answer:\" : \"Your answer:\";\n\t\t\tthis.fieldLabel.setText(prefix + (focused ? theme.fg(\"accent\", labelText) : theme.fg(\"muted\", labelText)));\n\t\t}\n\t}\n\n\tprivate syncFieldFocus(): void {\n\t\tconst fieldFocused = this._focused && this.cursorOnField();\n\t\tif (this.input) this.input.focused = fieldFocused;\n\t\tif (this.editor) this.editor.focused = fieldFocused;\n\t}\n\n\tprivate moveCursor(delta: number): void {\n\t\tconst first = this.options.length > 0 ? 0 : this.freeTextRow;\n\t\tconst next = Math.max(first, Math.min(this.lastRow, this.cursorIndex + delta));\n\t\tif (next === this.cursorIndex) return;\n\t\tthis.cursorIndex = next;\n\t\tthis.renderRows();\n\t\tthis.syncFieldFocus();\n\t}\n\n\tprivate fieldText(editorTextOverride?: string): string {\n\t\tif (this.editor) return (editorTextOverride ?? this.editor.getText()).trim();\n\t\tif (this.input) return this.input.getValue().trim();\n\t\treturn \"\";\n\t}\n\n\tprivate currentAnswer(editorTextOverride?: string): AskResult | undefined {\n\t\tconst customText = this.fieldText(editorTextOverride) || undefined;\n\t\tif (this.multiSelect) {\n\t\t\tconst selected = this.options.filter((_, i) => this.checked[i]);\n\t\t\tif (selected.length === 0 && !customText) return undefined;\n\t\t\treturn { selected, customText };\n\t\t}\n\t\tif (this.cursorOnField()) {\n\t\t\tif (!customText) return undefined;\n\t\t\treturn { selected: [], customText };\n\t\t}\n\t\tconst option = this.options[this.cursorIndex];\n\t\t// Single-select: submit the highlighted option together with any typed\n\t\t// free text, matching the Dashboard, which combines a radio selection with\n\t\t// custom text (and the tool's own combined-answer result formatting).\n\t\treturn option ? { selected: [option], customText } : undefined;\n\t}\n\n\tprivate submit(editorTextOverride?: string): void {\n\t\tif (this.submitted) return;\n\t\tconst answer = this.currentAnswer(editorTextOverride);\n\t\tif (!answer) return; // Nothing to submit yet — Esc stops the turn instead.\n\t\tthis.submitted = true;\n\t\tthis.onSubmitCallback(answer);\n\t}\n\n\tprivate stop(): void {\n\t\tif (this.submitted) return;\n\t\tthis.submitted = true;\n\t\tthis.onStopCallback();\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getKeybindings();\n\n\t\tif (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\tthis.stop();\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.cursorOnField()) {\n\t\t\t// Multiline editor: Enter submits (via onSubmit), Shift+Enter inserts a\n\t\t\t// newline, ↑ at the top line leaves to the options, everything else edits.\n\t\t\tif (this.editor) {\n\t\t\t\tif (kb.matches(keyData, \"tui.select.up\") && this.editor.getCursor().line === 0) {\n\t\t\t\t\tthis.moveCursor(-1);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.editor.handleInput(keyData);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Single-line input: ↑ leaves to the options, Enter submits, else edits.\n\t\t\tif (this.input) {\n\t\t\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\t\t\tthis.moveCursor(-1);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (kb.matches(keyData, \"tui.select.confirm\") || keyData === \"\\n\") {\n\t\t\t\t\tthis.submit();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.input.handleInput(keyData);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Cursor on an option row.\n\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\tthis.moveCursor(-1);\n\t\t} else if (kb.matches(keyData, \"tui.select.down\")) {\n\t\t\tthis.moveCursor(1);\n\t\t} else if (this.multiSelect && keyData === \" \") {\n\t\t\tthis.checked[this.cursorIndex] = !this.checked[this.cursorIndex];\n\t\t\tthis.renderRows();\n\t\t} else if (kb.matches(keyData, \"tui.select.confirm\") || keyData === \"\\n\") {\n\t\t\tthis.submit();\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.countdown?.dispose();\n\t}\n}\n"]}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ask_user question component.
|
|
3
|
+
*
|
|
4
|
+
* Renders a clarifying question with optional single- or multi-select options
|
|
5
|
+
* and an optional free-text field, matching the approved cross-surface design.
|
|
6
|
+
*
|
|
7
|
+
* Keyboard model (settled in issue 396 discussion):
|
|
8
|
+
* ↑/↓ move a single cursor through the options and, last, the free-text field
|
|
9
|
+
* Space toggle the highlighted checkbox (multi-select only)
|
|
10
|
+
* Enter single-select: pick the highlighted option and submit;
|
|
11
|
+
* free-text field: submit the typed answer;
|
|
12
|
+
* multi-select: submit all checked options plus any free text
|
|
13
|
+
* Shift+Enter insert a newline in the multiline free-text field
|
|
14
|
+
* Esc stop the current agent turn
|
|
15
|
+
*/
|
|
16
|
+
import { Container, Editor, getKeybindings, Input, Markdown, Spacer, Text } from "@dreb/tui";
|
|
17
|
+
import { getEditorTheme, getMarkdownTheme, theme } from "../theme/theme.js";
|
|
18
|
+
import { CountdownTimer } from "./countdown-timer.js";
|
|
19
|
+
import { DynamicBorder } from "./dynamic-border.js";
|
|
20
|
+
import { keyHint, rawKeyHint } from "./keybinding-hints.js";
|
|
21
|
+
export class AskUserComponent extends Container {
|
|
22
|
+
options;
|
|
23
|
+
allowFreeText;
|
|
24
|
+
multiSelect;
|
|
25
|
+
multiline;
|
|
26
|
+
/** Cursor over [options..., freeTextRow?]. */
|
|
27
|
+
cursorIndex = 0;
|
|
28
|
+
/** Checkbox state for multi-select, aligned to options. */
|
|
29
|
+
checked;
|
|
30
|
+
onSubmitCallback;
|
|
31
|
+
onStopCallback;
|
|
32
|
+
titleText;
|
|
33
|
+
baseTitle;
|
|
34
|
+
optionsContainer;
|
|
35
|
+
fieldLabel;
|
|
36
|
+
input;
|
|
37
|
+
editor;
|
|
38
|
+
countdown;
|
|
39
|
+
submitted = false;
|
|
40
|
+
_focused = false;
|
|
41
|
+
get focused() {
|
|
42
|
+
return this._focused;
|
|
43
|
+
}
|
|
44
|
+
set focused(value) {
|
|
45
|
+
this._focused = value;
|
|
46
|
+
this.syncFieldFocus();
|
|
47
|
+
}
|
|
48
|
+
get freeTextRow() {
|
|
49
|
+
return this.allowFreeText ? this.options.length : -1;
|
|
50
|
+
}
|
|
51
|
+
get lastRow() {
|
|
52
|
+
return this.allowFreeText ? this.options.length : this.options.length - 1;
|
|
53
|
+
}
|
|
54
|
+
cursorOnField() {
|
|
55
|
+
return this.allowFreeText && this.cursorIndex === this.freeTextRow;
|
|
56
|
+
}
|
|
57
|
+
constructor(request, onSubmit, onStop, opts) {
|
|
58
|
+
super();
|
|
59
|
+
this.options = request.options ?? [];
|
|
60
|
+
this.allowFreeText = request.allowFreeText !== false;
|
|
61
|
+
this.multiSelect = request.multiSelect === true && this.options.length > 0;
|
|
62
|
+
this.multiline = request.multiline === true;
|
|
63
|
+
this.checked = this.options.map(() => false);
|
|
64
|
+
this.onSubmitCallback = onSubmit;
|
|
65
|
+
this.onStopCallback = onStop;
|
|
66
|
+
this.baseTitle = request.title?.trim() || "Question";
|
|
67
|
+
// Start the cursor on the first option, or the free-text field when there
|
|
68
|
+
// are no options.
|
|
69
|
+
this.cursorIndex = this.options.length > 0 ? 0 : this.freeTextRow;
|
|
70
|
+
this.addChild(new DynamicBorder());
|
|
71
|
+
this.addChild(new Spacer(1));
|
|
72
|
+
this.titleText = new Text(theme.fg("accent", theme.bold(this.baseTitle)), 1, 0);
|
|
73
|
+
this.addChild(this.titleText);
|
|
74
|
+
this.addChild(new Spacer(1));
|
|
75
|
+
this.addChild(new Markdown(request.question, 1, 0, getMarkdownTheme(), undefined, true));
|
|
76
|
+
this.addChild(new Spacer(1));
|
|
77
|
+
if (opts?.timeout && opts.timeout > 0 && opts.tui) {
|
|
78
|
+
this.countdown = new CountdownTimer(opts.timeout, opts.tui, (s) => this.titleText.setText(theme.fg("accent", theme.bold(`${this.baseTitle} (${s}s)`))), () => this.stop());
|
|
79
|
+
}
|
|
80
|
+
this.optionsContainer = new Container();
|
|
81
|
+
this.addChild(this.optionsContainer);
|
|
82
|
+
if (this.allowFreeText) {
|
|
83
|
+
this.addChild(new Spacer(1));
|
|
84
|
+
this.fieldLabel = new Text("", 1, 0);
|
|
85
|
+
this.addChild(this.fieldLabel);
|
|
86
|
+
if (this.multiline && opts?.tui) {
|
|
87
|
+
this.editor = new Editor(opts.tui, getEditorTheme(), {});
|
|
88
|
+
// The Editor clears its own state before invoking onSubmit, so we must
|
|
89
|
+
// use the text it hands us rather than re-reading the now-empty editor.
|
|
90
|
+
this.editor.onSubmit = (text) => this.submit(text);
|
|
91
|
+
this.addChild(this.editor);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
this.input = new Input();
|
|
95
|
+
this.addChild(this.input);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
this.addChild(new Spacer(1));
|
|
99
|
+
this.addChild(new Text(this.buildHint(), 1, 0));
|
|
100
|
+
this.addChild(new Spacer(1));
|
|
101
|
+
this.addChild(new DynamicBorder());
|
|
102
|
+
this.renderRows();
|
|
103
|
+
this.syncFieldFocus();
|
|
104
|
+
}
|
|
105
|
+
buildHint() {
|
|
106
|
+
const parts = [rawKeyHint("↑↓", "move")];
|
|
107
|
+
if (this.multiSelect)
|
|
108
|
+
parts.push(rawKeyHint("Space", "toggle"));
|
|
109
|
+
parts.push(keyHint("tui.select.confirm", "submit"));
|
|
110
|
+
if (this.multiline)
|
|
111
|
+
parts.push(keyHint("tui.input.newLine", "newline"));
|
|
112
|
+
parts.push(keyHint("tui.select.cancel", "stop agent"));
|
|
113
|
+
return parts.join(" ");
|
|
114
|
+
}
|
|
115
|
+
renderRows() {
|
|
116
|
+
this.optionsContainer.clear();
|
|
117
|
+
for (let i = 0; i < this.options.length; i++) {
|
|
118
|
+
const focused = this.cursorIndex === i;
|
|
119
|
+
const cursor = focused ? theme.fg("accent", "→ ") : " ";
|
|
120
|
+
const glyph = this.multiSelect ? (this.checked[i] ? "[x]" : "[ ]") : focused ? "(•)" : "( )";
|
|
121
|
+
const label = focused ? theme.fg("accent", this.options[i]) : theme.fg("text", this.options[i]);
|
|
122
|
+
const glyphColored = this.checked[i] || (!this.multiSelect && focused) ? theme.fg("accent", glyph) : glyph;
|
|
123
|
+
this.optionsContainer.addChild(new Text(`${cursor}${glyphColored} ${label}`, 1, 0));
|
|
124
|
+
}
|
|
125
|
+
if (this.fieldLabel) {
|
|
126
|
+
const focused = this.cursorOnField();
|
|
127
|
+
const prefix = focused ? theme.fg("accent", "→ ") : " ";
|
|
128
|
+
const labelText = this.options.length > 0 ? "Or type your own answer:" : "Your answer:";
|
|
129
|
+
this.fieldLabel.setText(prefix + (focused ? theme.fg("accent", labelText) : theme.fg("muted", labelText)));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
syncFieldFocus() {
|
|
133
|
+
const fieldFocused = this._focused && this.cursorOnField();
|
|
134
|
+
if (this.input)
|
|
135
|
+
this.input.focused = fieldFocused;
|
|
136
|
+
if (this.editor)
|
|
137
|
+
this.editor.focused = fieldFocused;
|
|
138
|
+
}
|
|
139
|
+
moveCursor(delta) {
|
|
140
|
+
const first = this.options.length > 0 ? 0 : this.freeTextRow;
|
|
141
|
+
const next = Math.max(first, Math.min(this.lastRow, this.cursorIndex + delta));
|
|
142
|
+
if (next === this.cursorIndex)
|
|
143
|
+
return;
|
|
144
|
+
this.cursorIndex = next;
|
|
145
|
+
this.renderRows();
|
|
146
|
+
this.syncFieldFocus();
|
|
147
|
+
}
|
|
148
|
+
fieldText(editorTextOverride) {
|
|
149
|
+
if (this.editor)
|
|
150
|
+
return (editorTextOverride ?? this.editor.getText()).trim();
|
|
151
|
+
if (this.input)
|
|
152
|
+
return this.input.getValue().trim();
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
155
|
+
currentAnswer(editorTextOverride) {
|
|
156
|
+
const customText = this.fieldText(editorTextOverride) || undefined;
|
|
157
|
+
if (this.multiSelect) {
|
|
158
|
+
const selected = this.options.filter((_, i) => this.checked[i]);
|
|
159
|
+
if (selected.length === 0 && !customText)
|
|
160
|
+
return undefined;
|
|
161
|
+
return { selected, customText };
|
|
162
|
+
}
|
|
163
|
+
if (this.cursorOnField()) {
|
|
164
|
+
if (!customText)
|
|
165
|
+
return undefined;
|
|
166
|
+
return { selected: [], customText };
|
|
167
|
+
}
|
|
168
|
+
const option = this.options[this.cursorIndex];
|
|
169
|
+
// Single-select: submit the highlighted option together with any typed
|
|
170
|
+
// free text, matching the Dashboard, which combines a radio selection with
|
|
171
|
+
// custom text (and the tool's own combined-answer result formatting).
|
|
172
|
+
return option ? { selected: [option], customText } : undefined;
|
|
173
|
+
}
|
|
174
|
+
submit(editorTextOverride) {
|
|
175
|
+
if (this.submitted)
|
|
176
|
+
return;
|
|
177
|
+
const answer = this.currentAnswer(editorTextOverride);
|
|
178
|
+
if (!answer)
|
|
179
|
+
return; // Nothing to submit yet — Esc stops the turn instead.
|
|
180
|
+
this.submitted = true;
|
|
181
|
+
this.onSubmitCallback(answer);
|
|
182
|
+
}
|
|
183
|
+
stop() {
|
|
184
|
+
if (this.submitted)
|
|
185
|
+
return;
|
|
186
|
+
this.submitted = true;
|
|
187
|
+
this.onStopCallback();
|
|
188
|
+
}
|
|
189
|
+
handleInput(keyData) {
|
|
190
|
+
const kb = getKeybindings();
|
|
191
|
+
if (kb.matches(keyData, "tui.select.cancel")) {
|
|
192
|
+
this.stop();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (this.cursorOnField()) {
|
|
196
|
+
// Multiline editor: Enter submits (via onSubmit), Shift+Enter inserts a
|
|
197
|
+
// newline, ↑ at the top line leaves to the options, everything else edits.
|
|
198
|
+
if (this.editor) {
|
|
199
|
+
if (kb.matches(keyData, "tui.select.up") && this.editor.getCursor().line === 0) {
|
|
200
|
+
this.moveCursor(-1);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
this.editor.handleInput(keyData);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Single-line input: ↑ leaves to the options, Enter submits, else edits.
|
|
207
|
+
if (this.input) {
|
|
208
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
209
|
+
this.moveCursor(-1);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (kb.matches(keyData, "tui.select.confirm") || keyData === "\n") {
|
|
213
|
+
this.submit();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
this.input.handleInput(keyData);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// Cursor on an option row.
|
|
222
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
223
|
+
this.moveCursor(-1);
|
|
224
|
+
}
|
|
225
|
+
else if (kb.matches(keyData, "tui.select.down")) {
|
|
226
|
+
this.moveCursor(1);
|
|
227
|
+
}
|
|
228
|
+
else if (this.multiSelect && keyData === " ") {
|
|
229
|
+
this.checked[this.cursorIndex] = !this.checked[this.cursorIndex];
|
|
230
|
+
this.renderRows();
|
|
231
|
+
}
|
|
232
|
+
else if (kb.matches(keyData, "tui.select.confirm") || keyData === "\n") {
|
|
233
|
+
this.submit();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
dispose() {
|
|
237
|
+
this.countdown?.dispose();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=ask-user.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ask-user.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/ask-user.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ask_user question component.\n *\n * Renders a clarifying question with optional single- or multi-select options\n * and an optional free-text field, matching the approved cross-surface design.\n *\n * Keyboard model (settled in issue 396 discussion):\n * ↑/↓ move a single cursor through the options and, last, the free-text field\n * Space toggle the highlighted checkbox (multi-select only)\n * Enter single-select: pick the highlighted option and submit;\n * free-text field: submit the typed answer;\n * multi-select: submit all checked options plus any free text\n * Shift+Enter insert a newline in the multiline free-text field\n * Esc stop the current agent turn\n */\n\nimport { Container, Editor, type Focusable, getKeybindings, Input, Markdown, Spacer, Text, type TUI } from \"@dreb/tui\";\nimport type { AskRequest, AskResult } from \"../../../core/extensions/types.js\";\nimport { getEditorTheme, getMarkdownTheme, theme } from \"../theme/theme.js\";\nimport { CountdownTimer } from \"./countdown-timer.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\nimport { keyHint, rawKeyHint } from \"./keybinding-hints.js\";\n\nexport interface AskUserComponentOptions {\n\ttui?: TUI;\n\ttimeout?: number;\n}\n\nexport class AskUserComponent extends Container implements Focusable {\n\tprivate options: string[];\n\tprivate allowFreeText: boolean;\n\tprivate multiSelect: boolean;\n\tprivate multiline: boolean;\n\n\t/** Cursor over [options..., freeTextRow?]. */\n\tprivate cursorIndex = 0;\n\t/** Checkbox state for multi-select, aligned to options. */\n\tprivate checked: boolean[];\n\n\tprivate onSubmitCallback: (result: AskResult) => void;\n\tprivate onStopCallback: () => void;\n\n\tprivate titleText: Text;\n\tprivate baseTitle: string;\n\tprivate optionsContainer: Container;\n\tprivate fieldLabel: Text | undefined;\n\tprivate input: Input | undefined;\n\tprivate editor: Editor | undefined;\n\tprivate countdown: CountdownTimer | undefined;\n\tprivate submitted = false;\n\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.syncFieldFocus();\n\t}\n\n\tprivate get freeTextRow(): number {\n\t\treturn this.allowFreeText ? this.options.length : -1;\n\t}\n\n\tprivate get lastRow(): number {\n\t\treturn this.allowFreeText ? this.options.length : this.options.length - 1;\n\t}\n\n\tprivate cursorOnField(): boolean {\n\t\treturn this.allowFreeText && this.cursorIndex === this.freeTextRow;\n\t}\n\n\tconstructor(\n\t\trequest: AskRequest,\n\t\tonSubmit: (result: AskResult) => void,\n\t\tonStop: () => void,\n\t\topts?: AskUserComponentOptions,\n\t) {\n\t\tsuper();\n\n\t\tthis.options = request.options ?? [];\n\t\tthis.allowFreeText = request.allowFreeText !== false;\n\t\tthis.multiSelect = request.multiSelect === true && this.options.length > 0;\n\t\tthis.multiline = request.multiline === true;\n\t\tthis.checked = this.options.map(() => false);\n\t\tthis.onSubmitCallback = onSubmit;\n\t\tthis.onStopCallback = onStop;\n\t\tthis.baseTitle = request.title?.trim() || \"Question\";\n\n\t\t// Start the cursor on the first option, or the free-text field when there\n\t\t// are no options.\n\t\tthis.cursorIndex = this.options.length > 0 ? 0 : this.freeTextRow;\n\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.titleText = new Text(theme.fg(\"accent\", theme.bold(this.baseTitle)), 1, 0);\n\t\tthis.addChild(this.titleText);\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.addChild(new Markdown(request.question, 1, 0, getMarkdownTheme(), undefined, true));\n\t\tthis.addChild(new Spacer(1));\n\n\t\tif (opts?.timeout && opts.timeout > 0 && opts.tui) {\n\t\t\tthis.countdown = new CountdownTimer(\n\t\t\t\topts.timeout,\n\t\t\t\topts.tui,\n\t\t\t\t(s) => this.titleText.setText(theme.fg(\"accent\", theme.bold(`${this.baseTitle} (${s}s)`))),\n\t\t\t\t() => this.stop(),\n\t\t\t);\n\t\t}\n\n\t\tthis.optionsContainer = new Container();\n\t\tthis.addChild(this.optionsContainer);\n\n\t\tif (this.allowFreeText) {\n\t\t\tthis.addChild(new Spacer(1));\n\t\t\tthis.fieldLabel = new Text(\"\", 1, 0);\n\t\t\tthis.addChild(this.fieldLabel);\n\t\t\tif (this.multiline && opts?.tui) {\n\t\t\t\tthis.editor = new Editor(opts.tui, getEditorTheme(), {});\n\t\t\t\t// The Editor clears its own state before invoking onSubmit, so we must\n\t\t\t\t// use the text it hands us rather than re-reading the now-empty editor.\n\t\t\t\tthis.editor.onSubmit = (text) => this.submit(text);\n\t\t\t\tthis.addChild(this.editor);\n\t\t\t} else {\n\t\t\t\tthis.input = new Input();\n\t\t\t\tthis.addChild(this.input);\n\t\t\t}\n\t\t}\n\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(this.buildHint(), 1, 0));\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\n\t\tthis.renderRows();\n\t\tthis.syncFieldFocus();\n\t}\n\n\tprivate buildHint(): string {\n\t\tconst parts = [rawKeyHint(\"↑↓\", \"move\")];\n\t\tif (this.multiSelect) parts.push(rawKeyHint(\"Space\", \"toggle\"));\n\t\tparts.push(keyHint(\"tui.select.confirm\", \"submit\"));\n\t\tif (this.multiline) parts.push(keyHint(\"tui.input.newLine\", \"newline\"));\n\t\tparts.push(keyHint(\"tui.select.cancel\", \"stop agent\"));\n\t\treturn parts.join(\" \");\n\t}\n\n\tprivate renderRows(): void {\n\t\tthis.optionsContainer.clear();\n\t\tfor (let i = 0; i < this.options.length; i++) {\n\t\t\tconst focused = this.cursorIndex === i;\n\t\t\tconst cursor = focused ? theme.fg(\"accent\", \"→ \") : \" \";\n\t\t\tconst glyph = this.multiSelect ? (this.checked[i] ? \"[x]\" : \"[ ]\") : focused ? \"(•)\" : \"( )\";\n\t\t\tconst label = focused ? theme.fg(\"accent\", this.options[i]) : theme.fg(\"text\", this.options[i]);\n\t\t\tconst glyphColored = this.checked[i] || (!this.multiSelect && focused) ? theme.fg(\"accent\", glyph) : glyph;\n\t\t\tthis.optionsContainer.addChild(new Text(`${cursor}${glyphColored} ${label}`, 1, 0));\n\t\t}\n\t\tif (this.fieldLabel) {\n\t\t\tconst focused = this.cursorOnField();\n\t\t\tconst prefix = focused ? theme.fg(\"accent\", \"→ \") : \" \";\n\t\t\tconst labelText = this.options.length > 0 ? \"Or type your own answer:\" : \"Your answer:\";\n\t\t\tthis.fieldLabel.setText(prefix + (focused ? theme.fg(\"accent\", labelText) : theme.fg(\"muted\", labelText)));\n\t\t}\n\t}\n\n\tprivate syncFieldFocus(): void {\n\t\tconst fieldFocused = this._focused && this.cursorOnField();\n\t\tif (this.input) this.input.focused = fieldFocused;\n\t\tif (this.editor) this.editor.focused = fieldFocused;\n\t}\n\n\tprivate moveCursor(delta: number): void {\n\t\tconst first = this.options.length > 0 ? 0 : this.freeTextRow;\n\t\tconst next = Math.max(first, Math.min(this.lastRow, this.cursorIndex + delta));\n\t\tif (next === this.cursorIndex) return;\n\t\tthis.cursorIndex = next;\n\t\tthis.renderRows();\n\t\tthis.syncFieldFocus();\n\t}\n\n\tprivate fieldText(editorTextOverride?: string): string {\n\t\tif (this.editor) return (editorTextOverride ?? this.editor.getText()).trim();\n\t\tif (this.input) return this.input.getValue().trim();\n\t\treturn \"\";\n\t}\n\n\tprivate currentAnswer(editorTextOverride?: string): AskResult | undefined {\n\t\tconst customText = this.fieldText(editorTextOverride) || undefined;\n\t\tif (this.multiSelect) {\n\t\t\tconst selected = this.options.filter((_, i) => this.checked[i]);\n\t\t\tif (selected.length === 0 && !customText) return undefined;\n\t\t\treturn { selected, customText };\n\t\t}\n\t\tif (this.cursorOnField()) {\n\t\t\tif (!customText) return undefined;\n\t\t\treturn { selected: [], customText };\n\t\t}\n\t\tconst option = this.options[this.cursorIndex];\n\t\t// Single-select: submit the highlighted option together with any typed\n\t\t// free text, matching the Dashboard, which combines a radio selection with\n\t\t// custom text (and the tool's own combined-answer result formatting).\n\t\treturn option ? { selected: [option], customText } : undefined;\n\t}\n\n\tprivate submit(editorTextOverride?: string): void {\n\t\tif (this.submitted) return;\n\t\tconst answer = this.currentAnswer(editorTextOverride);\n\t\tif (!answer) return; // Nothing to submit yet — Esc stops the turn instead.\n\t\tthis.submitted = true;\n\t\tthis.onSubmitCallback(answer);\n\t}\n\n\tprivate stop(): void {\n\t\tif (this.submitted) return;\n\t\tthis.submitted = true;\n\t\tthis.onStopCallback();\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getKeybindings();\n\n\t\tif (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\tthis.stop();\n\t\t\treturn;\n\t\t}\n\n\t\tif (this.cursorOnField()) {\n\t\t\t// Multiline editor: Enter submits (via onSubmit), Shift+Enter inserts a\n\t\t\t// newline, ↑ at the top line leaves to the options, everything else edits.\n\t\t\tif (this.editor) {\n\t\t\t\tif (kb.matches(keyData, \"tui.select.up\") && this.editor.getCursor().line === 0) {\n\t\t\t\t\tthis.moveCursor(-1);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.editor.handleInput(keyData);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Single-line input: ↑ leaves to the options, Enter submits, else edits.\n\t\t\tif (this.input) {\n\t\t\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\t\t\tthis.moveCursor(-1);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (kb.matches(keyData, \"tui.select.confirm\") || keyData === \"\\n\") {\n\t\t\t\t\tthis.submit();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.input.handleInput(keyData);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Cursor on an option row.\n\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\tthis.moveCursor(-1);\n\t\t} else if (kb.matches(keyData, \"tui.select.down\")) {\n\t\t\tthis.moveCursor(1);\n\t\t} else if (this.multiSelect && keyData === \" \") {\n\t\t\tthis.checked[this.cursorIndex] = !this.checked[this.cursorIndex];\n\t\t\tthis.renderRows();\n\t\t} else if (kb.matches(keyData, \"tui.select.confirm\") || keyData === \"\\n\") {\n\t\t\tthis.submit();\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.countdown?.dispose();\n\t}\n}\n"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@dreb/agent-core";
|
|
2
2
|
import type { Transport } from "@dreb/ai";
|
|
3
3
|
import { type Component, Container, SettingsList } from "@dreb/tui";
|
|
4
|
+
import type { SubagentArbiterSettings } from "../../../core/settings-manager.js";
|
|
4
5
|
export interface SettingsConfig {
|
|
5
6
|
autoCompact: boolean;
|
|
6
7
|
showImages: boolean;
|
|
@@ -33,6 +34,8 @@ export interface SettingsConfig {
|
|
|
33
34
|
agentNames: string[];
|
|
34
35
|
/** Available model IDs for selection in the ranked list */
|
|
35
36
|
availableModelIds: string[];
|
|
37
|
+
/** Global-only Dispatch Arbiter policy. */
|
|
38
|
+
subagentArbiter: SubagentArbiterSettings;
|
|
36
39
|
}
|
|
37
40
|
export interface SettingsCallbacks {
|
|
38
41
|
onAutoCompactChange: (enabled: boolean) => void;
|
|
@@ -56,6 +59,7 @@ export interface SettingsCallbacks {
|
|
|
56
59
|
onQuietStartupChange: (enabled: boolean) => void;
|
|
57
60
|
onAutoLoadNestedContextChange: (enabled: boolean) => void;
|
|
58
61
|
onAgentModelsChange: (agentName: string, models: string[]) => void;
|
|
62
|
+
onSubagentArbiterChange: (settings: SubagentArbiterSettings) => boolean | Promise<boolean>;
|
|
59
63
|
onCancel: () => void;
|
|
60
64
|
}
|
|
61
65
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/settings-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ThinkingLevel } from \"@dreb/agent-core\";\nimport type { Transport } from \"@dreb/ai\";\nimport {\n\ttype Component,\n\tContainer,\n\tgetCapabilities,\n\tgetKeybindings,\n\ttype RankedItem,\n\tRankedList,\n\ttype RankedListTheme,\n\ttype SelectItem,\n\tSelectList,\n\ttype SelectListLayoutOptions,\n\ttype SettingItem,\n\tSettingsList,\n\tSpacer,\n\tText,\n} from \"@dreb/tui\";\nimport { getSelectListTheme, getSettingsListTheme, theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\nconst SETTINGS_SUBMENU_SELECT_LIST_LAYOUT: SelectListLayoutOptions = {\n\tminPrimaryColumnWidth: 12,\n\tmaxPrimaryColumnWidth: 32,\n};\n\nconst THINKING_DESCRIPTIONS: Record<ThinkingLevel, string> = {\n\toff: \"No reasoning\",\n\tminimal: \"Very brief reasoning (~1k tokens)\",\n\tlow: \"Light reasoning (~2k tokens)\",\n\tmedium: \"Moderate reasoning (~8k tokens)\",\n\thigh: \"Deep reasoning (~16k tokens)\",\n\txhigh: \"Maximum reasoning (~32k tokens)\",\n};\n\nexport interface SettingsConfig {\n\tautoCompact: boolean;\n\tshowImages: boolean;\n\tautoResizeImages: boolean;\n\tblockImages: boolean;\n\tenableSkillCommands: boolean;\n\tsteeringMode: \"all\" | \"one-at-a-time\";\n\tfollowUpMode: \"all\" | \"one-at-a-time\";\n\ttransport: Transport;\n\tthinkingLevel: ThinkingLevel;\n\tavailableThinkingLevels: ThinkingLevel[];\n\tcurrentTheme: string;\n\tavailableThemes: string[];\n\thideThinkingBlock: boolean;\n\t/** Whether the current model supports adaptive thinking (gates the thinking-display toggle). */\n\tthinkingDisplaySupported: boolean;\n\t/** Effective thinking-display value for the current model (\"summarized\" shown as on). */\n\tthinkingDisplay: \"summarized\" | \"omitted\";\n\tdoubleEscapeAction: \"fork\" | \"tree\" | \"none\";\n\ttreeFilterMode: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\";\n\tshowHardwareCursor: boolean;\n\teditorPaddingX: number;\n\tautocompleteMaxVisible: number;\n\tquietStartup: boolean;\n\t/** Whether nested AGENTS.md/CLAUDE.md auto-loading is enabled. */\n\tautoLoadNestedContext: boolean;\n\t/** Per-agent model overrides from agentModels settings */\n\tagentModels: Record<string, string[]>;\n\t/** Known agent names for the mach6 models submenu */\n\tagentNames: string[];\n\t/** Available model IDs for selection in the ranked list */\n\tavailableModelIds: string[];\n}\n\nexport interface SettingsCallbacks {\n\tonAutoCompactChange: (enabled: boolean) => void;\n\tonShowImagesChange: (enabled: boolean) => void;\n\tonAutoResizeImagesChange: (enabled: boolean) => void;\n\tonBlockImagesChange: (blocked: boolean) => void;\n\tonEnableSkillCommandsChange: (enabled: boolean) => void;\n\tonSteeringModeChange: (mode: \"all\" | \"one-at-a-time\") => void;\n\tonFollowUpModeChange: (mode: \"all\" | \"one-at-a-time\") => void;\n\tonTransportChange: (transport: Transport) => void;\n\tonThinkingLevelChange: (level: ThinkingLevel) => void;\n\tonThemeChange: (theme: string) => void;\n\tonThemePreview?: (theme: string) => void;\n\tonHideThinkingBlockChange: (hidden: boolean) => void;\n\tonThinkingDisplayChange: (display: \"summarized\" | \"omitted\") => void;\n\tonDoubleEscapeActionChange: (action: \"fork\" | \"tree\" | \"none\") => void;\n\tonTreeFilterModeChange: (mode: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\") => void;\n\tonShowHardwareCursorChange: (enabled: boolean) => void;\n\tonEditorPaddingXChange: (padding: number) => void;\n\tonAutocompleteMaxVisibleChange: (maxVisible: number) => void;\n\tonQuietStartupChange: (enabled: boolean) => void;\n\tonAutoLoadNestedContextChange: (enabled: boolean) => void;\n\tonAgentModelsChange: (agentName: string, models: string[]) => void;\n\tonCancel: () => void;\n}\n\n/**\n * A submenu component for selecting from a list of options.\n */\nclass SelectSubmenu extends Container {\n\tprivate selectList: SelectList;\n\n\tconstructor(\n\t\ttitle: string,\n\t\tdescription: string,\n\t\toptions: SelectItem[],\n\t\tcurrentValue: string,\n\t\tonSelect: (value: string) => void,\n\t\tonCancel: () => void,\n\t\tonSelectionChange?: (value: string) => void,\n\t) {\n\t\tsuper();\n\n\t\t// Title\n\t\tthis.addChild(new Text(theme.bold(theme.fg(\"accent\", title)), 0, 0));\n\n\t\t// Description\n\t\tif (description) {\n\t\t\tthis.addChild(new Spacer(1));\n\t\t\tthis.addChild(new Text(theme.fg(\"muted\", description), 0, 0));\n\t\t}\n\n\t\t// Spacer\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Select list\n\t\tthis.selectList = new SelectList(\n\t\t\toptions,\n\t\t\tMath.min(options.length, 10),\n\t\t\tgetSelectListTheme(),\n\t\t\tSETTINGS_SUBMENU_SELECT_LIST_LAYOUT,\n\t\t);\n\n\t\t// Pre-select current value\n\t\tconst currentIndex = options.findIndex((o) => o.value === currentValue);\n\t\tif (currentIndex !== -1) {\n\t\t\tthis.selectList.setSelectedIndex(currentIndex);\n\t\t}\n\n\t\tthis.selectList.onSelect = (item) => {\n\t\t\tonSelect(item.value);\n\t\t};\n\n\t\tthis.selectList.onCancel = onCancel;\n\n\t\tif (onSelectionChange) {\n\t\t\tthis.selectList.onSelectionChange = (item) => {\n\t\t\t\tonSelectionChange(item.value);\n\t\t\t};\n\t\t}\n\n\t\tthis.addChild(this.selectList);\n\n\t\t// Hint\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.fg(\"dim\", \" Enter to select · Esc to go back\"), 0, 0));\n\t}\n\n\thandleInput(data: string): void {\n\t\tthis.selectList.handleInput(data);\n\t}\n}\n\nfunction getRankedListTheme(): RankedListTheme {\n\treturn {\n\t\tselectedText: (t: string) => theme.bold(t),\n\t\trank: (t: string) => theme.fg(\"muted\", t),\n\t\tdescription: (t: string) => theme.fg(\"dim\", t),\n\t\thint: (t: string) => theme.fg(\"dim\", t),\n\t\tempty: (t: string) => theme.fg(\"dim\", t),\n\t};\n}\n\n/**\n * Top-level Mach6 Models submenu. Shows a list of agents, selecting one opens\n * a RankedList editor for that agent's model fallback list.\n *\n * Navigation: Agent list → RankedList → (optional) Add Model picker\n */\nexport class AgentModelsSubmenu implements Component {\n\tprivate agentList: SelectList;\n\tprivate rankedList: RankedList | null = null;\n\tprivate addList: SelectList | null = null;\n\tprivate addListSearchQuery = \"\";\n\t// Transient notice shown in the ranked view when an add was attempted but no\n\t// models are available to add. Cleared on the next ranked-view input.\n\tprivate rankedNotice: string | null = null;\n\tprivate activeView: \"agents\" | \"ranked\" | \"add\" = \"agents\";\n\tprivate currentAgentName: string | null = null;\n\tprivate agentNames: string[];\n\tprivate agentModels: Record<string, string[]>;\n\tprivate availableModelIds: string[];\n\tprivate onModelsChange: (agentName: string, models: string[]) => void;\n\tprivate onCancel: () => void;\n\n\tconstructor(\n\t\tagentNames: string[],\n\t\tagentModels: Record<string, string[]>,\n\t\tavailableModelIds: string[],\n\t\tonModelsChange: (agentName: string, models: string[]) => void,\n\t\tonCancel: () => void,\n\t) {\n\t\tthis.agentNames = agentNames;\n\t\tthis.agentModels = agentModels;\n\t\tthis.availableModelIds = availableModelIds;\n\t\tthis.onModelsChange = onModelsChange;\n\t\tthis.onCancel = onCancel;\n\n\t\tthis.agentList = this.buildAgentList();\n\t}\n\n\tprivate buildAgentList(): SelectList {\n\t\tconst agentItems: SelectItem[] = this.agentNames.map((name) => {\n\t\t\tconst models = this.agentModels[name];\n\t\t\tconst desc = models && models.length > 0 ? models.join(\", \") : \"default\";\n\t\t\treturn { value: name, label: name, description: desc };\n\t\t});\n\n\t\tconst list = new SelectList(agentItems, Math.min(agentItems.length, 12), getSelectListTheme(), {\n\t\t\tminPrimaryColumnWidth: 20,\n\t\t\tmaxPrimaryColumnWidth: 30,\n\t\t});\n\n\t\tlist.onSelect = (item) => {\n\t\t\tthis.openRankedList(item.value);\n\t\t};\n\n\t\tlist.onCancel = this.onCancel;\n\t\treturn list;\n\t}\n\n\tprivate openRankedList(agentName: string): void {\n\t\tthis.currentAgentName = agentName;\n\t\tconst currentModels = this.agentModels[agentName] ?? [];\n\t\tconst items: RankedItem[] = currentModels.map((m) => ({ value: m, label: m }));\n\t\tthis.rankedList = new RankedList(items, 10, getRankedListTheme());\n\n\t\tthis.rankedList.onReorder = (reorderedItems) => {\n\t\t\tthis.saveModels(\n\t\t\t\tagentName,\n\t\t\t\treorderedItems.map((i) => i.value),\n\t\t\t);\n\t\t};\n\n\t\tthis.rankedList.onRemove = (_removed, remaining) => {\n\t\t\tthis.saveModels(\n\t\t\t\tagentName,\n\t\t\t\tremaining.map((i) => i.value),\n\t\t\t);\n\t\t};\n\n\t\tthis.rankedList.onSelect = () => {\n\t\t\tthis.openAddModelPicker();\n\t\t};\n\n\t\tthis.rankedList.onCancel = () => {\n\t\t\tthis.rankedList = null;\n\t\t\tthis.currentAgentName = null;\n\t\t\tthis.activeView = \"agents\";\n\t\t\t// Rebuild agent list so descriptions reflect any changes made\n\t\t\tthis.agentList = this.buildAgentList();\n\t\t};\n\n\t\tthis.activeView = \"ranked\";\n\t}\n\n\tprivate openAddModelPicker(): void {\n\t\tif (!this.rankedList) return;\n\n\t\tconst existingValues = new Set(this.rankedList.getItems().map((i) => i.value));\n\t\tconst allOptions = this.availableModelIds\n\t\t\t.filter((m) => !existingValues.has(m))\n\t\t\t.map((m) => ({ value: m, label: m }));\n\n\t\tif (allOptions.length === 0) {\n\t\t\t// Don't silently swallow the \"add\" intent — tell the user why nothing happened.\n\t\t\tthis.rankedNotice =\n\t\t\t\tthis.availableModelIds.length === 0\n\t\t\t\t\t? \"No models available to add (no authenticated providers)\"\n\t\t\t\t\t: \"All available models are already in the list\";\n\t\t\treturn;\n\t\t}\n\n\t\tthis.addList = new SelectList(\n\t\t\tallOptions,\n\t\t\tMath.min(allOptions.length, 10),\n\t\t\tgetSelectListTheme(),\n\t\t\tSETTINGS_SUBMENU_SELECT_LIST_LAYOUT,\n\t\t);\n\n\t\tthis.addListSearchQuery = \"\";\n\n\t\tthis.addList.onSelect = (item) => {\n\t\t\tif (!this.rankedList || !this.currentAgentName) return;\n\t\t\tconst newItems = [...this.rankedList.getItems(), { value: item.value, label: item.value }];\n\t\t\tthis.rankedList.setItems(newItems);\n\t\t\tthis.addList = null;\n\t\t\tthis.addListSearchQuery = \"\";\n\t\t\tthis.activeView = \"ranked\";\n\t\t\tthis.saveModels(\n\t\t\t\tthis.currentAgentName,\n\t\t\t\tnewItems.map((i) => i.value),\n\t\t\t);\n\t\t};\n\n\t\tthis.addList.onCancel = () => {\n\t\t\tthis.addList = null;\n\t\t\tthis.addListSearchQuery = \"\";\n\t\t\tthis.activeView = \"ranked\";\n\t\t};\n\n\t\tthis.activeView = \"add\";\n\t}\n\n\tprivate saveModels(agentName: string, models: string[]): void {\n\t\tthis.agentModels[agentName] = models;\n\t\tthis.onModelsChange(agentName, models);\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\tif (this.activeView === \"add\" && this.addList) {\n\t\t\tlines.push(theme.bold(theme.fg(\"accent\", \"Add Model\")));\n\t\t\tlines.push(\"\");\n\t\t\tconst searchDisplay = this.addListSearchQuery\n\t\t\t\t? ` Search: ${this.addListSearchQuery}▋`\n\t\t\t\t: theme.fg(\"dim\", \" Type to filter · ↑↓: navigate · Enter: add · Esc: back\");\n\t\t\tlines.push(searchDisplay);\n\t\t\tlines.push(\"\");\n\t\t\tlines.push(...this.addList.render(width));\n\t\t\treturn lines;\n\t\t}\n\n\t\tif (this.activeView === \"ranked\" && this.rankedList && this.currentAgentName) {\n\t\t\tlines.push(theme.bold(theme.fg(\"accent\", `Agent Models › ${this.currentAgentName}`)));\n\t\t\tlines.push(\"\");\n\t\t\tlines.push(theme.fg(\"muted\", \"Configure model fallback priority (first = preferred)\"));\n\t\t\tlines.push(\"\");\n\t\t\tlines.push(...this.rankedList.render(width));\n\t\t\tif (this.rankedNotice) {\n\t\t\t\tlines.push(\"\");\n\t\t\t\tlines.push(theme.fg(\"warning\", ` ${this.rankedNotice}`));\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Default: agent list\n\t\tlines.push(theme.bold(theme.fg(\"accent\", \"Agent Models\")));\n\t\tlines.push(\"\");\n\t\tlines.push(theme.fg(\"muted\", \"Select an agent to configure its model fallback list\"));\n\t\tlines.push(\"\");\n\t\tlines.push(...this.agentList.render(width));\n\t\tlines.push(\"\");\n\t\tlines.push(theme.fg(\"dim\", \" Enter to configure · Esc to go back\"));\n\t\treturn lines;\n\t}\n\n\thandleInput(data: string): void {\n\t\tif (this.activeView === \"add\" && this.addList) {\n\t\t\tconst kb = getKeybindings();\n\t\t\tif (\n\t\t\t\tkb.matches(data, \"tui.select.cancel\") ||\n\t\t\t\tkb.matches(data, \"tui.select.up\") ||\n\t\t\t\tkb.matches(data, \"tui.select.down\") ||\n\t\t\t\tkb.matches(data, \"tui.select.confirm\")\n\t\t\t) {\n\t\t\t\tthis.addList.handleInput(data);\n\t\t\t} else if (data === \"\\x7f\" || data === \"\\x08\") {\n\t\t\t\t// Backspace — remove last char from search\n\t\t\t\tthis.addListSearchQuery = this.addListSearchQuery.slice(0, -1);\n\t\t\t\tthis.addList.setFilter(this.addListSearchQuery);\n\t\t\t} else if (data.length === 1 && data >= \" \") {\n\t\t\t\t// Printable char — add to search\n\t\t\t\tthis.addListSearchQuery += data;\n\t\t\t\tthis.addList.setFilter(this.addListSearchQuery);\n\t\t\t}\n\t\t} else if (this.activeView === \"ranked\" && this.rankedList) {\n\t\t\t// Clear any stale \"nothing to add\" notice; openAddModelPicker re-sets it\n\t\t\t// if this input is another failed add attempt.\n\t\t\tthis.rankedNotice = null;\n\t\t\tthis.rankedList.handleInput(data);\n\t\t} else {\n\t\t\tthis.agentList.handleInput(data);\n\t\t}\n\t}\n}\n\n/**\n * Main settings selector component.\n */\nexport class SettingsSelectorComponent extends Container {\n\tprivate settingsList: SettingsList;\n\n\tconstructor(config: SettingsConfig, callbacks: SettingsCallbacks) {\n\t\tsuper();\n\n\t\tconst supportsImages = getCapabilities().images;\n\n\t\tconst items: SettingItem[] = [\n\t\t\t{\n\t\t\t\tid: \"autocompact\",\n\t\t\t\tlabel: \"Auto-compact\",\n\t\t\t\tdescription: \"Automatically compact context when it gets too large\",\n\t\t\t\tcurrentValue: config.autoCompact ? \"true\" : \"false\",\n\t\t\t\tvalues: [\"true\", \"false\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"auto-load-nested-context\",\n\t\t\t\tlabel: \"Unrestricted nested context loading (expert)\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"OFF by default. Expert-only: automatically loads AGENTS.md/CLAUDE.md from every tool target; untrusted files can contain prompt injection.\",\n\t\t\t\tcurrentValue: config.autoLoadNestedContext ? \"true\" : \"false\",\n\t\t\t\tvalues: [\"true\", \"false\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"steering-mode\",\n\t\t\t\tlabel: \"Steering mode\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Enter while streaming queues steering messages. 'one-at-a-time': deliver one, wait for response. 'all': deliver all at once.\",\n\t\t\t\tcurrentValue: config.steeringMode,\n\t\t\t\tvalues: [\"one-at-a-time\", \"all\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"follow-up-mode\",\n\t\t\t\tlabel: \"Follow-up mode\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Alt+Enter queues follow-up messages until agent stops. 'one-at-a-time': deliver one, wait for response. 'all': deliver all at once.\",\n\t\t\t\tcurrentValue: config.followUpMode,\n\t\t\t\tvalues: [\"one-at-a-time\", \"all\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"transport\",\n\t\t\t\tlabel: \"Transport\",\n\t\t\t\tdescription: \"Preferred transport for providers that support multiple transports\",\n\t\t\t\tcurrentValue: config.transport,\n\t\t\t\tvalues: [\"sse\", \"websocket\", \"auto\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"hide-thinking\",\n\t\t\t\tlabel: \"Hide thinking\",\n\t\t\t\tdescription: \"Hide thinking blocks in assistant responses\",\n\t\t\t\tcurrentValue: config.hideThinkingBlock ? \"true\" : \"false\",\n\t\t\t\tvalues: [\"true\", \"false\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"quiet-startup\",\n\t\t\t\tlabel: \"Quiet startup\",\n\t\t\t\tdescription: \"Disable verbose printing at startup\",\n\t\t\t\tcurrentValue: config.quietStartup ? \"true\" : \"false\",\n\t\t\t\tvalues: [\"true\", \"false\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"double-escape-action\",\n\t\t\t\tlabel: \"Double-escape action\",\n\t\t\t\tdescription: \"Action when pressing Escape twice with empty editor\",\n\t\t\t\tcurrentValue: config.doubleEscapeAction,\n\t\t\t\tvalues: [\"tree\", \"fork\", \"none\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"tree-filter-mode\",\n\t\t\t\tlabel: \"Tree filter mode\",\n\t\t\t\tdescription: \"Default filter when opening /tree\",\n\t\t\t\tcurrentValue: config.treeFilterMode,\n\t\t\t\tvalues: [\"default\", \"no-tools\", \"user-only\", \"labeled-only\", \"all\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"thinking\",\n\t\t\t\tlabel: \"Thinking level\",\n\t\t\t\tdescription: \"Reasoning depth for thinking-capable models\",\n\t\t\t\tcurrentValue: config.thinkingLevel,\n\t\t\t\tsubmenu: (currentValue, done) =>\n\t\t\t\t\tnew SelectSubmenu(\n\t\t\t\t\t\t\"Thinking Level\",\n\t\t\t\t\t\t\"Select reasoning depth for thinking-capable models\",\n\t\t\t\t\t\tconfig.availableThinkingLevels.map((level) => ({\n\t\t\t\t\t\t\tvalue: level,\n\t\t\t\t\t\t\tlabel: level,\n\t\t\t\t\t\t\tdescription: THINKING_DESCRIPTIONS[level],\n\t\t\t\t\t\t})),\n\t\t\t\t\t\tcurrentValue,\n\t\t\t\t\t\t(value) => {\n\t\t\t\t\t\t\tcallbacks.onThinkingLevelChange(value as ThinkingLevel);\n\t\t\t\t\t\t\tdone(value);\n\t\t\t\t\t\t},\n\t\t\t\t\t\t() => done(),\n\t\t\t\t\t),\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"theme\",\n\t\t\t\tlabel: \"Theme\",\n\t\t\t\tdescription: \"Color theme for the interface\",\n\t\t\t\tcurrentValue: config.currentTheme,\n\t\t\t\tsubmenu: (currentValue, done) =>\n\t\t\t\t\tnew SelectSubmenu(\n\t\t\t\t\t\t\"Theme\",\n\t\t\t\t\t\t\"Select color theme\",\n\t\t\t\t\t\tconfig.availableThemes.map((t) => ({\n\t\t\t\t\t\t\tvalue: t,\n\t\t\t\t\t\t\tlabel: t,\n\t\t\t\t\t\t})),\n\t\t\t\t\t\tcurrentValue,\n\t\t\t\t\t\t(value) => {\n\t\t\t\t\t\t\tcallbacks.onThemeChange(value);\n\t\t\t\t\t\t\tdone(value);\n\t\t\t\t\t\t},\n\t\t\t\t\t\t() => {\n\t\t\t\t\t\t\t// Restore original theme on cancel\n\t\t\t\t\t\t\tcallbacks.onThemePreview?.(currentValue);\n\t\t\t\t\t\t\tdone();\n\t\t\t\t\t\t},\n\t\t\t\t\t\t(value) => {\n\t\t\t\t\t\t\t// Preview theme on selection change\n\t\t\t\t\t\t\tcallbacks.onThemePreview?.(value);\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t},\n\t\t];\n\n\t\t// Thinking-display toggle for the current model. Only adaptive-thinking\n\t\t// Opus/Sonnet 4.6–4.x and Claude 5 families honor this field.\n\t\t// Shown as on (\"summarized\") by default; off (\"omitted\") hides thinking for lower latency.\n\t\tif (config.thinkingDisplaySupported) {\n\t\t\tconst hideThinkingIndex = items.findIndex((item) => item.id === \"hide-thinking\");\n\t\t\titems.splice(hideThinkingIndex + 1, 0, {\n\t\t\t\tid: \"thinking-display\",\n\t\t\t\tlabel: \"Show thinking summary\",\n\t\t\t\tdescription: \"Show summarized thinking for this model (off hides it for lower latency)\",\n\t\t\t\tcurrentValue: config.thinkingDisplay === \"summarized\" ? \"true\" : \"false\",\n\t\t\t\tvalues: [\"true\", \"false\"],\n\t\t\t});\n\t\t}\n\n\t\t// Single \"Agent Models\" entry that opens the agent picker submenu\n\t\tif (config.agentNames.length > 0) {\n\t\t\titems.push({\n\t\t\t\tid: \"agent-models\",\n\t\t\t\tlabel: \"Agent Models\",\n\t\t\t\tdescription: \"Configure model fallback lists for subagents\",\n\t\t\t\tcurrentValue: \"\",\n\t\t\t\tsubmenu: (_currentValue, done) =>\n\t\t\t\t\tnew AgentModelsSubmenu(\n\t\t\t\t\t\tconfig.agentNames,\n\t\t\t\t\t\tconfig.agentModels,\n\t\t\t\t\t\tconfig.availableModelIds,\n\t\t\t\t\t\t(agentName, models) => {\n\t\t\t\t\t\t\tcallbacks.onAgentModelsChange(agentName, models);\n\t\t\t\t\t\t},\n\t\t\t\t\t\t() => done(),\n\t\t\t\t\t),\n\t\t\t});\n\t\t}\n\n\t\t// Only show image toggle if terminal supports it\n\t\tif (supportsImages) {\n\t\t\t// Insert after autocompact\n\t\t\titems.splice(1, 0, {\n\t\t\t\tid: \"show-images\",\n\t\t\t\tlabel: \"Show images\",\n\t\t\t\tdescription: \"Render images inline in terminal\",\n\t\t\t\tcurrentValue: config.showImages ? \"true\" : \"false\",\n\t\t\t\tvalues: [\"true\", \"false\"],\n\t\t\t});\n\t\t}\n\n\t\t// Image auto-resize toggle (always available, affects both attached and read images)\n\t\titems.splice(supportsImages ? 2 : 1, 0, {\n\t\t\tid: \"auto-resize-images\",\n\t\t\tlabel: \"Auto-resize images\",\n\t\t\tdescription: \"Resize large images to 2000x2000 max for better model compatibility\",\n\t\t\tcurrentValue: config.autoResizeImages ? \"true\" : \"false\",\n\t\t\tvalues: [\"true\", \"false\"],\n\t\t});\n\n\t\t// Block images toggle (always available, insert after auto-resize-images)\n\t\tconst autoResizeIndex = items.findIndex((item) => item.id === \"auto-resize-images\");\n\t\titems.splice(autoResizeIndex + 1, 0, {\n\t\t\tid: \"block-images\",\n\t\t\tlabel: \"Block images\",\n\t\t\tdescription: \"Prevent images from being sent to LLM providers\",\n\t\t\tcurrentValue: config.blockImages ? \"true\" : \"false\",\n\t\t\tvalues: [\"true\", \"false\"],\n\t\t});\n\n\t\t// Skill commands toggle (insert after block-images)\n\t\tconst blockImagesIndex = items.findIndex((item) => item.id === \"block-images\");\n\t\titems.splice(blockImagesIndex + 1, 0, {\n\t\t\tid: \"skill-commands\",\n\t\t\tlabel: \"Skill commands\",\n\t\t\tdescription: \"Register skills as /skill:name commands\",\n\t\t\tcurrentValue: config.enableSkillCommands ? \"true\" : \"false\",\n\t\t\tvalues: [\"true\", \"false\"],\n\t\t});\n\n\t\t// Hardware cursor toggle (insert after skill-commands)\n\t\tconst skillCommandsIndex = items.findIndex((item) => item.id === \"skill-commands\");\n\t\titems.splice(skillCommandsIndex + 1, 0, {\n\t\t\tid: \"show-hardware-cursor\",\n\t\t\tlabel: \"Show hardware cursor\",\n\t\t\tdescription: \"Show the terminal cursor while still positioning it for IME support\",\n\t\t\tcurrentValue: config.showHardwareCursor ? \"true\" : \"false\",\n\t\t\tvalues: [\"true\", \"false\"],\n\t\t});\n\n\t\t// Editor padding toggle (insert after show-hardware-cursor)\n\t\tconst hardwareCursorIndex = items.findIndex((item) => item.id === \"show-hardware-cursor\");\n\t\titems.splice(hardwareCursorIndex + 1, 0, {\n\t\t\tid: \"editor-padding\",\n\t\t\tlabel: \"Editor padding\",\n\t\t\tdescription: \"Horizontal padding for input editor (0-3)\",\n\t\t\tcurrentValue: String(config.editorPaddingX),\n\t\t\tvalues: [\"0\", \"1\", \"2\", \"3\"],\n\t\t});\n\n\t\t// Autocomplete max visible toggle (insert after editor-padding)\n\t\tconst editorPaddingIndex = items.findIndex((item) => item.id === \"editor-padding\");\n\t\titems.splice(editorPaddingIndex + 1, 0, {\n\t\t\tid: \"autocomplete-max-visible\",\n\t\t\tlabel: \"Autocomplete max items\",\n\t\t\tdescription: \"Max visible items in autocomplete dropdown (3-20)\",\n\t\t\tcurrentValue: String(config.autocompleteMaxVisible),\n\t\t\tvalues: [\"3\", \"5\", \"7\", \"10\", \"15\", \"20\"],\n\t\t});\n\n\t\t// Add borders\n\t\tthis.addChild(new DynamicBorder());\n\n\t\tthis.settingsList = new SettingsList(\n\t\t\titems,\n\t\t\t10,\n\t\t\tgetSettingsListTheme(),\n\t\t\t(id, newValue) => {\n\t\t\t\tswitch (id) {\n\t\t\t\t\tcase \"autocompact\":\n\t\t\t\t\t\tcallbacks.onAutoCompactChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"auto-load-nested-context\":\n\t\t\t\t\t\tcallbacks.onAutoLoadNestedContextChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"show-images\":\n\t\t\t\t\t\tcallbacks.onShowImagesChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"auto-resize-images\":\n\t\t\t\t\t\tcallbacks.onAutoResizeImagesChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"block-images\":\n\t\t\t\t\t\tcallbacks.onBlockImagesChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"skill-commands\":\n\t\t\t\t\t\tcallbacks.onEnableSkillCommandsChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"steering-mode\":\n\t\t\t\t\t\tcallbacks.onSteeringModeChange(newValue as \"all\" | \"one-at-a-time\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"follow-up-mode\":\n\t\t\t\t\t\tcallbacks.onFollowUpModeChange(newValue as \"all\" | \"one-at-a-time\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"transport\":\n\t\t\t\t\t\tcallbacks.onTransportChange(newValue as Transport);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"hide-thinking\":\n\t\t\t\t\t\tcallbacks.onHideThinkingBlockChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"thinking-display\":\n\t\t\t\t\t\tcallbacks.onThinkingDisplayChange(newValue === \"true\" ? \"summarized\" : \"omitted\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"quiet-startup\":\n\t\t\t\t\t\tcallbacks.onQuietStartupChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"double-escape-action\":\n\t\t\t\t\t\tcallbacks.onDoubleEscapeActionChange(newValue as \"fork\" | \"tree\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"tree-filter-mode\":\n\t\t\t\t\t\tcallbacks.onTreeFilterModeChange(\n\t\t\t\t\t\t\tnewValue as \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\",\n\t\t\t\t\t\t);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"show-hardware-cursor\":\n\t\t\t\t\t\tcallbacks.onShowHardwareCursorChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"editor-padding\":\n\t\t\t\t\t\tcallbacks.onEditorPaddingXChange(parseInt(newValue, 10));\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"autocomplete-max-visible\":\n\t\t\t\t\t\tcallbacks.onAutocompleteMaxVisibleChange(parseInt(newValue, 10));\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t},\n\t\t\tcallbacks.onCancel,\n\t\t\t{ enableSearch: true },\n\t\t);\n\n\t\tthis.addChild(this.settingsList);\n\t\tthis.addChild(new DynamicBorder());\n\t}\n\n\tgetSettingsList(): SettingsList {\n\t\treturn this.settingsList;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"settings-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/settings-selector.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ThinkingLevel } from \"@dreb/agent-core\";\nimport type { Transport } from \"@dreb/ai\";\nimport {\n\ttype Component,\n\tContainer,\n\ttype Focusable,\n\tgetCapabilities,\n\tgetKeybindings,\n\tInput,\n\ttype RankedItem,\n\tRankedList,\n\ttype RankedListTheme,\n\ttype SelectItem,\n\tSelectList,\n\ttype SelectListLayoutOptions,\n\ttype SettingItem,\n\tSettingsList,\n\tSpacer,\n\tText,\n} from \"@dreb/tui\";\nimport type { SubagentArbiterSettings } from \"../../../core/settings-manager.js\";\nimport { getSelectListTheme, getSettingsListTheme, theme } from \"../theme/theme.js\";\nimport { DynamicBorder } from \"./dynamic-border.js\";\n\nconst SETTINGS_SUBMENU_SELECT_LIST_LAYOUT: SelectListLayoutOptions = {\n\tminPrimaryColumnWidth: 12,\n\tmaxPrimaryColumnWidth: 32,\n};\n\nconst THINKING_DESCRIPTIONS: Record<ThinkingLevel, string> = {\n\toff: \"No reasoning\",\n\tminimal: \"Very brief reasoning (~1k tokens)\",\n\tlow: \"Light reasoning (~2k tokens)\",\n\tmedium: \"Moderate reasoning (~8k tokens)\",\n\thigh: \"Deep reasoning (~16k tokens)\",\n\txhigh: \"Maximum reasoning (~32k tokens)\",\n};\n\nexport interface SettingsConfig {\n\tautoCompact: boolean;\n\tshowImages: boolean;\n\tautoResizeImages: boolean;\n\tblockImages: boolean;\n\tenableSkillCommands: boolean;\n\tsteeringMode: \"all\" | \"one-at-a-time\";\n\tfollowUpMode: \"all\" | \"one-at-a-time\";\n\ttransport: Transport;\n\tthinkingLevel: ThinkingLevel;\n\tavailableThinkingLevels: ThinkingLevel[];\n\tcurrentTheme: string;\n\tavailableThemes: string[];\n\thideThinkingBlock: boolean;\n\t/** Whether the current model supports adaptive thinking (gates the thinking-display toggle). */\n\tthinkingDisplaySupported: boolean;\n\t/** Effective thinking-display value for the current model (\"summarized\" shown as on). */\n\tthinkingDisplay: \"summarized\" | \"omitted\";\n\tdoubleEscapeAction: \"fork\" | \"tree\" | \"none\";\n\ttreeFilterMode: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\";\n\tshowHardwareCursor: boolean;\n\teditorPaddingX: number;\n\tautocompleteMaxVisible: number;\n\tquietStartup: boolean;\n\t/** Whether nested AGENTS.md/CLAUDE.md auto-loading is enabled. */\n\tautoLoadNestedContext: boolean;\n\t/** Per-agent model overrides from agentModels settings */\n\tagentModels: Record<string, string[]>;\n\t/** Known agent names for the mach6 models submenu */\n\tagentNames: string[];\n\t/** Available model IDs for selection in the ranked list */\n\tavailableModelIds: string[];\n\t/** Global-only Dispatch Arbiter policy. */\n\tsubagentArbiter: SubagentArbiterSettings;\n}\n\nexport interface SettingsCallbacks {\n\tonAutoCompactChange: (enabled: boolean) => void;\n\tonShowImagesChange: (enabled: boolean) => void;\n\tonAutoResizeImagesChange: (enabled: boolean) => void;\n\tonBlockImagesChange: (blocked: boolean) => void;\n\tonEnableSkillCommandsChange: (enabled: boolean) => void;\n\tonSteeringModeChange: (mode: \"all\" | \"one-at-a-time\") => void;\n\tonFollowUpModeChange: (mode: \"all\" | \"one-at-a-time\") => void;\n\tonTransportChange: (transport: Transport) => void;\n\tonThinkingLevelChange: (level: ThinkingLevel) => void;\n\tonThemeChange: (theme: string) => void;\n\tonThemePreview?: (theme: string) => void;\n\tonHideThinkingBlockChange: (hidden: boolean) => void;\n\tonThinkingDisplayChange: (display: \"summarized\" | \"omitted\") => void;\n\tonDoubleEscapeActionChange: (action: \"fork\" | \"tree\" | \"none\") => void;\n\tonTreeFilterModeChange: (mode: \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\") => void;\n\tonShowHardwareCursorChange: (enabled: boolean) => void;\n\tonEditorPaddingXChange: (padding: number) => void;\n\tonAutocompleteMaxVisibleChange: (maxVisible: number) => void;\n\tonQuietStartupChange: (enabled: boolean) => void;\n\tonAutoLoadNestedContextChange: (enabled: boolean) => void;\n\tonAgentModelsChange: (agentName: string, models: string[]) => void;\n\tonSubagentArbiterChange: (settings: SubagentArbiterSettings) => boolean | Promise<boolean>;\n\tonCancel: () => void;\n}\n\n/**\n * A submenu component for selecting from a list of options.\n */\nclass SelectSubmenu extends Container {\n\tprivate selectList: SelectList;\n\n\tconstructor(\n\t\ttitle: string,\n\t\tdescription: string,\n\t\toptions: SelectItem[],\n\t\tcurrentValue: string,\n\t\tonSelect: (value: string) => void,\n\t\tonCancel: () => void,\n\t\tonSelectionChange?: (value: string) => void,\n\t) {\n\t\tsuper();\n\n\t\t// Title\n\t\tthis.addChild(new Text(theme.bold(theme.fg(\"accent\", title)), 0, 0));\n\n\t\t// Description\n\t\tif (description) {\n\t\t\tthis.addChild(new Spacer(1));\n\t\t\tthis.addChild(new Text(theme.fg(\"muted\", description), 0, 0));\n\t\t}\n\n\t\t// Spacer\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Select list\n\t\tthis.selectList = new SelectList(\n\t\t\toptions,\n\t\t\tMath.min(options.length, 10),\n\t\t\tgetSelectListTheme(),\n\t\t\tSETTINGS_SUBMENU_SELECT_LIST_LAYOUT,\n\t\t);\n\n\t\t// Pre-select current value\n\t\tconst currentIndex = options.findIndex((o) => o.value === currentValue);\n\t\tif (currentIndex !== -1) {\n\t\t\tthis.selectList.setSelectedIndex(currentIndex);\n\t\t}\n\n\t\tthis.selectList.onSelect = (item) => {\n\t\t\tonSelect(item.value);\n\t\t};\n\n\t\tthis.selectList.onCancel = onCancel;\n\n\t\tif (onSelectionChange) {\n\t\t\tthis.selectList.onSelectionChange = (item) => {\n\t\t\t\tonSelectionChange(item.value);\n\t\t\t};\n\t\t}\n\n\t\tthis.addChild(this.selectList);\n\n\t\t// Hint\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.fg(\"dim\", \" Enter to select · Esc to go back\"), 0, 0));\n\t}\n\n\thandleInput(data: string): void {\n\t\tthis.selectList.handleInput(data);\n\t}\n}\n\nclass TextInputSubmenu extends Container implements Focusable {\n\tprivate readonly input = new Input();\n\tprivate readonly onSubmit: (value: string) => void;\n\tprivate readonly onCancel: () => void;\n\tprivate _focused = false;\n\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.input.focused = value;\n\t}\n\n\tconstructor(\n\t\ttitle: string,\n\t\tdescription: string,\n\t\tcurrentValue: string,\n\t\tonSubmit: (value: string) => void,\n\t\tonCancel: () => void,\n\t) {\n\t\tsuper();\n\t\tthis.onSubmit = onSubmit;\n\t\tthis.onCancel = onCancel;\n\t\tthis.addChild(new Text(theme.bold(theme.fg(\"accent\", title)), 0, 0));\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.fg(\"muted\", description), 0, 0));\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.input.setValue(currentValue);\n\t\tthis.addChild(this.input);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.fg(\"dim\", \" Enter to save · Esc to go back\"), 0, 0));\n\t}\n\n\thandleInput(data: string): void {\n\t\tconst kb = getKeybindings();\n\t\tif (kb.matches(data, \"tui.select.confirm\") || data === \"\\n\") this.onSubmit(this.input.getValue());\n\t\telse if (kb.matches(data, \"tui.select.cancel\")) this.onCancel();\n\t\telse this.input.handleInput(data);\n\t}\n}\n\nfunction getRankedListTheme(): RankedListTheme {\n\treturn {\n\t\tselectedText: (t: string) => theme.bold(t),\n\t\trank: (t: string) => theme.fg(\"muted\", t),\n\t\tdescription: (t: string) => theme.fg(\"dim\", t),\n\t\thint: (t: string) => theme.fg(\"dim\", t),\n\t\tempty: (t: string) => theme.fg(\"dim\", t),\n\t};\n}\n\n/**\n * Top-level Mach6 Models submenu. Shows a list of agents, selecting one opens\n * a RankedList editor for that agent's model fallback list.\n *\n * Navigation: Agent list → RankedList → (optional) Add Model picker\n */\nexport class AgentModelsSubmenu implements Component {\n\tprivate agentList: SelectList;\n\tprivate rankedList: RankedList | null = null;\n\tprivate addList: SelectList | null = null;\n\tprivate addListSearchQuery = \"\";\n\t// Transient notice shown in the ranked view when an add was attempted but no\n\t// models are available to add. Cleared on the next ranked-view input.\n\tprivate rankedNotice: string | null = null;\n\tprivate activeView: \"agents\" | \"ranked\" | \"add\" = \"agents\";\n\tprivate currentAgentName: string | null = null;\n\tprivate agentNames: string[];\n\tprivate agentModels: Record<string, string[]>;\n\tprivate availableModelIds: string[];\n\tprivate onModelsChange: (agentName: string, models: string[]) => void;\n\tprivate onCancel: () => void;\n\n\tconstructor(\n\t\tagentNames: string[],\n\t\tagentModels: Record<string, string[]>,\n\t\tavailableModelIds: string[],\n\t\tonModelsChange: (agentName: string, models: string[]) => void,\n\t\tonCancel: () => void,\n\t) {\n\t\tthis.agentNames = agentNames;\n\t\tthis.agentModels = agentModels;\n\t\tthis.availableModelIds = availableModelIds;\n\t\tthis.onModelsChange = onModelsChange;\n\t\tthis.onCancel = onCancel;\n\n\t\tthis.agentList = this.buildAgentList();\n\t}\n\n\tprivate buildAgentList(): SelectList {\n\t\tconst agentItems: SelectItem[] = this.agentNames.map((name) => {\n\t\t\tconst models = this.agentModels[name];\n\t\t\tconst desc = models && models.length > 0 ? models.join(\", \") : \"default\";\n\t\t\treturn { value: name, label: name, description: desc };\n\t\t});\n\n\t\tconst list = new SelectList(agentItems, Math.min(agentItems.length, 12), getSelectListTheme(), {\n\t\t\tminPrimaryColumnWidth: 20,\n\t\t\tmaxPrimaryColumnWidth: 30,\n\t\t});\n\n\t\tlist.onSelect = (item) => {\n\t\t\tthis.openRankedList(item.value);\n\t\t};\n\n\t\tlist.onCancel = this.onCancel;\n\t\treturn list;\n\t}\n\n\tprivate openRankedList(agentName: string): void {\n\t\tthis.currentAgentName = agentName;\n\t\tconst currentModels = this.agentModels[agentName] ?? [];\n\t\tconst items: RankedItem[] = currentModels.map((m) => ({ value: m, label: m }));\n\t\tthis.rankedList = new RankedList(items, 10, getRankedListTheme());\n\n\t\tthis.rankedList.onReorder = (reorderedItems) => {\n\t\t\tthis.saveModels(\n\t\t\t\tagentName,\n\t\t\t\treorderedItems.map((i) => i.value),\n\t\t\t);\n\t\t};\n\n\t\tthis.rankedList.onRemove = (_removed, remaining) => {\n\t\t\tthis.saveModels(\n\t\t\t\tagentName,\n\t\t\t\tremaining.map((i) => i.value),\n\t\t\t);\n\t\t};\n\n\t\tthis.rankedList.onSelect = () => {\n\t\t\tthis.openAddModelPicker();\n\t\t};\n\n\t\tthis.rankedList.onCancel = () => {\n\t\t\tthis.rankedList = null;\n\t\t\tthis.currentAgentName = null;\n\t\t\tthis.activeView = \"agents\";\n\t\t\t// Rebuild agent list so descriptions reflect any changes made\n\t\t\tthis.agentList = this.buildAgentList();\n\t\t};\n\n\t\tthis.activeView = \"ranked\";\n\t}\n\n\tprivate openAddModelPicker(): void {\n\t\tif (!this.rankedList) return;\n\n\t\tconst existingValues = new Set(this.rankedList.getItems().map((i) => i.value));\n\t\tconst allOptions = this.availableModelIds\n\t\t\t.filter((m) => !existingValues.has(m))\n\t\t\t.map((m) => ({ value: m, label: m }));\n\n\t\tif (allOptions.length === 0) {\n\t\t\t// Don't silently swallow the \"add\" intent — tell the user why nothing happened.\n\t\t\tthis.rankedNotice =\n\t\t\t\tthis.availableModelIds.length === 0\n\t\t\t\t\t? \"No models available to add (no authenticated providers)\"\n\t\t\t\t\t: \"All available models are already in the list\";\n\t\t\treturn;\n\t\t}\n\n\t\tthis.addList = new SelectList(\n\t\t\tallOptions,\n\t\t\tMath.min(allOptions.length, 10),\n\t\t\tgetSelectListTheme(),\n\t\t\tSETTINGS_SUBMENU_SELECT_LIST_LAYOUT,\n\t\t);\n\n\t\tthis.addListSearchQuery = \"\";\n\n\t\tthis.addList.onSelect = (item) => {\n\t\t\tif (!this.rankedList || !this.currentAgentName) return;\n\t\t\tconst newItems = [...this.rankedList.getItems(), { value: item.value, label: item.value }];\n\t\t\tthis.rankedList.setItems(newItems);\n\t\t\tthis.addList = null;\n\t\t\tthis.addListSearchQuery = \"\";\n\t\t\tthis.activeView = \"ranked\";\n\t\t\tthis.saveModels(\n\t\t\t\tthis.currentAgentName,\n\t\t\t\tnewItems.map((i) => i.value),\n\t\t\t);\n\t\t};\n\n\t\tthis.addList.onCancel = () => {\n\t\t\tthis.addList = null;\n\t\t\tthis.addListSearchQuery = \"\";\n\t\t\tthis.activeView = \"ranked\";\n\t\t};\n\n\t\tthis.activeView = \"add\";\n\t}\n\n\tprivate saveModels(agentName: string, models: string[]): void {\n\t\tthis.agentModels[agentName] = models;\n\t\tthis.onModelsChange(agentName, models);\n\t}\n\n\tinvalidate(): void {}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\n\t\tif (this.activeView === \"add\" && this.addList) {\n\t\t\tlines.push(theme.bold(theme.fg(\"accent\", \"Add Model\")));\n\t\t\tlines.push(\"\");\n\t\t\tconst searchDisplay = this.addListSearchQuery\n\t\t\t\t? ` Search: ${this.addListSearchQuery}▋`\n\t\t\t\t: theme.fg(\"dim\", \" Type to filter · ↑↓: navigate · Enter: add · Esc: back\");\n\t\t\tlines.push(searchDisplay);\n\t\t\tlines.push(\"\");\n\t\t\tlines.push(...this.addList.render(width));\n\t\t\treturn lines;\n\t\t}\n\n\t\tif (this.activeView === \"ranked\" && this.rankedList && this.currentAgentName) {\n\t\t\tlines.push(theme.bold(theme.fg(\"accent\", `Agent Models › ${this.currentAgentName}`)));\n\t\t\tlines.push(\"\");\n\t\t\tlines.push(theme.fg(\"muted\", \"Configure model fallback priority (first = preferred)\"));\n\t\t\tlines.push(\"\");\n\t\t\tlines.push(...this.rankedList.render(width));\n\t\t\tif (this.rankedNotice) {\n\t\t\t\tlines.push(\"\");\n\t\t\t\tlines.push(theme.fg(\"warning\", ` ${this.rankedNotice}`));\n\t\t\t}\n\t\t\treturn lines;\n\t\t}\n\n\t\t// Default: agent list\n\t\tlines.push(theme.bold(theme.fg(\"accent\", \"Agent Models\")));\n\t\tlines.push(\"\");\n\t\tlines.push(theme.fg(\"muted\", \"Select an agent to configure its model fallback list\"));\n\t\tlines.push(\"\");\n\t\tlines.push(...this.agentList.render(width));\n\t\tlines.push(\"\");\n\t\tlines.push(theme.fg(\"dim\", \" Enter to configure · Esc to go back\"));\n\t\treturn lines;\n\t}\n\n\thandleInput(data: string): void {\n\t\tif (this.activeView === \"add\" && this.addList) {\n\t\t\tconst kb = getKeybindings();\n\t\t\tif (\n\t\t\t\tkb.matches(data, \"tui.select.cancel\") ||\n\t\t\t\tkb.matches(data, \"tui.select.up\") ||\n\t\t\t\tkb.matches(data, \"tui.select.down\") ||\n\t\t\t\tkb.matches(data, \"tui.select.confirm\")\n\t\t\t) {\n\t\t\t\tthis.addList.handleInput(data);\n\t\t\t} else if (data === \"\\x7f\" || data === \"\\x08\") {\n\t\t\t\t// Backspace — remove last char from search\n\t\t\t\tthis.addListSearchQuery = this.addListSearchQuery.slice(0, -1);\n\t\t\t\tthis.addList.setFilter(this.addListSearchQuery);\n\t\t\t} else if (data.length === 1 && data >= \" \") {\n\t\t\t\t// Printable char — add to search\n\t\t\t\tthis.addListSearchQuery += data;\n\t\t\t\tthis.addList.setFilter(this.addListSearchQuery);\n\t\t\t}\n\t\t} else if (this.activeView === \"ranked\" && this.rankedList) {\n\t\t\t// Clear any stale \"nothing to add\" notice; openAddModelPicker re-sets it\n\t\t\t// if this input is another failed add attempt.\n\t\t\tthis.rankedNotice = null;\n\t\t\tthis.rankedList.handleInput(data);\n\t\t} else {\n\t\t\tthis.agentList.handleInput(data);\n\t\t}\n\t}\n}\n\n/**\n * Main settings selector component.\n */\nexport class SettingsSelectorComponent extends Container {\n\tprivate settingsList: SettingsList;\n\n\tconstructor(config: SettingsConfig, callbacks: SettingsCallbacks) {\n\t\tsuper();\n\n\t\tconst supportsImages = getCapabilities().images;\n\t\tlet arbiterSettings = { ...config.subagentArbiter };\n\t\tconst updateArbiter = async (patch: Partial<SubagentArbiterSettings>): Promise<boolean> => {\n\t\t\tconst next = { ...arbiterSettings, ...patch };\n\t\t\tif (!(await callbacks.onSubagentArbiterChange(next))) return false;\n\t\t\tarbiterSettings = next;\n\t\t\treturn true;\n\t\t};\n\n\t\tconst items: SettingItem[] = [\n\t\t\t{\n\t\t\t\tid: \"autocompact\",\n\t\t\t\tlabel: \"Auto-compact\",\n\t\t\t\tdescription: \"Automatically compact context when it gets too large\",\n\t\t\t\tcurrentValue: config.autoCompact ? \"true\" : \"false\",\n\t\t\t\tvalues: [\"true\", \"false\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"auto-load-nested-context\",\n\t\t\t\tlabel: \"Unrestricted nested context loading (expert)\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"OFF by default. Expert-only: automatically loads AGENTS.md/CLAUDE.md from every tool target; untrusted files can contain prompt injection.\",\n\t\t\t\tcurrentValue: config.autoLoadNestedContext ? \"true\" : \"false\",\n\t\t\t\tvalues: [\"true\", \"false\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"steering-mode\",\n\t\t\t\tlabel: \"Steering mode\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Enter while streaming queues steering messages. 'one-at-a-time': deliver one, wait for response. 'all': deliver all at once.\",\n\t\t\t\tcurrentValue: config.steeringMode,\n\t\t\t\tvalues: [\"one-at-a-time\", \"all\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"follow-up-mode\",\n\t\t\t\tlabel: \"Follow-up mode\",\n\t\t\t\tdescription:\n\t\t\t\t\t\"Alt+Enter queues follow-up messages until agent stops. 'one-at-a-time': deliver one, wait for response. 'all': deliver all at once.\",\n\t\t\t\tcurrentValue: config.followUpMode,\n\t\t\t\tvalues: [\"one-at-a-time\", \"all\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"transport\",\n\t\t\t\tlabel: \"Transport\",\n\t\t\t\tdescription: \"Preferred transport for providers that support multiple transports\",\n\t\t\t\tcurrentValue: config.transport,\n\t\t\t\tvalues: [\"sse\", \"websocket\", \"auto\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"hide-thinking\",\n\t\t\t\tlabel: \"Hide thinking\",\n\t\t\t\tdescription: \"Hide thinking blocks in assistant responses\",\n\t\t\t\tcurrentValue: config.hideThinkingBlock ? \"true\" : \"false\",\n\t\t\t\tvalues: [\"true\", \"false\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"quiet-startup\",\n\t\t\t\tlabel: \"Quiet startup\",\n\t\t\t\tdescription: \"Disable verbose printing at startup\",\n\t\t\t\tcurrentValue: config.quietStartup ? \"true\" : \"false\",\n\t\t\t\tvalues: [\"true\", \"false\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"double-escape-action\",\n\t\t\t\tlabel: \"Double-escape action\",\n\t\t\t\tdescription: \"Action when pressing Escape twice with empty editor\",\n\t\t\t\tcurrentValue: config.doubleEscapeAction,\n\t\t\t\tvalues: [\"tree\", \"fork\", \"none\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"tree-filter-mode\",\n\t\t\t\tlabel: \"Tree filter mode\",\n\t\t\t\tdescription: \"Default filter when opening /tree\",\n\t\t\t\tcurrentValue: config.treeFilterMode,\n\t\t\t\tvalues: [\"default\", \"no-tools\", \"user-only\", \"labeled-only\", \"all\"],\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"thinking\",\n\t\t\t\tlabel: \"Thinking level\",\n\t\t\t\tdescription: \"Reasoning depth for thinking-capable models\",\n\t\t\t\tcurrentValue: config.thinkingLevel,\n\t\t\t\tsubmenu: (currentValue, done) =>\n\t\t\t\t\tnew SelectSubmenu(\n\t\t\t\t\t\t\"Thinking Level\",\n\t\t\t\t\t\t\"Select reasoning depth for thinking-capable models\",\n\t\t\t\t\t\tconfig.availableThinkingLevels.map((level) => ({\n\t\t\t\t\t\t\tvalue: level,\n\t\t\t\t\t\t\tlabel: level,\n\t\t\t\t\t\t\tdescription: THINKING_DESCRIPTIONS[level],\n\t\t\t\t\t\t})),\n\t\t\t\t\t\tcurrentValue,\n\t\t\t\t\t\t(value) => {\n\t\t\t\t\t\t\tcallbacks.onThinkingLevelChange(value as ThinkingLevel);\n\t\t\t\t\t\t\tdone(value);\n\t\t\t\t\t\t},\n\t\t\t\t\t\t() => done(),\n\t\t\t\t\t),\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"theme\",\n\t\t\t\tlabel: \"Theme\",\n\t\t\t\tdescription: \"Color theme for the interface\",\n\t\t\t\tcurrentValue: config.currentTheme,\n\t\t\t\tsubmenu: (currentValue, done) =>\n\t\t\t\t\tnew SelectSubmenu(\n\t\t\t\t\t\t\"Theme\",\n\t\t\t\t\t\t\"Select color theme\",\n\t\t\t\t\t\tconfig.availableThemes.map((t) => ({\n\t\t\t\t\t\t\tvalue: t,\n\t\t\t\t\t\t\tlabel: t,\n\t\t\t\t\t\t})),\n\t\t\t\t\t\tcurrentValue,\n\t\t\t\t\t\t(value) => {\n\t\t\t\t\t\t\tcallbacks.onThemeChange(value);\n\t\t\t\t\t\t\tdone(value);\n\t\t\t\t\t\t},\n\t\t\t\t\t\t() => {\n\t\t\t\t\t\t\t// Restore original theme on cancel\n\t\t\t\t\t\t\tcallbacks.onThemePreview?.(currentValue);\n\t\t\t\t\t\t\tdone();\n\t\t\t\t\t\t},\n\t\t\t\t\t\t(value) => {\n\t\t\t\t\t\t\t// Preview theme on selection change\n\t\t\t\t\t\t\tcallbacks.onThemePreview?.(value);\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t},\n\t\t];\n\n\t\t// Thinking-display toggle for the current model. Only adaptive-thinking\n\t\t// Opus/Sonnet 4.6–4.x and Claude 5 families honor this field.\n\t\t// Shown as on (\"summarized\") by default; off (\"omitted\") hides thinking for lower latency.\n\t\tif (config.thinkingDisplaySupported) {\n\t\t\tconst hideThinkingIndex = items.findIndex((item) => item.id === \"hide-thinking\");\n\t\t\titems.splice(hideThinkingIndex + 1, 0, {\n\t\t\t\tid: \"thinking-display\",\n\t\t\t\tlabel: \"Show thinking summary\",\n\t\t\t\tdescription: \"Show summarized thinking for this model (off hides it for lower latency)\",\n\t\t\t\tcurrentValue: config.thinkingDisplay === \"summarized\" ? \"true\" : \"false\",\n\t\t\t\tvalues: [\"true\", \"false\"],\n\t\t\t});\n\t\t}\n\n\t\titems.push(\n\t\t\t{\n\t\t\t\tid: \"dispatch-arbiter-enabled\",\n\t\t\t\tlabel: \"Dispatch Arbiter\",\n\t\t\t\tdescription: arbiterSettings.model\n\t\t\t\t\t? \"Global-only fail-closed pre-spawn routing (configure model, thinking, and guide below)\"\n\t\t\t\t\t: \"Global-only; choose an arbiter model before enabling\",\n\t\t\t\tcurrentValue: arbiterSettings.enabled === true ? \"true\" : \"false\",\n\t\t\t\tsubmenu: (currentValue, done) =>\n\t\t\t\t\tnew SelectSubmenu(\n\t\t\t\t\t\t\"Dispatch Arbiter\",\n\t\t\t\t\t\t\"Enable only when the exact model, live scope, and guide are ready; failures do not persist\",\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\t{ value: \"false\", label: \"off\", description: \"Disabled (default)\" },\n\t\t\t\t\t\t\t{ value: \"true\", label: \"on\", description: \"Fail closed before every child spawn\" },\n\t\t\t\t\t\t],\n\t\t\t\t\t\tcurrentValue,\n\t\t\t\t\t\tasync (value) => {\n\t\t\t\t\t\t\tif (await updateArbiter({ enabled: value === \"true\" })) done(value);\n\t\t\t\t\t\t\telse done();\n\t\t\t\t\t\t},\n\t\t\t\t\t\t() => done(),\n\t\t\t\t\t),\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"dispatch-arbiter-model\",\n\t\t\t\tlabel: \"Dispatch Arbiter model\",\n\t\t\t\tdescription: \"Exact authenticated provider/model; no fallback\",\n\t\t\t\tcurrentValue: arbiterSettings.model ?? \"not set\",\n\t\t\t\tsubmenu: (currentValue, done) =>\n\t\t\t\t\tnew SelectSubmenu(\n\t\t\t\t\t\t\"Dispatch Arbiter Model\",\n\t\t\t\t\t\t\"Select the exact model used for the tool-less pre-spawn decision\",\n\t\t\t\t\t\tconfig.availableModelIds.map((model) => ({ value: model, label: model })),\n\t\t\t\t\t\tcurrentValue,\n\t\t\t\t\t\tasync (value) => {\n\t\t\t\t\t\t\tif (await updateArbiter({ model: value })) done(value);\n\t\t\t\t\t\t\telse done();\n\t\t\t\t\t\t},\n\t\t\t\t\t\t() => done(),\n\t\t\t\t\t),\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"dispatch-arbiter-thinking\",\n\t\t\t\tlabel: \"Dispatch Arbiter thinking\",\n\t\t\t\tdescription: \"Reasoning level for the arbiter call; validated against its model\",\n\t\t\t\tcurrentValue: arbiterSettings.thinking ?? \"off\",\n\t\t\t\tsubmenu: (currentValue, done) =>\n\t\t\t\t\tnew SelectSubmenu(\n\t\t\t\t\t\t\"Dispatch Arbiter Thinking\",\n\t\t\t\t\t\t\"Select reasoning depth for the headless arbiter call\",\n\t\t\t\t\t\t([\"off\", \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\"] as ThinkingLevel[]).map((level) => ({\n\t\t\t\t\t\t\tvalue: level,\n\t\t\t\t\t\t\tlabel: level,\n\t\t\t\t\t\t\tdescription: THINKING_DESCRIPTIONS[level],\n\t\t\t\t\t\t})),\n\t\t\t\t\t\tcurrentValue,\n\t\t\t\t\t\tasync (value) => {\n\t\t\t\t\t\t\tif (await updateArbiter({ thinking: value as ThinkingLevel })) done(value);\n\t\t\t\t\t\t\telse done();\n\t\t\t\t\t\t},\n\t\t\t\t\t\t() => done(),\n\t\t\t\t\t),\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: \"dispatch-arbiter-guide\",\n\t\t\t\tlabel: \"Dispatch Arbiter guide\",\n\t\t\t\tdescription: \"Routing-guide path; blank uses ~/.dreb/agent/model-routing-guide.md\",\n\t\t\t\tcurrentValue: arbiterSettings.guidePath ?? \"default\",\n\t\t\t\tsubmenu: (currentValue, done) =>\n\t\t\t\t\tnew TextInputSubmenu(\n\t\t\t\t\t\t\"Dispatch Arbiter Guide Path\",\n\t\t\t\t\t\t\"Enter a path, or leave blank for ~/.dreb/agent/model-routing-guide.md\",\n\t\t\t\t\t\tcurrentValue === \"default\" ? \"\" : currentValue,\n\t\t\t\t\t\tasync (value) => {\n\t\t\t\t\t\t\tconst guidePath = value.trim();\n\t\t\t\t\t\t\tif (await updateArbiter({ guidePath: guidePath || undefined })) done(guidePath || \"default\");\n\t\t\t\t\t\t\telse done();\n\t\t\t\t\t\t},\n\t\t\t\t\t\t() => done(),\n\t\t\t\t\t),\n\t\t\t},\n\t\t);\n\n\t\t// Single \"Agent Models\" entry that opens the agent picker submenu\n\t\tif (config.agentNames.length > 0) {\n\t\t\titems.push({\n\t\t\t\tid: \"agent-models\",\n\t\t\t\tlabel: \"Agent Models\",\n\t\t\t\tdescription: \"Configure model fallback lists for subagents\",\n\t\t\t\tcurrentValue: \"\",\n\t\t\t\tsubmenu: (_currentValue, done) =>\n\t\t\t\t\tnew AgentModelsSubmenu(\n\t\t\t\t\t\tconfig.agentNames,\n\t\t\t\t\t\tconfig.agentModels,\n\t\t\t\t\t\tconfig.availableModelIds,\n\t\t\t\t\t\t(agentName, models) => {\n\t\t\t\t\t\t\tcallbacks.onAgentModelsChange(agentName, models);\n\t\t\t\t\t\t},\n\t\t\t\t\t\t() => done(),\n\t\t\t\t\t),\n\t\t\t});\n\t\t}\n\n\t\t// Only show image toggle if terminal supports it\n\t\tif (supportsImages) {\n\t\t\t// Insert after autocompact\n\t\t\titems.splice(1, 0, {\n\t\t\t\tid: \"show-images\",\n\t\t\t\tlabel: \"Show images\",\n\t\t\t\tdescription: \"Render images inline in terminal\",\n\t\t\t\tcurrentValue: config.showImages ? \"true\" : \"false\",\n\t\t\t\tvalues: [\"true\", \"false\"],\n\t\t\t});\n\t\t}\n\n\t\t// Image auto-resize toggle (always available, affects both attached and read images)\n\t\titems.splice(supportsImages ? 2 : 1, 0, {\n\t\t\tid: \"auto-resize-images\",\n\t\t\tlabel: \"Auto-resize images\",\n\t\t\tdescription: \"Resize large images to 2000x2000 max for better model compatibility\",\n\t\t\tcurrentValue: config.autoResizeImages ? \"true\" : \"false\",\n\t\t\tvalues: [\"true\", \"false\"],\n\t\t});\n\n\t\t// Block images toggle (always available, insert after auto-resize-images)\n\t\tconst autoResizeIndex = items.findIndex((item) => item.id === \"auto-resize-images\");\n\t\titems.splice(autoResizeIndex + 1, 0, {\n\t\t\tid: \"block-images\",\n\t\t\tlabel: \"Block images\",\n\t\t\tdescription: \"Prevent images from being sent to LLM providers\",\n\t\t\tcurrentValue: config.blockImages ? \"true\" : \"false\",\n\t\t\tvalues: [\"true\", \"false\"],\n\t\t});\n\n\t\t// Skill commands toggle (insert after block-images)\n\t\tconst blockImagesIndex = items.findIndex((item) => item.id === \"block-images\");\n\t\titems.splice(blockImagesIndex + 1, 0, {\n\t\t\tid: \"skill-commands\",\n\t\t\tlabel: \"Skill commands\",\n\t\t\tdescription: \"Register skills as /skill:name commands\",\n\t\t\tcurrentValue: config.enableSkillCommands ? \"true\" : \"false\",\n\t\t\tvalues: [\"true\", \"false\"],\n\t\t});\n\n\t\t// Hardware cursor toggle (insert after skill-commands)\n\t\tconst skillCommandsIndex = items.findIndex((item) => item.id === \"skill-commands\");\n\t\titems.splice(skillCommandsIndex + 1, 0, {\n\t\t\tid: \"show-hardware-cursor\",\n\t\t\tlabel: \"Show hardware cursor\",\n\t\t\tdescription: \"Show the terminal cursor while still positioning it for IME support\",\n\t\t\tcurrentValue: config.showHardwareCursor ? \"true\" : \"false\",\n\t\t\tvalues: [\"true\", \"false\"],\n\t\t});\n\n\t\t// Editor padding toggle (insert after show-hardware-cursor)\n\t\tconst hardwareCursorIndex = items.findIndex((item) => item.id === \"show-hardware-cursor\");\n\t\titems.splice(hardwareCursorIndex + 1, 0, {\n\t\t\tid: \"editor-padding\",\n\t\t\tlabel: \"Editor padding\",\n\t\t\tdescription: \"Horizontal padding for input editor (0-3)\",\n\t\t\tcurrentValue: String(config.editorPaddingX),\n\t\t\tvalues: [\"0\", \"1\", \"2\", \"3\"],\n\t\t});\n\n\t\t// Autocomplete max visible toggle (insert after editor-padding)\n\t\tconst editorPaddingIndex = items.findIndex((item) => item.id === \"editor-padding\");\n\t\titems.splice(editorPaddingIndex + 1, 0, {\n\t\t\tid: \"autocomplete-max-visible\",\n\t\t\tlabel: \"Autocomplete max items\",\n\t\t\tdescription: \"Max visible items in autocomplete dropdown (3-20)\",\n\t\t\tcurrentValue: String(config.autocompleteMaxVisible),\n\t\t\tvalues: [\"3\", \"5\", \"7\", \"10\", \"15\", \"20\"],\n\t\t});\n\n\t\t// Add borders\n\t\tthis.addChild(new DynamicBorder());\n\n\t\tthis.settingsList = new SettingsList(\n\t\t\titems,\n\t\t\t10,\n\t\t\tgetSettingsListTheme(),\n\t\t\t(id, newValue) => {\n\t\t\t\tswitch (id) {\n\t\t\t\t\tcase \"autocompact\":\n\t\t\t\t\t\tcallbacks.onAutoCompactChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"auto-load-nested-context\":\n\t\t\t\t\t\tcallbacks.onAutoLoadNestedContextChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"show-images\":\n\t\t\t\t\t\tcallbacks.onShowImagesChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"auto-resize-images\":\n\t\t\t\t\t\tcallbacks.onAutoResizeImagesChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"block-images\":\n\t\t\t\t\t\tcallbacks.onBlockImagesChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"skill-commands\":\n\t\t\t\t\t\tcallbacks.onEnableSkillCommandsChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"steering-mode\":\n\t\t\t\t\t\tcallbacks.onSteeringModeChange(newValue as \"all\" | \"one-at-a-time\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"follow-up-mode\":\n\t\t\t\t\t\tcallbacks.onFollowUpModeChange(newValue as \"all\" | \"one-at-a-time\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"transport\":\n\t\t\t\t\t\tcallbacks.onTransportChange(newValue as Transport);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"hide-thinking\":\n\t\t\t\t\t\tcallbacks.onHideThinkingBlockChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"thinking-display\":\n\t\t\t\t\t\tcallbacks.onThinkingDisplayChange(newValue === \"true\" ? \"summarized\" : \"omitted\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"quiet-startup\":\n\t\t\t\t\t\tcallbacks.onQuietStartupChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"double-escape-action\":\n\t\t\t\t\t\tcallbacks.onDoubleEscapeActionChange(newValue as \"fork\" | \"tree\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"tree-filter-mode\":\n\t\t\t\t\t\tcallbacks.onTreeFilterModeChange(\n\t\t\t\t\t\t\tnewValue as \"default\" | \"no-tools\" | \"user-only\" | \"labeled-only\" | \"all\",\n\t\t\t\t\t\t);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"show-hardware-cursor\":\n\t\t\t\t\t\tcallbacks.onShowHardwareCursorChange(newValue === \"true\");\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"editor-padding\":\n\t\t\t\t\t\tcallbacks.onEditorPaddingXChange(parseInt(newValue, 10));\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"autocomplete-max-visible\":\n\t\t\t\t\t\tcallbacks.onAutocompleteMaxVisibleChange(parseInt(newValue, 10));\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t},\n\t\t\tcallbacks.onCancel,\n\t\t\t{ enableSearch: true },\n\t\t);\n\n\t\tthis.addChild(this.settingsList);\n\t\tthis.addChild(new DynamicBorder());\n\t}\n\n\tgetSettingsList(): SettingsList {\n\t\treturn this.settingsList;\n\t}\n}\n"]}
|