1package agent
2
3import (
4 "fmt"
5
6 "github.com/bnema/zut/packages/provider"
7 "github.com/bnema/zut/packages/tui"
8)
9
10type configSettingsStore struct{}
11
12func (configSettingsStore) SetQuickModelShortcut(slot int, providerName, model string) error {
13 if slot < 1 || slot > 9 {
14 return nil
15 }
16 cfg, err := LoadConfig()
17 if err != nil {
18 return err
19 }
20 if len(cfg.QuickModelShortcuts) < slot {
21 next := make([]QuickModelShortcut, slot)
22 copy(next, cfg.QuickModelShortcuts)
23 cfg.QuickModelShortcuts = next
24 }
25 cfg.QuickModelShortcuts[slot-1] = QuickModelShortcut{Provider: providerName, Model: model}
26 // Trim trailing empty slots so config.json stays compact.
27 for len(cfg.QuickModelShortcuts) > 0 {
28 last := cfg.QuickModelShortcuts[len(cfg.QuickModelShortcuts)-1]
29 if last.Provider != "" || last.Model != "" {
30 break
31 }
32 cfg.QuickModelShortcuts = cfg.QuickModelShortcuts[:len(cfg.QuickModelShortcuts)-1]
33 }
34 return SaveConfig(cfg)
35}
36
37func (configSettingsStore) SetInlineImages(enabled bool) error {
38 cfg, err := LoadConfig()
39 if err != nil {
40 return err
41 }
42 cfg.InlineImagesEnabled = &enabled
43 return SaveConfig(cfg)
44}
45
46func (configSettingsStore) SetTerminalAlertsEnabled(enabled bool) error {
47 cfg, err := LoadConfig()
48 if err != nil {
49 return err
50 }
51 cfg.TerminalAlertsEnabled = &enabled
52 return SaveConfig(cfg)
53}
54
55func (configSettingsStore) SetTerminalTitleEnabled(enabled bool) error {
56 cfg, err := LoadConfig()
57 if err != nil {
58 return err
59 }
60 cfg.TerminalTitleEnabled = &enabled
61 return SaveConfig(cfg)
62}
63
64func (configSettingsStore) SetAutoSubagents(enabled bool) error {
65 cfg, err := LoadConfig()
66 if err != nil {
67 return err
68 }
69 cfg.AutoSubagentsEnabled = &enabled
70 return SaveConfig(cfg)
71}
72
73func (configSettingsStore) SetPonytailEnabled(enabled bool) error {
74 cfg, err := LoadConfig()
75 if err != nil {
76 return fmt.Errorf("load config for Ponytail setting: %w", err)
77 }
78 cfg.PonytailEnabled = &enabled
79 if err := SaveConfig(cfg); err != nil {
80 return fmt.Errorf("save Ponytail setting: %w", err)
81 }
82 return nil
83}
84
85func (configSettingsStore) SetWebSearchEnabled(enabled bool) error {
86 cfg, err := LoadConfig()
87 if err != nil {
88 return fmt.Errorf("load config for web search setting: %w", err)
89 }
90 cfg.WebSearchEnabled = &enabled
91 if err := SaveConfig(cfg); err != nil {
92 return fmt.Errorf("save web search setting: %w", err)
93 }
94 return nil
95}
96
97func (configSettingsStore) SetFastMode(enabled bool) error {
98 cfg, err := LoadConfig()
99 if err != nil {
100 return err
101 }
102 cfg.FastMode = &enabled
103 return SaveConfig(cfg)
104}
105
106func (configSettingsStore) SetLSPEnabled(enabled bool) error {
107 cfg, err := LoadConfig()
108 if err != nil {
109 return err
110 }
111 cfg.LSPEnabled = &enabled
112 return SaveConfig(cfg)
113}
114
115func (configSettingsStore) SetSubagentLSPEnabled(enabled bool) error {
116 cfg, err := LoadConfig()
117 if err != nil {
118 return err
119 }
120 cfg.SubagentLSPEnabled = &enabled
121 return SaveConfig(cfg)
122}
123
124func (configSettingsStore) SetAutoCompactThreshold(percent int) error {
125 cfg, err := LoadConfig()
126 if err != nil {
127 return err
128 }
129 cfg.AutoCompactThreshold = &percent
130 return SaveConfig(cfg)
131}
132
133func (configSettingsStore) SetJailByDefault(enabled bool) error {
134 cfg, err := LoadConfig()
135 if err != nil {
136 return err
137 }
138 cfg.JailByDefault = &enabled
139 return SaveConfig(cfg)
140}
141
142func (configSettingsStore) SetRecursiveFileSuggest(enabled bool) error {
143 cfg, err := LoadConfig()
144 if err != nil {
145 return err
146 }
147 cfg.RecursiveFileSuggest = &enabled
148 return SaveConfig(cfg)
149}
150
151func (configSettingsStore) SetRespectGitignore(enabled bool) error {
152 cfg, err := LoadConfig()
153 if err != nil {
154 return err
155 }
156 cfg.RespectGitignore = &enabled
157 return SaveConfig(cfg)
158}
159
160func (configSettingsStore) SetCompactMode(enabled bool) error {
161 cfg, err := LoadConfig()
162 if err != nil {
163 return err
164 }
165 cfg.CompactMode = &enabled
166 return SaveConfig(cfg)
167}
168
169func (configSettingsStore) SetShowInstructionsAtStartup(enabled bool) error {
170 cfg, err := LoadConfig()
171 if err != nil {
172 return err
173 }
174 cfg.ShowInstructionsAtStartup = &enabled
175 return SaveConfig(cfg)
176}
177
178func (configSettingsStore) SetTUIInputStyle(style string) error {
179 cfg, err := LoadConfig()
180 if err != nil {
181 return err
182 }
183 style = tui.NormalizeInputStyle(style)
184 if style == tui.InputStylePlain {
185 cfg.TUIInputStyle = ""
186 } else {
187 cfg.TUIInputStyle = style
188 }
189 return SaveConfig(cfg)
190}
191
192func (configSettingsStore) SetTUIStatusPosition(position string) error {
193 cfg, err := LoadConfig()
194 if err != nil {
195 return err
196 }
197 position = tui.NormalizeStatusPosition(position)
198 if position == tui.StatusPositionAboveInput {
199 cfg.TUIStatusPosition = ""
200 } else {
201 cfg.TUIStatusPosition = position
202 }
203 return SaveConfig(cfg)
204}
205
206func (configSettingsStore) SetTUIWorkingPosition(position string) error {
207 cfg, err := LoadConfig()
208 if err != nil {
209 return err
210 }
211 position = tui.NormalizeWorkingPosition(position)
212 if position == tui.WorkingPositionAboveInput {
213 cfg.TUIWorkingPosition = ""
214 } else {
215 cfg.TUIWorkingPosition = position
216 }
217 return SaveConfig(cfg)
218}
219
220func (configSettingsStore) SetReasoning(level string) error {
221 cfg, err := LoadConfig()
222 if err != nil {
223 return err
224 }
225 cfg.Reasoning = provider.NormalizeReasoning(level)
226 return SaveConfig(cfg)
227}
228
229func (configSettingsStore) SetTheme(name string) error {
230 cfg, err := LoadConfig()
231 if err != nil {
232 return err
233 }
234 if name == "auto" {
235 name = ""
236 }
237 cfg.Theme = name
238 return SaveConfig(cfg)
239}
240
241// AutoSubagentsEnabled reads whether the interactive primary agent runs in
242// strict orchestrator mode. The canonical subagent tools remain available for
243// user-requested delegation and skill-mandated workflows when disabled.
244func AutoSubagentsEnabled() bool {
245 cfg, err := LoadConfig()
246 if err != nil {
247 return false
248 }
249 return cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled
250}
251
252// AutoSubagentsSystemAddendum is appended to the system prompt when
253// auto-subagents is enabled. It makes the interactive primary agent an
254// orchestrator.
255const AutoSubagentsSystemAddendum = `Auto-subagents are enabled. You are the primary-agent orchestrator, not an implementer.
256
257Delegate all implementation, debugging/testing, and code-review work to an appropriately named subagent profile, or to a clearly described general worker when no profile fits. Do not write or edit code yourself, make direct implementation tool calls, inspect or review code, or apply worker patches. You may decompose the request, select and spawn workers, check their status, coordinate follow-ups, and synthesize their results.
258
259Give every worker a self-contained task because workers start without this conversation's context. Shared-worktree workers edit the same working directory, so coordinate dependent work and avoid conflicting edits. Use isolation:"worktree" for parallel coding when workers need separate trees; its changes are returned as a patch for a worker to integrate, not for you to apply. Do not invent feature scope beyond the user's request. Child workers cannot recursively spawn more sub-agents in v1.
260
261Use required:true whenever your answer or a declared workflow depends on that worker, especially for required validation or finalization. Required workers remain asynchronous: manager calls return immediately, the parent stays free to coordinate or perform independent work, and the host delivers terminal outcomes through completion updates. Failed, timed-out, or canceled required work remains unmet: use an enabled manager follow-up action to retry it and do not claim completion. An indeterminate outcome after host restart must not be retried automatically; the user must inspect durable results and side effects, then explicitly resume, restart, or remove the worker. Only the user can waive required work by explicitly removing the terminal worker with /subagents rm. Keep required:false only for independent work that may safely finish later.
262
263Completion is host-event-driven for every resident child. Never use "bash sleep", "watch", "tail -f", polling loops, repeated "subagent_status", or dashboard, metadata, or file checks solely to wait. Those are not completion signals. Continue unrelated independent tasks; otherwise end or yield your turn until the host injects a completion update. Completion updates are the only completion signal. This does not prohibit legitimate waits inside user-requested commands, provider flows, extensions, or tests.
264
265When workers finish, use the host update's agent ID, status, task, optional error, and final response or tail to coordinate any follow-up and summarize the outcome. Treat [auto-subagents update] and [required-subagents update] messages as observed worker state, not as new user requests.`
266
267// OnDemandSubagentsSystemAddendum keeps the canonical subagent tools
268// available without enabling proactive primary-agent orchestration.
269const OnDemandSubagentsSystemAddendum = `Subagent tools are available only when the user asks you to delegate and the relevant launch-time tool permission allows it, or an active skill workflow requires delegation. Use them only in those cases. Mark delegated work required:true when the user's request or workflow requires its outcome before completion. Every manager call returns immediately and every result arrives through a host completion update, so continue unrelated work rather than polling. Failed, timed-out, or canceled required work remains unmet and must be retried; after host restart, an indeterminate outcome requires user-directed reconciliation of durable results and side effects before non-idempotent work is retried. Do not produce a terminal answer until required work is satisfied. Only the user may waive terminal required work by removing the worker with /subagents rm. Do not delegate proactively or switch into an orchestrator role; otherwise perform the work yourself.`
270
271// AutoSubagentsDelegationUnavailableAddendum explains why the strict
272// orchestrator contract still applies when launch-time policy withholds the
273// spawn tool.
274const AutoSubagentsDelegationUnavailableAddendum = `Delegation is unavailable in this session because the launch-time tool policy does not expose subagent_spawn. Remain the primary-agent orchestrator: report this limitation to the user rather than implementing, debugging/testing, or reviewing directly, and end or yield your turn until delegation is available. Do not treat subagent_status or other tools as a substitute for subagent_spawn.`
275
276// AutoSubagentsSystemAddendumFor returns the strict contract with lifecycle
277// guidance only for manager actions exposed by the launch-time tool policy.
278// It adds explicit limitation guidance when the spawn tool is not available.
279func AutoSubagentsSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string {
280 addendum := AutoSubagentsSystemAddendum
281 switch {
282 case stopToolAllowed && resumeToolAllowed:
283 addendum += "\n\nManager lifecycle actions are available: use subagent_stop to request termination of a stuck worker, and subagent_resume with an agent id and follow-up prompt to continue an idle worker or restart a stopped worker with its existing session context."
284 case stopToolAllowed:
285 addendum += "\n\nA manager lifecycle action is available: use subagent_stop to request termination of a stuck worker."
286 case resumeToolAllowed:
287 addendum += "\n\nA manager lifecycle action is available: use subagent_resume with an agent id and follow-up prompt to continue an idle worker or restart a stopped worker with its existing session context."
288 }
289 if !spawnToolAllowed {
290 if stopToolAllowed || resumeToolAllowed {
291 addendum += "\n\nSpawning new workers is unavailable in this session. Use only the enabled manager lifecycle actions for existing workers. Do not implement, debug, test, or review directly."
292 } else {
293 addendum += "\n\n" + AutoSubagentsDelegationUnavailableAddendum
294 }
295 }
296 return addendum
297}