1package modes
2
3import (
4 "context"
5 "fmt"
6 "runtime"
7 "strconv"
8 "strings"
9
10 toolspkg "github.com/bnema/zut/packages/agent/tools"
11 "github.com/bnema/zut/packages/provider"
12 "github.com/bnema/zut/packages/tui"
13)
14
15func effectiveImageProtocol(override *bool) tui.ImageProtocol {
16 detected := tui.DetectImageProtocol()
17 if override == nil {
18 return detected
19 }
20 if !*override {
21 return tui.ImageProtocolNone
22 }
23 return detected
24}
25func imageProtocolName(p tui.ImageProtocol) string {
26 switch p {
27 case tui.ImageProtocolKitty:
28 return "kitty/ghostty"
29 case tui.ImageProtocolITerm2:
30 return "iTerm2"
31 default:
32 return "none"
33 }
34}
35func onOff(v bool) string {
36 if v {
37 return "enabled"
38 }
39 return "disabled"
40}
41func (i *Interactive) applyInputCursorColor() {
42 if i == nil || i.cfg.Terminal == nil {
43 return
44 }
45 if i.cfg.EffectiveThemeName == "auto" && i.cfg.Theme.FG == tui.TerminalDefault() {
46 _, _ = i.cfg.Terminal.Write([]byte(tui.ResetCursorColor() + tui.CursorShapeBlock()))
47 return
48 }
49 color := tui.Color256(15)
50 if i.cursorDimmed {
51 color = i.cfg.Theme.DimColor(color, modalBackdropDimPercent)
52 }
53 _, _ = i.cfg.Terminal.Write([]byte(tui.CursorColor(color) + tui.CursorShapeBlock()))
54}
55func (i *Interactive) setInputCursorDimmed(dimmed bool) {
56 if i == nil || i.cursorDimmed == dimmed {
57 return
58 }
59 i.cursorDimmed = dimmed
60 i.applyInputCursorColor()
61}
62func reasoningSettingOptions(levels []string) []settingsOption {
63 descriptions := map[string]string{
64 "": "no reasoning",
65 "minimum": "very brief (~1k tokens)",
66 "low": "light (~2k tokens)",
67 "medium": "moderate (~8k tokens)",
68 "high": "deep (~16k tokens)",
69 "xhigh": "extra-high effort",
70 "max": "unconstrained effort",
71 }
72 options := make([]settingsOption, 0, len(levels))
73 for _, level := range levels {
74 label := level
75 if label == "" {
76 label = "off"
77 }
78 options = append(options, settingsOption{value: level, label: label, desc: descriptions[level]})
79 }
80 return options
81}
82func (i *Interactive) reasoningSettingItem() settingsItem {
83 levels := []string{"", "minimum", "low", "medium", "high", "xhigh", "max"}
84 reasoning := provider.NormalizeReasoning(i.cfg.Reasoning)
85 desc := "reasoning depth for reasoning-capable models"
86 hint := ""
87 if m, err := provider.FindModel(i.cfg.Provider, i.cfg.Model); err == nil {
88 levels = provider.AvailableReasoningLevels(m)
89 reasoning = provider.ClampReasoningForModel(m, reasoning)
90 if !m.Reasoning {
91 hint = "current model does not support reasoning"
92 desc += "; current model does not support reasoning"
93 } else if len(levels) == 1 {
94 hint = "current model has no adjustable reasoning levels"
95 desc += "; current model has no adjustable reasoning levels"
96 } else {
97 desc += "; only levels supported by the current model are shown"
98 }
99 }
100 options := reasoningSettingOptions(levels)
101 choice := 0
102 for idx, opt := range options {
103 if opt.value == reasoning {
104 choice = idx
105 break
106 }
107 }
108
109 return settingsItem{
110 key: "reasoning",
111 label: "reasoning level",
112 desc: desc,
113 options: options,
114 choice: choice,
115 hint: hint,
116 }
117}
118func (i *Interactive) openReasoningDialog() {
119 i.settingsDialog.OpenDirectOption(i.reasoningSettingItem())
120}
121func (i *Interactive) cycleReasoningSetting() {
122 item := i.reasoningSettingItem()
123 if len(item.options) <= 1 {
124 i.mu.Lock()
125 i.statusOK = item.hint
126 i.statusErr = ""
127 i.mu.Unlock()
128 i.invalidate()
129 return
130 }
131 next := item.options[(item.choice+1)%len(item.options)]
132 i.applyReasoningSetting(next.value)
133}
134func (i *Interactive) openSettingsDialog() {
135 detected := tui.DetectImageProtocol()
136 imgEnabled := detected != tui.ImageProtocolNone
137 if i.cfg.InlineImagesEnabled != nil {
138 imgEnabled = *i.cfg.InlineImagesEnabled
139 }
140 imgDisabled := detected == tui.ImageProtocolNone
141 imgHint := ""
142 if imgDisabled {
143 imgEnabled = false
144 imgHint = "this terminal does not support inline images"
145 } else {
146 imgHint = "terminal supports " + imageProtocolName(detected)
147 }
148
149 terminalAlerts := terminalAlertsEnabled(i.cfg.TerminalAlertsEnabled)
150 terminalTitles := terminalTitleEnabled(i.cfg.TerminalTitleEnabled)
151 autoSubagents := false
152 if i.cfg.AutoSubagentsEnabled != nil {
153 autoSubagents = *i.cfg.AutoSubagentsEnabled
154 }
155 autoSubagentsDisabled := !i.autoSubagentsAvailable()
156 autoSubagentsHint := i.autoSubagentsUnavailableHint()
157 if autoSubagentsDisabled {
158 autoSubagents = false
159 }
160
161 ponytailEnabled := i.ponytailEnabled()
162 ponytailDisabled := !i.ponytailSettingsAvailable()
163 ponytailHint := ""
164 if ponytailDisabled {
165 ponytailHint = "requires persistent settings and live prompt refresh support"
166 }
167
168 webSearchEnabled := i.webSearchEffectiveEnabled()
169 webSearchDisabled := !i.webSearchSettingsAvailable()
170 webSearchHint := i.webSearchUnavailableHint()
171
172 fastMode := i.cfg.FastMode != nil && *i.cfg.FastMode
173 fastModeHint := "OpenAI service tier"
174 if !provider.SupportsFastMode(i.cfg.Provider) {
175 fastModeHint = "only supported for OpenAI providers"
176 }
177 lspEnabled := i.cfg.LSPEnabled == nil || *i.cfg.LSPEnabled
178 subagentLSPEnabled := i.cfg.SubagentLSPEnabled == nil || *i.cfg.SubagentLSPEnabled
179
180 jailByDefault := i.cfg.JailByDefault != nil && *i.cfg.JailByDefault
181 recursiveFiles := i.cfg.RecursiveFileSuggest != nil && *i.cfg.RecursiveFileSuggest
182 respectGitignore := i.cfg.RespectGitignore == nil || *i.cfg.RespectGitignore
183 compactMode := i.compactModeEnabled()
184 showInstructions := i.cfg.ShowInstructionsAtStartup != nil && *i.cfg.ShowInstructionsAtStartup
185 inputStyle := tui.NormalizeInputStyle(i.cfg.TUIInputStyle)
186 statusPosition := tui.NormalizeStatusPosition(i.cfg.TUIStatusPosition)
187 workingPosition := tui.NormalizeWorkingPosition(i.cfg.TUIWorkingPosition)
188 quickItems := i.quickModelSettingItems()
189
190 autoCompactOptions := []settingsOption{
191 {value: "0", label: "off", desc: "disable context-percentage triggers; payload-too-large recovery stays enabled"},
192 {value: "70", label: "70%", desc: "compact earlier to keep more context headroom"},
193 {value: "80", label: "80%", desc: "compact with moderate context headroom"},
194 {value: "85", label: "85%", desc: "default balance between history and headroom"},
195 {value: "90", label: "90%", desc: "retain more history before compacting"},
196 {value: "95", label: "95%", desc: "retain most history before compacting"},
197 }
198 autoCompactThreshold := NormalizeAutoCompactThreshold(i.cfg.AutoCompactThreshold)
199 autoCompactChoice := 0
200 for idx, opt := range autoCompactOptions {
201 if opt.value == strconv.Itoa(autoCompactThreshold) {
202 autoCompactChoice = idx
203 break
204 }
205 }
206
207 reasoningItem := i.reasoningSettingItem()
208
209 themeName := i.cfg.ThemeName
210 if themeName == "" {
211 themeName = "auto"
212 }
213 if themeName != "auto" && !tui.ThemeExists(i.cfg.ZutHome, themeName) {
214 themeName = "auto"
215 i.cfg.ThemeName = ""
216 if i.cfg.SettingsStore != nil {
217 _ = i.cfg.SettingsStore.SetTheme("auto")
218 }
219 i.applyThemeNow("auto")
220 }
221 themeOptions := []settingsOption{}
222 themeChoice := 0
223 availableThemes := tui.AvailableThemes(i.cfg.ZutHome)
224 if i.cfg.ExtensionThemes != nil {
225 availableThemes = append(availableThemes, i.cfg.ExtensionThemes()...)
226 }
227 for idx, opt := range availableThemes {
228 themeOptions = append(themeOptions, settingsOption{value: opt.Value, label: opt.Label, desc: opt.Description})
229 if opt.Value == themeName {
230 themeChoice = idx
231 }
232 }
233
234 inputStyleOptions := []settingsOption{
235 {value: tui.InputStylePlain, label: "plain", desc: "render the input as the normal prompt line"},
236 {value: tui.InputStyleLines, label: "lines", desc: "draw separator lines above and below the input"},
237 {value: tui.InputStyleBlock, label: "block", desc: "render the input as a user-bubble-style block"},
238 }
239 inputStyleChoice := 0
240 for idx, opt := range inputStyleOptions {
241 if opt.value == inputStyle {
242 inputStyleChoice = idx
243 break
244 }
245 }
246 statusPositionOptions := []settingsOption{
247 {value: tui.StatusPositionAboveInput, label: "above input", desc: "show model, usage, and working directory above the input"},
248 {value: tui.StatusPositionBelowInput, label: "below input", desc: "show model, usage, and working directory below the input"},
249 }
250 statusPositionChoice := 0
251 for idx, opt := range statusPositionOptions {
252 if opt.value == statusPosition {
253 statusPositionChoice = idx
254 break
255 }
256 }
257
258 workingPositionOptions := []settingsOption{
259 {value: tui.WorkingPositionAboveInput, label: "above input", desc: "show the working spinner above the input"},
260 {value: tui.WorkingPositionBelowInput, label: "below input", desc: "show the working spinner below the input"},
261 }
262 workingPositionChoice := 0
263 for idx, opt := range workingPositionOptions {
264 if opt.value == workingPosition {
265 workingPositionChoice = idx
266 break
267 }
268 }
269
270 items := []settingsItem{
271 {
272 key: "inline_images_enabled",
273 label: "render images when supported",
274 desc: "draw screenshots inline instead of showing a text placeholder",
275 value: imgEnabled,
276 disabled: imgDisabled,
277 hint: imgHint,
278 },
279 {
280 key: "terminal_alerts_enabled",
281 label: "terminal alerts",
282 desc: "ring the terminal when the main session needs attention or an extension raises an alert",
283 value: terminalAlerts,
284 },
285 {
286 key: "terminal_title_enabled",
287 label: "AI terminal titles",
288 desc: "ask the active model for a concise title after the first prompt; uses one extra hidden request",
289 value: terminalTitles,
290 },
291 {
292 key: "auto_subagents_enabled",
293 label: "proactive delegation",
294 desc: "delegate independent sidecar work while keeping the critical path local",
295 value: autoSubagents,
296 disabled: autoSubagentsDisabled,
297 hint: autoSubagentsHint,
298 },
299 {
300 key: "ponytail_enabled",
301 label: "ponytail coding mode",
302 desc: "apply compact coding guidance that favors small, validated, maintainable changes",
303 value: ponytailEnabled,
304 disabled: ponytailDisabled,
305 hint: ponytailHint,
306 },
307 {
308 key: "web_search_enabled",
309 label: "web search",
310 desc: "search DuckDuckGo for bounded public-web sources; sends queries to the public backend",
311 value: webSearchEnabled,
312 disabled: webSearchDisabled,
313 hint: webSearchHint,
314 },
315 {
316 key: "fast_mode",
317 label: "fast mode",
318 desc: "request the provider's fast tier where supported; unsupported providers return an error",
319 value: fastMode,
320 hint: fastModeHint,
321 },
322 {
323 key: "lsp_enabled",
324 label: "lsp in main session",
325 desc: "enable the lsp tool and code diagnostics for the main agent",
326 value: lspEnabled,
327 },
328 {
329 key: "subagent_lsp_enabled",
330 label: "lsp in sub-agents",
331 desc: "allow newly spawned background sub-agents to use the lsp tool",
332 value: subagentLSPEnabled,
333 },
334 {
335 key: "auto_compact_threshold",
336 label: "auto-compact threshold",
337 desc: "choose how full the model context can get before zut condenses conversation history",
338 options: autoCompactOptions,
339 choice: autoCompactChoice,
340 },
341 {
342 key: "jail_by_default",
343 label: "jail new sessions by default",
344 desc: "confine tools to the session working directory unless /unjail is used",
345 value: jailByDefault,
346 },
347 {
348 key: "recursive_file_suggest",
349 label: "recursive @-file search",
350 desc: "fuzzy-search the whole project tree when picking files with @ instead of browsing one directory at a time",
351 value: recursiveFiles,
352 },
353 {
354 key: "respect_gitignore",
355 label: "hide gitignored files in @-picker",
356 desc: "skip files and directories matched by the project's root .gitignore (and .git) when picking files with @",
357 value: respectGitignore,
358 },
359 {
360 key: "compact_mode",
361 label: "compact transcript rendering",
362 desc: "reduce visual chrome by rendering tool calls without boxes and sent messages without padded bubbles",
363 value: compactMode,
364 },
365 {
366 key: "show_instructions_at_startup",
367 label: "show loaded resources at startup",
368 desc: "list loaded context files, extensions, and user-installed skills above the transcript",
369 value: showInstructions,
370 },
371 {
372 key: "tui_settings",
373 label: "tui settings",
374 desc: "choose input chrome in its order around the prompt",
375 children: []settingsItem{
376 {
377 key: "tui_status_position",
378 label: "status position",
379 desc: "place model, usage, and working directory above or below the input",
380 options: statusPositionOptions,
381 choice: statusPositionChoice,
382 },
383 {
384 key: "tui_working_position",
385 label: "working spinner position",
386 desc: "place the working spinner above or below the input",
387 options: workingPositionOptions,
388 choice: workingPositionChoice,
389 },
390 {
391 key: "tui_input_style",
392 label: "input style",
393 desc: "choose between the plain prompt, lines, and a block input area",
394 options: inputStyleOptions,
395 choice: inputStyleChoice,
396 },
397 },
398 },
399 reasoningItem,
400 {
401 key: "theme",
402 label: "color theme",
403 desc: "choose terminal-owned auto, a built-in dark/light theme, or a loaded theme file",
404 options: themeOptions,
405 choice: themeChoice,
406 },
407 }
408 if len(quickItems) > 0 {
409 items = append(items, settingsItem{
410 key: "quick_models",
411 label: "model profiles",
412 desc: "configure " + quickModelShortcutPrefix() + "+1 through " + quickModelShortcutPrefix() + "+9 model, reasoning, and fast-mode profiles",
413 children: quickItems,
414 })
415 }
416 i.settingsDialog.Open(items)
417}
418func (i *Interactive) applySettingChange(act settingsAction) {
419 switch {
420 case strings.HasPrefix(act.Key, "quick_model_"):
421 i.applyQuickModelSetting(act.Key, act.StringValue)
422 case act.Key == "reasoning":
423 i.applyReasoningSetting(act.StringValue)
424 case act.Key == "theme":
425 i.applyThemeSetting(act.StringValue)
426 case act.Key == "auto_compact_threshold":
427 i.applyAutoCompactThresholdSetting(act.StringValue)
428 case act.Key == "tui_input_style":
429 i.applyTUIInputStyleSetting(act.StringValue)
430 case act.Key == "tui_status_position":
431 i.applyTUIStatusPositionSetting(act.StringValue)
432 case act.Key == "tui_working_position":
433 i.applyTUIWorkingPositionSetting(act.StringValue)
434 default:
435 i.applySettingToggle(act.Key, act.Value)
436 }
437}
438func (i *Interactive) applyAutoCompactThresholdSetting(value string) {
439 threshold, err := strconv.Atoi(value)
440 if err != nil {
441 return
442 }
443 threshold = NormalizeAutoCompactThreshold(&threshold)
444 if store, ok := i.cfg.SettingsStore.(autoCompactThresholdSettingsStore); ok {
445 if err := store.SetAutoCompactThreshold(threshold); err != nil {
446 i.mu.Lock()
447 i.statusErr = "settings: " + err.Error()
448 i.mu.Unlock()
449 return
450 }
451 }
452 i.mu.Lock()
453 i.cfg.AutoCompactThreshold = &threshold
454 if threshold == 0 {
455 i.statusOK = "auto-compact threshold off"
456 } else {
457 i.statusOK = fmt.Sprintf("auto-compact threshold %d%%", threshold)
458 }
459 i.statusErr = ""
460 i.mu.Unlock()
461 i.invalidate()
462}
463func (i *Interactive) quickModelSettingItems() []settingsItem {
464 if len(i.cfg.QuickModelShortcuts) < 9 {
465 next := make([]QuickModelShortcut, 9)
466 copy(next, i.cfg.QuickModelShortcuts)
467 i.cfg.QuickModelShortcuts = next
468 }
469 items := make([]settingsItem, 0, 9)
470 for slot := 1; slot <= 9; slot++ {
471 items = append(items, i.quickModelSettingItem(slot))
472 }
473 return items
474}
475func (i *Interactive) quickModelSettingItem(slot int) settingsItem {
476 current := QuickModelShortcut{}
477 if slot >= 1 && len(i.cfg.QuickModelShortcuts) >= slot {
478 current = i.cfg.QuickModelShortcuts[slot-1]
479 }
480 hint := "not assigned"
481 if current.Provider != "" && current.Model != "" {
482 hint = modelProfileDescription(current)
483 }
484 if slot == i.cfg.ActiveModelProfile {
485 hint += " (active)"
486 }
487 return settingsItem{
488 key: "quick_model_" + strconv.Itoa(slot),
489 label: "profile " + strconv.Itoa(slot),
490 desc: quickModelShortcutLabel(slot) + " recalls model, reasoning, and fast mode. Enter assigns, Left/Right in the picker sets reasoning, Backspace clears.",
491 picker: true,
492 hint: hint,
493 }
494}
495func (i *Interactive) compactModeEnabled() bool {
496 return i.cfg.CompactMode != nil && *i.cfg.CompactMode
497}
498func quickModelShortcutSlot(k tui.Key) int {
499 if k.Kind != tui.KeyRune || k.Alt {
500 return 0
501 }
502 if runtime.GOOS == "darwin" {
503 if !k.Super && !k.Ctrl {
504 return 0
505 }
506 } else if !k.Ctrl {
507 return 0
508 }
509 if k.Rune >= '1' && k.Rune <= '9' {
510 return int(k.Rune - '0')
511 }
512 // French AZERTY's unshifted number row. Require a modifier so normal
513 // typing and pasted punctuation never activate profiles.
514 for idx, r := range []rune("&é\"'(-è_ç") {
515 if k.Rune == r {
516 return idx + 1
517 }
518 }
519 return 0
520}
521func quickModelShortcutPrefix() string {
522 return "Ctrl"
523}
524func quickModelShortcutLabel(slot int) string {
525 return quickModelShortcutPrefix() + "+" + strconv.Itoa(slot)
526}
527func (i *Interactive) openQuickModelPicker(slot int) {
528 if slot < 1 || slot > 9 {
529 return
530 }
531 i.quickModelAssign = slot
532 i.quickModelActivate = false
533 current, reasoning := i.cfg.Model, i.cfg.Reasoning
534 if len(i.cfg.QuickModelShortcuts) >= slot && i.cfg.QuickModelShortcuts[slot-1].Model != "" {
535 current = i.cfg.QuickModelShortcuts[slot-1].Model
536 reasoning = i.cfg.QuickModelShortcuts[slot-1].Reasoning
537 }
538 var loggedIn []string
539 if i.cfg.LoggedInProviders != nil {
540 loggedIn = i.cfg.LoggedInProviders()
541 }
542 i.modelDialog.Open(current, loggedIn, reasoning)
543}
544func (i *Interactive) applyQuickModelSelection(slot int, providerName, model string) {
545 fastMode := i.cfg.FastMode != nil && *i.cfg.FastMode
546 if slot <= len(i.cfg.QuickModelShortcuts) && i.cfg.QuickModelShortcuts[slot-1].Model != "" {
547 fastMode = i.cfg.QuickModelShortcuts[slot-1].FastMode
548 }
549 p := QuickModelShortcut{Provider: providerName, Model: model, Reasoning: i.modelDialog.reasoning, FastMode: fastMode}
550 if i.quickModelActivate {
551 i.activateModelProfile(slot, p)
552 } else {
553 i.setQuickModelProfile(slot, p)
554 }
555 i.quickModelActivate = false
556}
557func (i *Interactive) applyQuickModelShortcut(slot int) {
558 if slot < 1 || slot > 9 {
559 return
560 }
561 if i.busy {
562 i.mu.Lock()
563 i.statusErr = "cannot switch model while a turn is running"
564 i.statusOK = ""
565 i.mu.Unlock()
566 i.invalidate()
567 return
568 }
569 p := QuickModelShortcut{}
570 if slot <= len(i.cfg.QuickModelShortcuts) {
571 p = i.cfg.QuickModelShortcuts[slot-1]
572 }
573 if p.Provider == "" && p.Model == "" {
574 i.openQuickModelPicker(slot)
575 i.quickModelActivate = true
576 i.mu.Lock()
577 i.statusErr = ""
578 i.statusOK = fmt.Sprintf("choose a model for profile %d; Enter saves and activates", slot)
579 i.mu.Unlock()
580 i.invalidate()
581 return
582 }
583 i.activateModelProfile(slot, p)
584}
585
586// cycleModelProfile moves to the next (+1) or previous (-1) profile slot
587// with wraparound across slots 1-9. It delegates to applyQuickModelShortcut
588// so empty slots open the model picker and busy turns show the same guard
589// as Ctrl+1..9. With no active profile, Tab starts at slot 1 and Shift-Tab
590// at slot 9.
591func (i *Interactive) cycleModelProfile(dir int) {
592 start := i.cfg.ActiveModelProfile
593 var next int
594 switch {
595 case start >= 1 && start <= 9:
596 next = ((start-1+dir)%9+9)%9 + 1
597 case dir < 0:
598 next = 9
599 default:
600 next = 1
601 }
602 i.applyQuickModelShortcut(next)
603}
604func (i *Interactive) applyQuickModelSetting(key, value string) {
605 slotText := strings.TrimPrefix(key, "quick_model_")
606 slot, err := strconv.Atoi(slotText)
607 if err != nil || slot < 1 || slot > 9 {
608 return
609 }
610 providerName, model := "", ""
611 if value != "" {
612 parts := strings.SplitN(value, "\t", 2)
613 if len(parts) == 2 {
614 providerName, model = parts[0], parts[1]
615 }
616 }
617 i.setQuickModelShortcut(slot, providerName, model)
618}
619func (i *Interactive) setQuickModelShortcut(slot int, providerName, model string) {
620 i.setQuickModelProfile(slot, QuickModelShortcut{
621 Provider: providerName, Model: model, Reasoning: i.cfg.Reasoning,
622 FastMode: i.cfg.FastMode != nil && *i.cfg.FastMode,
623 })
624}
625func (i *Interactive) refreshQuickModelSettingsItem(slot int) {
626 if i.settingsDialog == nil || !i.settingsDialog.Active() || len(i.settingsDialog.items) == 0 {
627 return
628 }
629 key := "quick_model_" + strconv.Itoa(slot)
630 for idx, it := range i.settingsDialog.items {
631 if it.key == key {
632 i.settingsDialog.items[idx] = i.quickModelSettingItem(slot)
633 return
634 }
635 }
636}
637func (i *Interactive) resetSettingsToggle(key string, value bool) {
638 if i.settingsDialog == nil {
639 return
640 }
641 for idx := range i.settingsDialog.items {
642 if i.settingsDialog.items[idx].key == key {
643 i.settingsDialog.items[idx].value = value
644 return
645 }
646 }
647}
648func (i *Interactive) ponytailEnabled() bool {
649 return i.cfg.PonytailEnabled == nil || *i.cfg.PonytailEnabled
650}
651func (i *Interactive) ponytailSettingsAvailable() bool {
652 _, persistent := i.cfg.SettingsStore.(ponytailSettingsStore)
653 return persistent && i.cfg.RefreshPrompt != nil
654}
655func (i *Interactive) webSearchEnabled() bool {
656 return i.cfg.WebSearchEnabled == nil || *i.cfg.WebSearchEnabled
657}
658func (i *Interactive) webSearchToolAllowed() bool {
659 return i.cfg.WebSearchToolAllowed == nil || *i.cfg.WebSearchToolAllowed
660}
661func (i *Interactive) webSearchEffectiveEnabled() bool {
662 return i.webSearchToolAllowed() && (i.cfg.WebSearchInvocationOverride || i.webSearchEnabled())
663}
664func (i *Interactive) webSearchSettingsAvailable() bool {
665 _, persistent := i.cfg.SettingsStore.(webSearchSettingsStore)
666 return i.webSearchToolAllowed() && !i.cfg.WebSearchInvocationOverride && persistent && i.cfg.RefreshTools != nil
667}
668func (i *Interactive) webSearchUnavailableHint() string {
669 if !i.webSearchToolAllowed() {
670 return "unavailable in this session due to the launch-time tool capability ceiling"
671 }
672 if i.cfg.WebSearchInvocationOverride {
673 return "enabled for this session by explicit --tools web_search; the persisted setting applies to invocations without --tools"
674 }
675 if !i.webSearchSettingsAvailable() {
676 return "requires persistent settings and live tool refresh support"
677 }
678 return ""
679}
680func (i *Interactive) setWebSearchAvailable(available bool) {
681 // Advance before publishing the new gate/child ceiling. A resolver that
682 // captured the prior generation can no longer commit after this transition.
683 // Callers may hold agentMu and i.mu; the callback must complete without
684 // calling back into Interactive or requiring either lock.
685 i.webSearchPolicyGeneration.Add(1)
686 if i.cfg.SetWebSearchAvailable != nil {
687 i.cfg.SetWebSearchAvailable(available)
688 }
689}
690func (i *Interactive) applyWebSearchSettingToggle(key string, value bool) {
691 previous := i.webSearchEnabled()
692 store, persistent := i.cfg.SettingsStore.(webSearchSettingsStore)
693 if !i.webSearchSettingsAvailable() || !persistent {
694 i.resetSettingsToggle(key, i.webSearchEffectiveEnabled())
695 i.mu.Lock()
696 i.statusOK = ""
697 i.statusErr = "web search unavailable: " + i.webSearchUnavailableHint()
698 i.mu.Unlock()
699 return
700 }
701 if err := store.SetWebSearchEnabled(value); err != nil {
702 i.resetSettingsToggle(key, previous)
703 i.mu.Lock()
704 i.statusOK = ""
705 i.statusErr = "settings: " + err.Error()
706 i.mu.Unlock()
707 return
708 }
709 val := value
710 i.cfg.WebSearchEnabled = &val
711 // Revoke and advance the policy generation for both directions. Enabling
712 // remains fail-closed until the freshly resolved registry commits, while a
713 // stale resolve from before this persisted transition is rejected.
714 i.stripWebSearchTool()
715 if err := i.cfg.RefreshTools(); err != nil {
716 // Refresh failure leaves the old registry's state unknown. Revoke both
717 // live execution and generic-child inheritance before rollback.
718 i.stripWebSearchTool()
719 errMsg := "settings: web search tool refresh: " + err.Error()
720 if rollbackErr := store.SetWebSearchEnabled(previous); rollbackErr != nil {
721 failClosed := false
722 i.cfg.WebSearchEnabled = &failClosed
723 i.resetSettingsToggle(key, false)
724 errMsg += "; rollback persistence: " + rollbackErr.Error()
725 } else {
726 previousVal := previous
727 i.cfg.WebSearchEnabled = &previousVal
728 i.resetSettingsToggle(key, previous)
729 if refreshErr := i.cfg.RefreshTools(); refreshErr != nil {
730 errMsg += "; rollback refresh: " + refreshErr.Error()
731 i.stripWebSearchTool()
732 }
733 }
734 i.mu.Lock()
735 i.statusOK = ""
736 i.statusErr = errMsg
737 i.mu.Unlock()
738 return
739 }
740 i.mu.Lock()
741 i.statusOK = "web search " + onOff(value)
742 i.statusErr = ""
743 i.mu.Unlock()
744}
745func (i *Interactive) stripWebSearchTool() {
746 // Revoke first so a stale tool snapshotted by core cannot execute while
747 // the advertised registry is being replaced.
748 i.setWebSearchAvailable(false)
749 i.agentMu.Lock()
750 defer i.agentMu.Unlock()
751 i.mu.Lock()
752 defer i.mu.Unlock()
753 if i.agent == nil {
754 return
755 }
756 tools := i.agent.ToolsSnapshot()
757 hasWebCapability := false
758 for _, name := range toolspkg.WebCapabilityNames {
759 if _, ok := tools[name]; ok {
760 hasWebCapability = true
761 break
762 }
763 }
764 if !hasWebCapability {
765 return
766 }
767 toolspkg.RemoveWebCapabilities(tools)
768 i.agent.SetTools(tools)
769}
770func (i *Interactive) applySettingToggle(key string, value bool) {
771 // Every setting toggle forces a full repaint at the end — same
772 // effect as the user pressing Ctrl+L — so any per-setting visual
773 // change (image rendering, status copy, future toggles) lands
774 // immediately instead of waiting for the next diff frame.
775 defer func() {
776 i.requestRendererClear()
777 i.invalidate()
778 }()
779 switch key {
780 case "inline_images_enabled":
781 val := value
782 i.cfg.InlineImagesEnabled = &val
783 if i.cfg.SettingsStore != nil {
784 if err := i.cfg.SettingsStore.SetInlineImages(value); err != nil {
785 i.mu.Lock()
786 i.statusErr = "settings: " + err.Error()
787 i.mu.Unlock()
788 return
789 }
790 }
791 i.mu.Lock()
792 i.view.ImageProto = effectiveImageProtocol(i.cfg.InlineImagesEnabled)
793 i.view.InvalidateRenderCache()
794 i.statusOK = "inline image rendering " + onOff(value)
795 i.statusErr = ""
796 i.mu.Unlock()
797 case "terminal_alerts_enabled":
798 val := value
799 i.mu.Lock()
800 i.cfg.TerminalAlertsEnabled = &val
801 i.mu.Unlock()
802 if store, ok := i.cfg.SettingsStore.(terminalAlertsSettingsStore); ok {
803 if err := store.SetTerminalAlertsEnabled(value); err != nil {
804 i.mu.Lock()
805 i.statusErr = "settings: " + err.Error()
806 i.mu.Unlock()
807 return
808 }
809 }
810 i.mu.Lock()
811 i.statusOK = "terminal alerts " + onOff(value)
812 i.statusErr = ""
813 i.mu.Unlock()
814 case "terminal_title_enabled":
815 val := value
816 var cancel context.CancelFunc
817 i.mu.Lock()
818 i.cfg.TerminalTitleEnabled = &val
819 if value {
820 i.writeTerminalTitleLocked(i.sessionTitle)
821 } else {
822 cancel = i.titleCancel
823 i.titleCancel = nil
824 i.titleVersion++
825 i.writeTerminalTitleLocked("")
826 }
827 i.mu.Unlock()
828 if cancel != nil {
829 cancel()
830 }
831 if store, ok := i.cfg.SettingsStore.(terminalTitleSettingsStore); ok {
832 if err := store.SetTerminalTitleEnabled(value); err != nil {
833 i.mu.Lock()
834 i.statusErr = "settings: " + err.Error()
835 i.mu.Unlock()
836 return
837 }
838 }
839 i.mu.Lock()
840 i.statusOK = "AI terminal titles " + onOff(value)
841 i.statusErr = ""
842 i.mu.Unlock()
843 case "web_search_enabled":
844 apply := func() { i.applyWebSearchSettingToggle(key, value) }
845 if i.cfg.SessionTransition != nil {
846 i.cfg.SessionTransition(apply)
847 } else {
848 apply()
849 }
850 case "ponytail_enabled":
851 previous := i.ponytailEnabled()
852 store, available := i.cfg.SettingsStore.(ponytailSettingsStore)
853 if !available || i.cfg.RefreshPrompt == nil {
854 i.resetSettingsToggle(key, previous)
855 i.mu.Lock()
856 i.statusOK = ""
857 i.statusErr = "ponytail coding mode unavailable: persistent settings and live prompt refresh are required"
858 i.mu.Unlock()
859 return
860 }
861 if err := store.SetPonytailEnabled(value); err != nil {
862 i.resetSettingsToggle(key, previous)
863 i.mu.Lock()
864 i.statusOK = ""
865 i.statusErr = "settings: " + err.Error()
866 i.mu.Unlock()
867 return
868 }
869 val := value
870 i.cfg.PonytailEnabled = &val
871 if err := i.cfg.RefreshPrompt(); err != nil {
872 errMsg := "settings: ponytail prompt refresh: " + err.Error()
873 if rollbackErr := store.SetPonytailEnabled(previous); rollbackErr != nil {
874 // The first write succeeded, so keep the in-memory setting at
875 // the value that remains durable instead of claiming that the
876 // rollback took effect.
877 i.resetSettingsToggle(key, value)
878 errMsg += "; rollback persistence: " + rollbackErr.Error()
879 if reconcileErr := i.cfg.RefreshPrompt(); reconcileErr != nil {
880 errMsg += "; durable-state refresh: " + reconcileErr.Error()
881 }
882 } else {
883 previousVal := previous
884 i.cfg.PonytailEnabled = &previousVal
885 i.resetSettingsToggle(key, previous)
886 if refreshErr := i.cfg.RefreshPrompt(); refreshErr != nil {
887 errMsg += "; rollback refresh: " + refreshErr.Error()
888 }
889 }
890 i.mu.Lock()
891 i.statusOK = ""
892 i.statusErr = errMsg
893 i.mu.Unlock()
894 return
895 }
896 i.mu.Lock()
897 i.statusOK = "ponytail coding mode " + onOff(value)
898 i.statusErr = ""
899 i.mu.Unlock()
900 case "auto_subagents_enabled":
901 if value && !i.autoSubagentsAvailable() {
902 i.mu.Lock()
903 i.statusOK = ""
904 i.statusErr = "subagent orchestrator unavailable: " + i.autoSubagentsUnavailableHint()
905 i.mu.Unlock()
906 return
907 }
908 i.mu.Lock()
909 previousFlag := i.cfg.AutoSubagentsEnabled
910 previous := previousFlag != nil && *previousFlag
911 val := value
912 i.cfg.AutoSubagentsEnabled = &val
913 i.mu.Unlock()
914 if i.cfg.SettingsStore != nil {
915 if err := i.cfg.SettingsStore.SetAutoSubagents(value); err != nil {
916 i.mu.Lock()
917 i.cfg.AutoSubagentsEnabled = previousFlag
918 i.statusOK = ""
919 i.statusErr = "settings: " + err.Error()
920 i.mu.Unlock()
921 i.resetSettingsToggle(key, previous)
922 return
923 }
924 }
925 // The subagent tools remain available for explicit user requests.
926 // Swap only the system-prompt guidance between strict orchestration and
927 // on-demand delegation.
928 i.applyAutoSubagentsTool()
929 // Also swap the system-prompt addendum so the model knows whether it
930 // should orchestrate proactively or delegate only on user request.
931 i.applyAutoSubagentsSystemPrompt(value)
932 i.mu.Lock()
933 i.statusOK = "subagent orchestrator " + onOff(value)
934 i.statusErr = ""
935 i.mu.Unlock()
936 case "fast_mode":
937 previous := i.cfg.FastMode != nil && *i.cfg.FastMode
938 if err := provider.ValidateFastMode(i.cfg.Provider, value); err != nil {
939 i.resetSettingsToggle(key, previous)
940 i.mu.Lock()
941 i.statusErr = "settings: " + err.Error()
942 i.mu.Unlock()
943 return
944 }
945 if slot := i.cfg.ActiveModelProfile; slot > 0 {
946 _, profileStore := i.cfg.SettingsStore.(modelProfileSettingsStore)
947 if i.cfg.SettingsStore == nil || profileStore {
948 p := QuickModelShortcut{
949 Provider: i.cfg.Provider, Model: i.cfg.Model, Reasoning: i.cfg.Reasoning,
950 FastMode: value,
951 }
952 if !i.persistModelProfile(slot, p, slot) {
953 i.resetSettingsToggle(key, previous)
954 return
955 }
956 i.setLiveFastMode(value)
957 i.mu.Lock()
958 i.statusOK = "fast mode " + onOff(value) + fmt.Sprintf(" — profile %d updated", slot)
959 i.statusErr = ""
960 i.mu.Unlock()
961 return
962 }
963 }
964 if store, ok := i.cfg.SettingsStore.(fastModeSettingsStore); ok {
965 if err := store.SetFastMode(value); err != nil {
966 i.resetSettingsToggle(key, previous)
967 i.mu.Lock()
968 i.statusErr = "settings: " + err.Error()
969 i.mu.Unlock()
970 return
971 }
972 }
973 i.setLiveFastMode(value)
974 i.mu.Lock()
975 i.statusOK = "fast mode " + onOff(value)
976 i.statusErr = ""
977 i.mu.Unlock()
978 case "lsp_enabled":
979 previous := i.cfg.LSPEnabled == nil || *i.cfg.LSPEnabled
980 store, hasStore := i.cfg.SettingsStore.(lspSettingsStore)
981 if hasStore {
982 if err := store.SetLSPEnabled(value); err != nil {
983 i.mu.Lock()
984 i.statusErr = "settings: " + err.Error()
985 i.mu.Unlock()
986 return
987 }
988 }
989 val := value
990 i.cfg.LSPEnabled = &val
991 if i.cfg.RefreshTools != nil {
992 if err := i.cfg.RefreshTools(); err != nil {
993 errMsg := "settings: refresh tools: " + err.Error()
994 rollbackErr := error(nil)
995 if hasStore {
996 rollbackErr = store.SetLSPEnabled(previous)
997 }
998 if rollbackErr != nil {
999 // The first write succeeded, so keep the in-memory setting at
1000 // the value that remains durable instead of claiming that the
1001 // rollback took effect.
1002 errMsg += "; rollback persistence: " + rollbackErr.Error()
1003 if reconcileErr := i.cfg.RefreshTools(); reconcileErr != nil {
1004 errMsg += "; durable-state refresh: " + reconcileErr.Error()
1005 }
1006 } else {
1007 previousVal := previous
1008 i.cfg.LSPEnabled = &previousVal
1009 if refreshErr := i.cfg.RefreshTools(); refreshErr != nil {
1010 errMsg += "; rollback refresh: " + refreshErr.Error()
1011 }
1012 }
1013 i.mu.Lock()
1014 i.statusOK = ""
1015 i.statusErr = errMsg
1016 i.mu.Unlock()
1017 return
1018 }
1019 }
1020 i.mu.Lock()
1021 i.statusOK = "lsp in main session " + onOff(value)
1022 i.statusErr = ""
1023 i.mu.Unlock()
1024 case "subagent_lsp_enabled":
1025 if store, ok := i.cfg.SettingsStore.(lspSettingsStore); ok {
1026 if err := store.SetSubagentLSPEnabled(value); err != nil {
1027 i.mu.Lock()
1028 i.statusErr = "settings: " + err.Error()
1029 i.mu.Unlock()
1030 return
1031 }
1032 }
1033 val := value
1034 i.cfg.SubagentLSPEnabled = &val
1035 i.mu.Lock()
1036 i.statusOK = "lsp in sub-agents " + onOff(value)
1037 i.statusErr = ""
1038 i.mu.Unlock()
1039 case "jail_by_default":
1040 val := value
1041 i.cfg.JailByDefault = &val
1042 if i.cfg.SettingsStore != nil {
1043 if err := i.cfg.SettingsStore.SetJailByDefault(value); err != nil {
1044 i.mu.Lock()
1045 i.statusErr = "settings: " + err.Error()
1046 i.mu.Unlock()
1047 return
1048 }
1049 }
1050 if i.cfg.Sandbox != nil {
1051 if value {
1052 i.cfg.Sandbox.Lock()
1053 } else {
1054 i.cfg.Sandbox.Unlock()
1055 }
1056 }
1057 i.mu.Lock()
1058 i.statusOK = "jail by default " + onOff(value)
1059 i.statusErr = ""
1060 i.mu.Unlock()
1061 case "recursive_file_suggest":
1062 val := value
1063 i.cfg.RecursiveFileSuggest = &val
1064 if i.cfg.SettingsStore != nil {
1065 if err := i.cfg.SettingsStore.SetRecursiveFileSuggest(value); err != nil {
1066 i.mu.Lock()
1067 i.statusErr = "settings: " + err.Error()
1068 i.mu.Unlock()
1069 return
1070 }
1071 }
1072 // Flip the live picker so the next @ reflects the new mode
1073 // without restarting zut. SetRecursive drops its cache.
1074 i.fileSuggest.SetRecursive(value)
1075 i.mu.Lock()
1076 i.statusOK = "recursive @-file search " + onOff(value)
1077 i.statusErr = ""
1078 i.mu.Unlock()
1079 case "respect_gitignore":
1080 val := value
1081 i.cfg.RespectGitignore = &val
1082 if i.cfg.SettingsStore != nil {
1083 if err := i.cfg.SettingsStore.SetRespectGitignore(value); err != nil {
1084 i.mu.Lock()
1085 i.statusErr = "settings: " + err.Error()
1086 i.mu.Unlock()
1087 return
1088 }
1089 }
1090 i.fileSuggest.SetRespectGitignore(value)
1091 i.mu.Lock()
1092 i.statusOK = "hide gitignored files in @-picker " + onOff(value)
1093 i.statusErr = ""
1094 i.mu.Unlock()
1095 case "compact_mode":
1096 val := value
1097 i.cfg.CompactMode = &val
1098 if i.cfg.SettingsStore != nil {
1099 if err := i.cfg.SettingsStore.SetCompactMode(value); err != nil {
1100 i.mu.Lock()
1101 i.statusErr = "settings: " + err.Error()
1102 i.mu.Unlock()
1103 return
1104 }
1105 }
1106 i.mu.Lock()
1107 i.view.CompactMode = value
1108 i.view.InvalidateRenderCache()
1109 i.statusOK = "compact transcript rendering " + onOff(value)
1110 i.statusErr = ""
1111 i.mu.Unlock()
1112 case "show_instructions_at_startup":
1113 val := value
1114 i.cfg.ShowInstructionsAtStartup = &val
1115 if store, ok := i.cfg.SettingsStore.(showInstructionsSettingsStore); ok {
1116 if err := store.SetShowInstructionsAtStartup(value); err != nil {
1117 i.mu.Lock()
1118 i.statusErr = "settings: " + err.Error()
1119 i.mu.Unlock()
1120 return
1121 }
1122 }
1123 i.mu.Lock()
1124 i.view.StartupAgentName = ""
1125 i.view.StartupContextPaths = nil
1126 i.view.StartupExtensionNames = nil
1127 i.view.StartupSkillNames = nil
1128 i.view.InvalidateRenderCache()
1129 if value {
1130 i.view.StartupAgentName = i.cfg.StartupAgentName
1131 i.view.StartupContextPaths = append(i.view.StartupContextPaths, i.cfg.StartupContextPaths...)
1132 i.view.StartupExtensionNames = append(i.view.StartupExtensionNames, i.cfg.StartupExtensionNames...)
1133 i.view.StartupSkillNames = append(i.view.StartupSkillNames, i.cfg.StartupSkillNames...)
1134 }
1135 i.statusOK = "show loaded resources at startup " + onOff(value)
1136 i.statusErr = ""
1137 i.mu.Unlock()
1138 }
1139}
1140func (i *Interactive) applyTUIInputStyleSetting(style string) {
1141 defer func() {
1142 i.requestRendererClear()
1143 i.invalidate()
1144 }()
1145 style = tui.NormalizeInputStyle(style)
1146 i.cfg.TUIInputStyle = style
1147 i.applyInputCursorColor()
1148 if i.cfg.SettingsStore != nil {
1149 if err := i.cfg.SettingsStore.SetTUIInputStyle(style); err != nil {
1150 i.mu.Lock()
1151 i.statusErr = "settings: " + err.Error()
1152 i.mu.Unlock()
1153 return
1154 }
1155 }
1156 i.mu.Lock()
1157 i.statusOK = "input style " + style
1158 i.statusErr = ""
1159 i.mu.Unlock()
1160}
1161func (i *Interactive) applyTUIStatusPositionSetting(position string) {
1162 defer func() {
1163 i.requestRendererClear()
1164 i.invalidate()
1165 }()
1166 position = tui.NormalizeStatusPosition(position)
1167 i.cfg.TUIStatusPosition = position
1168 if i.cfg.SettingsStore != nil {
1169 if err := i.cfg.SettingsStore.SetTUIStatusPosition(position); err != nil {
1170 i.mu.Lock()
1171 i.statusErr = "settings: " + err.Error()
1172 i.mu.Unlock()
1173 return
1174 }
1175 }
1176 i.mu.Lock()
1177 label := "above input"
1178 if position == tui.StatusPositionBelowInput {
1179 label = "below input"
1180 }
1181 i.statusOK = "status position " + label
1182 i.statusErr = ""
1183 i.mu.Unlock()
1184}
1185func (i *Interactive) applyTUIWorkingPositionSetting(position string) {
1186 defer func() {
1187 i.requestRendererClear()
1188 i.invalidate()
1189 }()
1190 position = tui.NormalizeWorkingPosition(position)
1191 i.cfg.TUIWorkingPosition = position
1192 if i.cfg.SettingsStore != nil {
1193 if err := i.cfg.SettingsStore.SetTUIWorkingPosition(position); err != nil {
1194 i.mu.Lock()
1195 i.statusErr = "settings: " + err.Error()
1196 i.mu.Unlock()
1197 return
1198 }
1199 }
1200 i.mu.Lock()
1201 label := "above input"
1202 if position == tui.WorkingPositionBelowInput {
1203 label = "below input"
1204 }
1205 i.statusOK = "working spinner position " + label
1206 i.statusErr = ""
1207 i.mu.Unlock()
1208}
1209func (i *Interactive) applyThemeSetting(name string) {
1210 if i.cfg.SettingsStore != nil {
1211 if err := i.cfg.SettingsStore.SetTheme(name); err != nil {
1212 i.mu.Lock()
1213 i.statusErr = "settings: " + err.Error()
1214 i.mu.Unlock()
1215 return
1216 }
1217 }
1218 i.cfg.ThemeName = name
1219 if name == "auto" {
1220 i.cfg.ThemeName = ""
1221 }
1222 if !i.cfg.ThemeForced {
1223 i.cfg.EffectiveThemeName = name
1224 }
1225 i.applyThemeNow(i.cfg.EffectiveThemeName)
1226}
1227
1228func (i *Interactive) applyThemeNow(name string) {
1229 if name == "" {
1230 name = "auto"
1231 }
1232 source, err := tui.LoadThemeSource(i.cfg.ZutHome, name)
1233 if err != nil {
1234 if i.cfg.SettingsStore != nil {
1235 _ = i.cfg.SettingsStore.SetTheme("auto")
1236 }
1237 i.cfg.ThemeName = ""
1238 i.cfg.EffectiveThemeName = "auto"
1239 i.activeThemeSource = nil
1240 i.stopThemeWatch()
1241 i.installResolvedTheme(tui.ResolveTheme("auto", nil, i.terminalProfile))
1242 i.mu.Lock()
1243 i.statusErr = "theme missing; reset to auto"
1244 i.mu.Unlock()
1245 return
1246 }
1247 i.activeThemeSource = source
1248 i.restartThemeWatch()
1249 i.installResolvedTheme(tui.ResolveTheme(name, source, i.terminalProfile))
1250 i.mu.Lock()
1251 i.statusOK = "theme " + i.cfg.EffectiveThemeName
1252 if i.cfg.ThemeForced {
1253 i.statusOK += " (forced by ZUT_THEME)"
1254 }
1255 i.statusErr = ""
1256 i.mu.Unlock()
1257}
1258
1259// installResolvedTheme is the one main-loop-owned installation boundary for
1260// settings, terminal profile, and accepted theme-file revisions.
1261// applyTerminalProfile records detected appearance independently from the
1262// currently applied theme. Forced and custom selections are re-resolved from
1263// their immutable accepted source; no terminal event performs filesystem I/O.
1264func (i *Interactive) applyTerminalProfile(profile tui.TerminalProfile) {
1265 i.terminalProfile = profile
1266 i.cfg.TerminalProfile = profile
1267 resolution := tui.ResolveTheme(i.cfg.EffectiveThemeName, i.activeThemeSource, profile)
1268 i.installResolvedTheme(resolution)
1269}
1270
1271func (i *Interactive) installResolvedTheme(resolution tui.ThemeResolution) {
1272 th := resolution.Theme
1273 i.mu.Lock()
1274 i.cfg.Theme = th
1275 i.view.Theme = th
1276 i.view.InvalidateRenderCache()
1277 i.chatCacheValid = false
1278 i.stableChatCacheValid = false
1279 i.ed.Prompt = th.AccentBar(th.Accent)
1280 i.mu.Unlock()
1281 i.btwDialog.setTheme(th)
1282 if i.residentChildSession != nil {
1283 i.residentChildSession.setTheme(th)
1284 }
1285 i.applyInputCursorColor()
1286 i.spin.Configure(th)
1287 i.requestRendererTheme(th)
1288 i.invalidate()
1289}
1290func (i *Interactive) applyReasoningSetting(level string) {
1291 defer func() {
1292 i.requestRendererClear()
1293 i.invalidate()
1294 }()
1295 level = provider.NormalizeReasoning(level)
1296 if slot := i.cfg.ActiveModelProfile; slot > 0 {
1297 p := QuickModelShortcut{
1298 Provider: i.cfg.Provider, Model: i.cfg.Model, Reasoning: level,
1299 FastMode: i.cfg.FastMode != nil && *i.cfg.FastMode,
1300 }
1301 if !i.persistModelProfile(slot, p, slot) {
1302 return
1303 }
1304 } else if i.cfg.SettingsStore != nil {
1305 if err := i.cfg.SettingsStore.SetReasoning(level); err != nil {
1306 i.mu.Lock()
1307 i.statusErr = "settings: " + err.Error()
1308 i.mu.Unlock()
1309 return
1310 }
1311 }
1312 i.setLiveReasoning(level)
1313 i.mu.Lock()
1314 label := level
1315 if label == "" {
1316 label = "off"
1317 }
1318 i.statusOK = "reasoning level " + label
1319 if i.cfg.ActiveModelProfile > 0 {
1320 i.statusOK += fmt.Sprintf(" — profile %d updated", i.cfg.ActiveModelProfile)
1321 }
1322 i.statusErr = ""
1323 i.mu.Unlock()
1324}