1package agent
2
3import (
4 "fmt"
5
6 "github.com/bnema/zut/packages/agent/modes"
7 "github.com/bnema/zut/packages/provider"
8 "github.com/bnema/zut/packages/tui"
9)
10
11type configSettingsStore struct{}
12
13// Retain the original SettingsStore contract for model-only callers.
14func (s configSettingsStore) SetQuickModelShortcut(slot int, providerName, model string) error {
15 if slot < 1 || slot > 9 {
16 return nil
17 }
18 cfg, err := LoadConfig()
19 if err != nil {
20 return err
21 }
22 if cfg.ActiveModelProfile == slot {
23 cfg.ActiveModelProfile = 0
24 }
25 profile := modes.QuickModelShortcut{Provider: providerName, Model: model}
26 if slot <= len(cfg.QuickModelShortcuts) {
27 current := cfg.QuickModelShortcuts[slot-1]
28 profile.Reasoning = current.Reasoning
29 profile.FastMode = current.FastMode
30 }
31 return s.SetModelProfile(slot, profile, cfg.ActiveModelProfile)
32}
33
34func (configSettingsStore) SetModelProfile(slot int, profile modes.QuickModelShortcut, activeSlot int) error {
35 if slot < 1 || slot > 9 || activeSlot < 0 || activeSlot > 9 {
36 return fmt.Errorf("model profile slot must be between 1 and 9 (active may be 0)")
37 }
38 cfg, err := LoadConfig()
39 if err != nil {
40 return err
41 }
42 if len(cfg.QuickModelShortcuts) < slot {
43 next := make([]QuickModelShortcut, slot)
44 copy(next, cfg.QuickModelShortcuts)
45 cfg.QuickModelShortcuts = next
46 }
47 cfg.QuickModelShortcuts[slot-1] = QuickModelShortcut{
48 Provider: profile.Provider, Model: profile.Model,
49 Reasoning: provider.NormalizeReasoning(profile.Reasoning), FastMode: profile.FastMode,
50 }
51 cfg.ActiveModelProfile = activeSlot
52 if activeSlot == slot {
53 if profile.Provider == "" || profile.Model == "" {
54 return fmt.Errorf("cannot activate an empty model profile")
55 }
56 cfg.Provider, cfg.Model = profile.Provider, profile.Model
57 cfg.Reasoning = provider.NormalizeReasoning(profile.Reasoning)
58 fastMode := profile.FastMode
59 cfg.FastMode = &fastMode
60 }
61 // Trim trailing empty slots so config.json stays compact.
62 for len(cfg.QuickModelShortcuts) > 0 {
63 last := cfg.QuickModelShortcuts[len(cfg.QuickModelShortcuts)-1]
64 if last.Provider != "" || last.Model != "" {
65 break
66 }
67 cfg.QuickModelShortcuts = cfg.QuickModelShortcuts[:len(cfg.QuickModelShortcuts)-1]
68 }
69 return SaveConfig(cfg)
70}
71
72func (configSettingsStore) SetInlineImages(enabled bool) error {
73 cfg, err := LoadConfig()
74 if err != nil {
75 return err
76 }
77 cfg.InlineImagesEnabled = &enabled
78 return SaveConfig(cfg)
79}
80
81func (configSettingsStore) SetTerminalAlertsEnabled(enabled bool) error {
82 cfg, err := LoadConfig()
83 if err != nil {
84 return err
85 }
86 cfg.TerminalAlertsEnabled = &enabled
87 return SaveConfig(cfg)
88}
89
90func (configSettingsStore) SetTerminalTitleEnabled(enabled bool) error {
91 cfg, err := LoadConfig()
92 if err != nil {
93 return err
94 }
95 cfg.TerminalTitleEnabled = &enabled
96 return SaveConfig(cfg)
97}
98
99func (configSettingsStore) SetAutoSubagents(enabled bool) error {
100 cfg, err := LoadConfig()
101 if err != nil {
102 return err
103 }
104 cfg.AutoSubagentsEnabled = &enabled
105 return SaveConfig(cfg)
106}
107
108func (configSettingsStore) SetPonytailEnabled(enabled bool) error {
109 cfg, err := LoadConfig()
110 if err != nil {
111 return fmt.Errorf("load config for Ponytail setting: %w", err)
112 }
113 cfg.PonytailEnabled = &enabled
114 if err := SaveConfig(cfg); err != nil {
115 return fmt.Errorf("save Ponytail setting: %w", err)
116 }
117 return nil
118}
119
120func (configSettingsStore) SetWebSearchEnabled(enabled bool) error {
121 cfg, err := LoadConfig()
122 if err != nil {
123 return fmt.Errorf("load config for web search setting: %w", err)
124 }
125 cfg.WebSearchEnabled = &enabled
126 if err := SaveConfig(cfg); err != nil {
127 return fmt.Errorf("save web search setting: %w", err)
128 }
129 return nil
130}
131
132func (configSettingsStore) SetFastMode(enabled bool) error {
133 cfg, err := LoadConfig()
134 if err != nil {
135 return err
136 }
137 cfg.FastMode = &enabled
138 return SaveConfig(cfg)
139}
140
141func (configSettingsStore) SetLSPEnabled(enabled bool) error {
142 cfg, err := LoadConfig()
143 if err != nil {
144 return err
145 }
146 cfg.LSPEnabled = &enabled
147 return SaveConfig(cfg)
148}
149
150func (configSettingsStore) SetSubagentLSPEnabled(enabled bool) error {
151 cfg, err := LoadConfig()
152 if err != nil {
153 return err
154 }
155 cfg.SubagentLSPEnabled = &enabled
156 return SaveConfig(cfg)
157}
158
159func (configSettingsStore) SetAutoCompactThreshold(percent int) error {
160 cfg, err := LoadConfig()
161 if err != nil {
162 return err
163 }
164 cfg.AutoCompactThreshold = &percent
165 return SaveConfig(cfg)
166}
167
168func (configSettingsStore) SetJailByDefault(enabled bool) error {
169 cfg, err := LoadConfig()
170 if err != nil {
171 return err
172 }
173 cfg.JailByDefault = &enabled
174 return SaveConfig(cfg)
175}
176
177func (configSettingsStore) SetRecursiveFileSuggest(enabled bool) error {
178 cfg, err := LoadConfig()
179 if err != nil {
180 return err
181 }
182 cfg.RecursiveFileSuggest = &enabled
183 return SaveConfig(cfg)
184}
185
186func (configSettingsStore) SetRespectGitignore(enabled bool) error {
187 cfg, err := LoadConfig()
188 if err != nil {
189 return err
190 }
191 cfg.RespectGitignore = &enabled
192 return SaveConfig(cfg)
193}
194
195func (configSettingsStore) SetCompactMode(enabled bool) error {
196 cfg, err := LoadConfig()
197 if err != nil {
198 return err
199 }
200 cfg.CompactMode = &enabled
201 return SaveConfig(cfg)
202}
203
204func (configSettingsStore) SetShowInstructionsAtStartup(enabled bool) error {
205 cfg, err := LoadConfig()
206 if err != nil {
207 return err
208 }
209 cfg.ShowInstructionsAtStartup = &enabled
210 return SaveConfig(cfg)
211}
212
213func (configSettingsStore) SetTUIInputStyle(style string) error {
214 cfg, err := LoadConfig()
215 if err != nil {
216 return err
217 }
218 style = tui.NormalizeInputStyle(style)
219 if style == tui.InputStylePlain {
220 cfg.TUIInputStyle = ""
221 } else {
222 cfg.TUIInputStyle = style
223 }
224 return SaveConfig(cfg)
225}
226
227func (configSettingsStore) SetTUIStatusPosition(position string) error {
228 cfg, err := LoadConfig()
229 if err != nil {
230 return err
231 }
232 position = tui.NormalizeStatusPosition(position)
233 if position == tui.StatusPositionAboveInput {
234 cfg.TUIStatusPosition = ""
235 } else {
236 cfg.TUIStatusPosition = position
237 }
238 return SaveConfig(cfg)
239}
240
241func (configSettingsStore) SetTUIWorkingPosition(position string) error {
242 cfg, err := LoadConfig()
243 if err != nil {
244 return err
245 }
246 position = tui.NormalizeWorkingPosition(position)
247 if position == tui.WorkingPositionAboveInput {
248 cfg.TUIWorkingPosition = ""
249 } else {
250 cfg.TUIWorkingPosition = position
251 }
252 return SaveConfig(cfg)
253}
254
255func (configSettingsStore) SetReasoning(level string) error {
256 cfg, err := LoadConfig()
257 if err != nil {
258 return err
259 }
260 cfg.Reasoning = provider.NormalizeReasoning(level)
261 return SaveConfig(cfg)
262}
263
264func (configSettingsStore) SetTheme(name string) error {
265 cfg, err := LoadConfig()
266 if err != nil {
267 return err
268 }
269 if name == "auto" || name == "inherited" {
270 name = ""
271 }
272 cfg.Theme = name
273 return SaveConfig(cfg)
274}
275
276// AutoSubagentsEnabled reads whether the interactive primary agent may
277// delegate proactively. The canonical subagent tools remain available for
278// user-requested delegation and skill-mandated workflows when disabled.
279func AutoSubagentsEnabled() bool {
280 cfg, err := LoadConfig()
281 if err != nil {
282 return false
283 }
284 return cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled
285}
286
287// ProactiveSubagentsSystemAddendum keeps the interactive primary agent on the
288// critical path and reserves delegation for genuinely parallel sidecar work.
289const ProactiveSubagentsSystemAddendum = `Proactive subagent delegation is enabled. You remain the primary owner and implementer of the user's task.
290
291Before delegating:
292- Form a short high-level plan and identify the immediate next task you will perform locally.
293- Delegate only a concrete, bounded sidecar that can progress independently while you perform useful local work.
294- Keep urgent, tightly coupled, or immediate blocking work local. If your next action depends on a result, do that work yourself instead of spawning and waiting for it.
295- Divide ownership by distinct question, responsibility, package, or disjoint file set. Give every worker a self-contained task.
296- If you cannot name useful non-overlapping local work, do not delegate.
297
298After delegating:
299- The worker owns its delegated scope until it completes. Continue only with the non-overlapping local task selected before delegation.
300- Do not search, read, test, review, edit, or otherwise reproduce the worker's task. You may dispatch additional workers only for genuinely distinct scopes.
301- If the user or an active workflow explicitly requires a worker to own blocking work, delegate it with required:true and end or yield your turn instead of duplicating it.
302- Integrate and, when appropriate, verify completed worker results before answering. You may apply a completed worktree patch without redoing the delegated task.`
303
304// StrictOrchestratorSystemAddendum is used only by explicit headless
305// orchestration. The parent coordinates workers and never becomes an
306// implementer itself.
307const StrictOrchestratorSystemAddendum = `Strict subagent orchestration is enabled. You are the primary-agent orchestrator, not an implementer.
308
309- Use read and grep only for read-only codebase exploration before dispatching work. Use web_search only for public-web research that improves worker task instructions.
310- Divide the request into concrete, bounded, non-overlapping worker scopes before dispatching. Delegate all implementation, debugging/testing, and code-review work to an appropriately named subagent profile or a clearly described general worker.
311- Do not write or edit code yourself, make direct implementation tool calls, inspect or review code through implementation tools, or apply worker patches. You may use read, grep, and web_search for the read-only research above and review worker reports.
312- Once a worker is active, do not investigate or perform its delegated scope. Only coordinate workers, dispatch additional disjoint work, or end or yield the turn.
313- Give every worker a self-contained task and synthesize the results.`
314
315const subagentLifecycleAddendum = `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 and never applied automatically. Do not invent feature scope beyond the user's request. Child workers cannot recursively spawn more sub-agents in v1.
316
317Use required:true whenever your answer or a declared workflow depends on that worker, especially for required validation or finalization. Required workers remain asynchronous unless you explicitly set subagent_spawn's wait field: without it, manager calls return immediately, the parent stays free to coordinate or perform policy-permitted non-overlapping work, and the host delivers terminal outcomes through completion updates. Failed, canceled, or interrupted 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 or restart the worker with a new prompt. Required work has no automatic waiver path. Keep required:false only for independent work that may safely finish later.
318
319Completion is host-event-driven unless you explicitly set subagent_spawn's wait field to a whole number of seconds (1–300) for that initial task. Omitting wait always returns immediately. An expired wait leaves the accepted child active, whether queued or running; do not retry it until terminal failure, cancellation, or interruption. 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 only with genuinely non-overlapping work; otherwise end or yield your turn until the host injects a completion update.
320
321When 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.`
322
323// OnDemandSubagentsSystemAddendum keeps the canonical subagent tools
324// available without enabling proactive delegation.
325const 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. A worker owns its delegated scope until completion: do not repeat that work in the parent. Continue only with a concrete non-overlapping task selected before spawning; if delegation owns the blocking task, end or yield the parent turn instead.
326
327Mark delegated work required:true when the user's request or workflow requires its outcome before completion. Omit subagent_spawn's wait field to return immediately and receive completion through a host update. Set wait only to an explicit 1–300 second value when you need to wait for that initial task; an expired wait leaves the accepted child active, whether queued or running. Do not retry it until terminal failure, cancellation, or interruption. Failed, canceled, or interrupted required work remains unmet until a successful follow-up; 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. Required work has no automatic waiver path. Do not delegate proactively or switch into an orchestrator role; otherwise perform the work yourself.`
328
329const StrictOrchestratorDelegationUnavailableAddendum = `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.`
330
331const ProactiveSubagentsDelegationUnavailableAddendum = `Proactive delegation is unavailable in this session because the launch-time tool policy does not expose subagent_spawn. Continue the user's task locally; do not report delegation as a blocker.`
332
333// ProactiveSubagentsSystemAddendumFor returns the interactive collaboration
334// contract with guidance only for manager actions exposed at launch time.
335func ProactiveSubagentsSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string {
336 return subagentsSystemAddendumFor(ProactiveSubagentsSystemAddendum, ProactiveSubagentsDelegationUnavailableAddendum, false, spawnToolAllowed, stopToolAllowed, resumeToolAllowed)
337}
338
339// StrictOrchestratorSystemAddendumFor returns the headless manager-only
340// contract with guidance only for manager actions exposed at launch time.
341func StrictOrchestratorSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string {
342 return subagentsSystemAddendumFor(StrictOrchestratorSystemAddendum, StrictOrchestratorDelegationUnavailableAddendum, true, spawnToolAllowed, stopToolAllowed, resumeToolAllowed)
343}
344
345func subagentsSystemAddendumFor(base, unavailable string, strict, spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string {
346 addendum := base + "\n\n" + subagentLifecycleAddendum
347 switch {
348 case stopToolAllowed && resumeToolAllowed:
349 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."
350 case stopToolAllowed:
351 addendum += "\n\nA manager lifecycle action is available: use subagent_stop to request termination of a stuck worker."
352 case resumeToolAllowed:
353 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."
354 }
355 if !spawnToolAllowed {
356 if stopToolAllowed || resumeToolAllowed {
357 addendum += "\n\nSpawning new workers is unavailable in this session. Use only the enabled manager lifecycle actions for existing workers."
358 if strict {
359 addendum += " Do not implement, debug, test, or review directly."
360 } else {
361 addendum += " Continue non-delegated work locally."
362 }
363 } else {
364 addendum += "\n\n" + unavailable
365 }
366 }
367 return addendum
368}