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" || name == "inherited" {
235 name = ""
236 }
237 cfg.Theme = name
238 return SaveConfig(cfg)
239}
240
241// AutoSubagentsEnabled reads whether the interactive primary agent may
242// delegate proactively. 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// ProactiveSubagentsSystemAddendum keeps the interactive primary agent on the
253// critical path and reserves delegation for genuinely parallel sidecar work.
254const ProactiveSubagentsSystemAddendum = `Proactive subagent delegation is enabled. You remain the primary owner and implementer of the user's task.
255
256Before delegating:
257- Form a short high-level plan and identify the immediate next task you will perform locally.
258- Delegate only a concrete, bounded sidecar that can progress independently while you perform useful local work.
259- 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.
260- Divide ownership by distinct question, responsibility, package, or disjoint file set. Give every worker a self-contained task.
261- If you cannot name useful non-overlapping local work, do not delegate.
262
263After delegating:
264- The worker owns its delegated scope until it completes. Continue only with the non-overlapping local task selected before delegation.
265- Do not search, read, test, review, edit, or otherwise reproduce the worker's task. You may dispatch additional workers only for genuinely distinct scopes.
266- 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.
267- Integrate and, when appropriate, verify completed worker results before answering. You may apply a completed worktree patch without redoing the delegated task.`
268
269// StrictOrchestratorSystemAddendum is used only by explicit headless
270// orchestration. The parent coordinates workers and never becomes an
271// implementer itself.
272const StrictOrchestratorSystemAddendum = `Strict subagent orchestration is enabled. You are the primary-agent orchestrator, not an implementer.
273
274- 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.
275- 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.
276- 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.
277- 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.
278- Give every worker a self-contained task and synthesize the results.`
279
280const 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.
281
282Use 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.
283
284Completion 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.
285
286When 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.`
287
288// OnDemandSubagentsSystemAddendum keeps the canonical subagent tools
289// available without enabling proactive delegation.
290const 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.
291
292Mark 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.`
293
294const 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.`
295
296const 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.`
297
298// ProactiveSubagentsSystemAddendumFor returns the interactive collaboration
299// contract with guidance only for manager actions exposed at launch time.
300func ProactiveSubagentsSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string {
301 return subagentsSystemAddendumFor(ProactiveSubagentsSystemAddendum, ProactiveSubagentsDelegationUnavailableAddendum, false, spawnToolAllowed, stopToolAllowed, resumeToolAllowed)
302}
303
304// StrictOrchestratorSystemAddendumFor returns the headless manager-only
305// contract with guidance only for manager actions exposed at launch time.
306func StrictOrchestratorSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string {
307 return subagentsSystemAddendumFor(StrictOrchestratorSystemAddendum, StrictOrchestratorDelegationUnavailableAddendum, true, spawnToolAllowed, stopToolAllowed, resumeToolAllowed)
308}
309
310func subagentsSystemAddendumFor(base, unavailable string, strict, spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string {
311 addendum := base + "\n\n" + subagentLifecycleAddendum
312 switch {
313 case stopToolAllowed && resumeToolAllowed:
314 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."
315 case stopToolAllowed:
316 addendum += "\n\nA manager lifecycle action is available: use subagent_stop to request termination of a stuck worker."
317 case resumeToolAllowed:
318 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."
319 }
320 if !spawnToolAllowed {
321 if stopToolAllowed || resumeToolAllowed {
322 addendum += "\n\nSpawning new workers is unavailable in this session. Use only the enabled manager lifecycle actions for existing workers."
323 if strict {
324 addendum += " Do not implement, debug, test, or review directly."
325 } else {
326 addendum += " Continue non-delegated work locally."
327 }
328 } else {
329 addendum += "\n\n" + unavailable
330 }
331 }
332 return addendum
333}