1package agent
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/bnema/zut/packages/agent/modes"
8 "github.com/bnema/zut/packages/provider"
9 "github.com/bnema/zut/packages/tui"
10)
11
12type configSettingsStore struct{}
13
14// Retain the original SettingsStore contract for model-only callers.
15func (s configSettingsStore) SetQuickModelShortcut(slot int, providerName, model string) error {
16 if slot < 1 || slot > 9 {
17 return nil
18 }
19 cfg, err := LoadConfig()
20 if err != nil {
21 return err
22 }
23 if cfg.ActiveModelProfile == slot {
24 cfg.ActiveModelProfile = 0
25 }
26 profile := modes.QuickModelShortcut{Provider: providerName, Model: model}
27 if slot <= len(cfg.QuickModelShortcuts) {
28 current := cfg.QuickModelShortcuts[slot-1]
29 profile.Reasoning = current.Reasoning
30 profile.FastMode = current.FastMode
31 }
32 return s.SetModelProfile(slot, profile, cfg.ActiveModelProfile)
33}
34
35func (configSettingsStore) SetModelProfile(slot int, profile modes.QuickModelShortcut, activeSlot int) error {
36 if slot < 1 || slot > 9 || activeSlot < 0 || activeSlot > 9 {
37 return fmt.Errorf("model profile slot must be between 1 and 9 (active may be 0)")
38 }
39 cfg, err := LoadConfig()
40 if err != nil {
41 return err
42 }
43 if len(cfg.QuickModelShortcuts) < slot {
44 next := make([]QuickModelShortcut, slot)
45 copy(next, cfg.QuickModelShortcuts)
46 cfg.QuickModelShortcuts = next
47 }
48 cfg.QuickModelShortcuts[slot-1] = QuickModelShortcut{
49 Provider: profile.Provider, Model: profile.Model,
50 Reasoning: provider.NormalizeReasoning(profile.Reasoning), FastMode: profile.FastMode,
51 }
52 cfg.ActiveModelProfile = activeSlot
53 if activeSlot == slot {
54 if profile.Provider == "" || profile.Model == "" {
55 return fmt.Errorf("cannot activate an empty model profile")
56 }
57 cfg.Provider, cfg.Model = profile.Provider, profile.Model
58 cfg.Reasoning = provider.NormalizeReasoning(profile.Reasoning)
59 fastMode := profile.FastMode
60 cfg.FastMode = &fastMode
61 }
62 // Trim trailing empty slots so config.json stays compact.
63 for len(cfg.QuickModelShortcuts) > 0 {
64 last := cfg.QuickModelShortcuts[len(cfg.QuickModelShortcuts)-1]
65 if last.Provider != "" || last.Model != "" {
66 break
67 }
68 cfg.QuickModelShortcuts = cfg.QuickModelShortcuts[:len(cfg.QuickModelShortcuts)-1]
69 }
70 return SaveConfig(cfg)
71}
72
73func (configSettingsStore) SetInlineImages(enabled bool) error {
74 cfg, err := LoadConfig()
75 if err != nil {
76 return err
77 }
78 cfg.InlineImagesEnabled = &enabled
79 return SaveConfig(cfg)
80}
81
82func (configSettingsStore) SetTerminalAlertsEnabled(enabled bool) error {
83 cfg, err := LoadConfig()
84 if err != nil {
85 return err
86 }
87 cfg.TerminalAlertsEnabled = &enabled
88 return SaveConfig(cfg)
89}
90
91func (configSettingsStore) SetTerminalTitleEnabled(enabled bool) error {
92 cfg, err := LoadConfig()
93 if err != nil {
94 return err
95 }
96 cfg.TerminalTitleEnabled = &enabled
97 return SaveConfig(cfg)
98}
99
100func (configSettingsStore) SetAutoSubagents(enabled bool) error {
101 cfg, err := LoadConfig()
102 if err != nil {
103 return err
104 }
105 cfg.AutoSubagentsEnabled = &enabled
106 return SaveConfig(cfg)
107}
108
109func (configSettingsStore) SetPonytailEnabled(enabled bool) error {
110 cfg, err := LoadConfig()
111 if err != nil {
112 return fmt.Errorf("load config for Ponytail setting: %w", err)
113 }
114 cfg.PonytailEnabled = &enabled
115 if err := SaveConfig(cfg); err != nil {
116 return fmt.Errorf("save Ponytail setting: %w", err)
117 }
118 return nil
119}
120
121func (configSettingsStore) SetWebSearchEnabled(enabled bool) error {
122 cfg, err := LoadConfig()
123 if err != nil {
124 return fmt.Errorf("load config for web search setting: %w", err)
125 }
126 cfg.WebSearchEnabled = &enabled
127 if err := SaveConfig(cfg); err != nil {
128 return fmt.Errorf("save web search setting: %w", err)
129 }
130 return nil
131}
132
133func (configSettingsStore) SetFastMode(enabled bool) error {
134 cfg, err := LoadConfig()
135 if err != nil {
136 return err
137 }
138 cfg.FastMode = &enabled
139 return SaveConfig(cfg)
140}
141
142func (configSettingsStore) SetLSPEnabled(enabled bool) error {
143 cfg, err := LoadConfig()
144 if err != nil {
145 return err
146 }
147 cfg.LSPEnabled = &enabled
148 return SaveConfig(cfg)
149}
150
151func (configSettingsStore) SetSubagentLSPEnabled(enabled bool) error {
152 cfg, err := LoadConfig()
153 if err != nil {
154 return err
155 }
156 cfg.SubagentLSPEnabled = &enabled
157 return SaveConfig(cfg)
158}
159
160func (configSettingsStore) SetAutoCompactThreshold(percent int) error {
161 cfg, err := LoadConfig()
162 if err != nil {
163 return err
164 }
165 cfg.AutoCompactThreshold = &percent
166 return SaveConfig(cfg)
167}
168
169func (configSettingsStore) SetJailByDefault(enabled bool) error {
170 cfg, err := LoadConfig()
171 if err != nil {
172 return err
173 }
174 cfg.JailByDefault = &enabled
175 return SaveConfig(cfg)
176}
177
178func (configSettingsStore) SetRecursiveFileSuggest(enabled bool) error {
179 cfg, err := LoadConfig()
180 if err != nil {
181 return err
182 }
183 cfg.RecursiveFileSuggest = &enabled
184 return SaveConfig(cfg)
185}
186
187func (configSettingsStore) SetRespectGitignore(enabled bool) error {
188 cfg, err := LoadConfig()
189 if err != nil {
190 return err
191 }
192 cfg.RespectGitignore = &enabled
193 return SaveConfig(cfg)
194}
195
196func (configSettingsStore) SetCompactMode(enabled bool) error {
197 cfg, err := LoadConfig()
198 if err != nil {
199 return err
200 }
201 cfg.CompactMode = &enabled
202 return SaveConfig(cfg)
203}
204
205func (configSettingsStore) SetShowInstructionsAtStartup(enabled bool) error {
206 cfg, err := LoadConfig()
207 if err != nil {
208 return err
209 }
210 cfg.ShowInstructionsAtStartup = &enabled
211 return SaveConfig(cfg)
212}
213
214func (configSettingsStore) SetTUIInputStyle(style string) error {
215 cfg, err := LoadConfig()
216 if err != nil {
217 return err
218 }
219 style = tui.NormalizeInputStyle(style)
220 if style == tui.InputStylePlain {
221 cfg.TUIInputStyle = ""
222 } else {
223 cfg.TUIInputStyle = style
224 }
225 return SaveConfig(cfg)
226}
227
228func (configSettingsStore) SetTUIStatusPosition(position string) error {
229 cfg, err := LoadConfig()
230 if err != nil {
231 return err
232 }
233 position = tui.NormalizeStatusPosition(position)
234 if position == tui.StatusPositionAboveInput {
235 cfg.TUIStatusPosition = ""
236 } else {
237 cfg.TUIStatusPosition = position
238 }
239 return SaveConfig(cfg)
240}
241
242func (configSettingsStore) SetTUIWorkingPosition(position string) error {
243 cfg, err := LoadConfig()
244 if err != nil {
245 return err
246 }
247 position = tui.NormalizeWorkingPosition(position)
248 if position == tui.WorkingPositionAboveInput {
249 cfg.TUIWorkingPosition = ""
250 } else {
251 cfg.TUIWorkingPosition = position
252 }
253 return SaveConfig(cfg)
254}
255
256func (configSettingsStore) SetReasoning(level string) error {
257 cfg, err := LoadConfig()
258 if err != nil {
259 return err
260 }
261 cfg.Reasoning = provider.NormalizeReasoning(level)
262 return SaveConfig(cfg)
263}
264
265func (configSettingsStore) SetTheme(name string) error {
266 cfg, err := LoadConfig()
267 if err != nil {
268 return err
269 }
270 if name == "auto" || name == "inherited" {
271 name = ""
272 }
273 cfg.Theme = name
274 return SaveConfig(cfg)
275}
276
277// AutoSubagentsEnabled reads whether the interactive primary agent may
278// delegate proactively. The canonical subagent tools remain available for
279// user-requested delegation and skill-mandated workflows when disabled.
280func AutoSubagentsEnabled() bool {
281 cfg, err := LoadConfig()
282 if err != nil {
283 return false
284 }
285 return cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled
286}
287
288// ProactiveSubagentsRoutingAddendum is prepended to the interactive
289// collaboration contract only when launch-time policy exposes the spawn
290// action: naming a profile is meaningless when the primary cannot dispatch.
291const ProactiveSubagentsRoutingAddendum = `Route before you work:
292- Process start tells you nothing about the task or the workers it needs; routing happens when the user states the request you are about to work on.
293- Name the parts you keep on the critical path and the [subagents_list] profile that owns each delegable part, before you implement or spawn.
294- State that routing once, in the reply that starts the work, and let the tool calls that follow speak for themselves instead of narrating them.
295- Say plainly when nothing is worth delegating instead of inventing sidecars, and spawn a clearly described general worker when no profile matches a scope.`
296
297// ProactiveSubagentsSystemAddendum keeps the interactive primary agent on the
298// critical path and reserves delegation for genuinely parallel sidecar work.
299const ProactiveSubagentsSystemAddendum = `Proactive subagent delegation is enabled. You remain the primary owner and implementer of the user's task.
300
301Before delegating:
302- Form a short high-level plan and identify the immediate next task you will perform locally.
303- Delegate only a concrete, bounded sidecar that can progress independently while you perform useful local work.
304- 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.
305- Divide ownership by distinct question, responsibility, package, or disjoint file set. Give every worker a self-contained task.
306- If you cannot name useful non-overlapping local work, do not delegate.
307
308After delegating:
309- The worker owns its delegated scope until it completes. Continue only with the non-overlapping local task selected before delegation.
310- Do not search, read, test, review, edit, or otherwise reproduce the worker's task. You may dispatch additional workers only for genuinely distinct scopes.
311- 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.
312- Integrate and, when appropriate, verify completed worker results before answering. You may apply a completed worktree patch without redoing the delegated task.`
313
314// StrictOrchestratorSystemAddendum is used only by explicit headless
315// orchestration. The parent coordinates workers and never becomes an
316// implementer itself.
317const StrictOrchestratorSystemAddendum = `Strict subagent orchestration is enabled. You are the primary-agent orchestrator, not an implementer.
318
319- Use read, grep, and ast only for read-only codebase exploration before dispatching work. Prefer ast when the question depends on source syntax. Use web_search only for public-web research that improves worker task instructions.
320- 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.
321- 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, ast, and web_search for the read-only research above and review worker reports.
322- 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.
323- Give every worker a self-contained task and synthesize the results.`
324
325const 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.
326
327Use 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 the wait field on a spawn or resume action: 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.
328
329Completion is host-event-driven unless you explicitly set the wait field on a spawn or resume action to a whole number of seconds (1–300): on spawn it bounds the initial task, and on resume it bounds the follow-up turn. 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 "status" calls, 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.
330
331When 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.`
332
333// OnDemandSubagentsSystemAddendum keeps the canonical subagent tools
334// available without enabling proactive delegation.
335const 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.
336
337Mark delegated work required:true when the user's request or workflow requires its outcome before completion. Omit the wait field on a spawn or resume action 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 task or follow-up turn; 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.`
338
339const StrictOrchestratorDelegationUnavailableAddendum = `Delegation is unavailable in this session because the launch-time tool policy does not expose the subagent spawn action. 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 the status action or other tools as a substitute for spawning.`
340
341const ProactiveSubagentsDelegationUnavailableAddendum = `Proactive delegation is unavailable in this session because the launch-time tool policy does not expose the subagent spawn action. Continue the user's task locally; do not report delegation as a blocker.`
342
343// SubagentActions records which subagent actions the launch-time tool policy
344// exposes, so prompt guidance names only actions the model can call.
345type SubagentActions struct {
346 Spawn, Stop, Resume, Interrupt bool
347}
348
349func (a SubagentActions) lifecycle() bool { return a.Stop || a.Resume || a.Interrupt }
350
351// ProactiveSubagentsSystemAddendumFor returns the interactive collaboration
352// contract with guidance only for manager actions exposed at launch time.
353func ProactiveSubagentsSystemAddendumFor(actions SubagentActions) string {
354 base := ProactiveSubagentsSystemAddendum
355 if actions.Spawn {
356 base = ProactiveSubagentsRoutingAddendum + "\n\n" + base
357 }
358 return subagentsSystemAddendumFor(base, ProactiveSubagentsDelegationUnavailableAddendum, false, actions)
359}
360
361// StrictOrchestratorSystemAddendumFor returns the headless manager-only
362// contract with guidance only for manager actions exposed at launch time.
363func StrictOrchestratorSystemAddendumFor(actions SubagentActions) string {
364 return subagentsSystemAddendumFor(StrictOrchestratorSystemAddendum, StrictOrchestratorDelegationUnavailableAddendum, true, actions)
365}
366
367func subagentsSystemAddendumFor(base, unavailable string, strict bool, actions SubagentActions) string {
368 addendum := base + "\n\n" + subagentLifecycleAddendum
369 var lifecycle []string
370 if actions.Resume {
371 lifecycle = append(lifecycle, "use resume with an agent id and follow-up prompt to continue an idle worker, steer a running worker mid-turn, or restart a stopped worker with its existing session context")
372 }
373 if actions.Interrupt {
374 lifecycle = append(lifecycle, "use interrupt to cancel only a worker's running turn while keeping its context, for example before asking it to wrap up")
375 }
376 if actions.Stop {
377 lifecycle = append(lifecycle, "use stop to request termination of a stuck worker")
378 }
379 if len(lifecycle) != 0 {
380 addendum += "\n\nSubagent manager lifecycle actions available: " + strings.Join(lifecycle, "; ") + "."
381 }
382 if !actions.Spawn {
383 if actions.lifecycle() {
384 addendum += "\n\nSpawning new workers is unavailable in this session. Use only the enabled manager lifecycle actions for existing workers."
385 if strict {
386 addendum += " Do not implement, debug, test, or review directly."
387 } else {
388 addendum += " Continue non-delegated work locally."
389 }
390 } else {
391 addendum += "\n\n" + unavailable
392 }
393 }
394 return addendum
395}