v0.44.0 efd47b3

settings_store.go RawHistoryFolder

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// ProactiveSubagentsRoutingAddendum is prepended to the interactive
288// collaboration contract only when launch-time policy exposes the spawn
289// action: naming a profile is meaningless when the primary cannot dispatch.
290const ProactiveSubagentsRoutingAddendum = `Route before you work:
291- 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.
292- Name the parts you keep on the critical path and the [subagents_list] profile that owns each delegable part, before you implement or spawn.
293- 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.
294- Say plainly when nothing is worth delegating instead of inventing sidecars, and spawn a clearly described general worker when no profile matches a scope.`
295
296// ProactiveSubagentsSystemAddendum keeps the interactive primary agent on the
297// critical path and reserves delegation for genuinely parallel sidecar work.
298const ProactiveSubagentsSystemAddendum = `Proactive subagent delegation is enabled. You remain the primary owner and implementer of the user's task.
299
300Before delegating:
301- Form a short high-level plan and identify the immediate next task you will perform locally.
302- Delegate only a concrete, bounded sidecar that can progress independently while you perform useful local work.
303- 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.
304- Divide ownership by distinct question, responsibility, package, or disjoint file set. Give every worker a self-contained task.
305- If you cannot name useful non-overlapping local work, do not delegate.
306
307After delegating:
308- The worker owns its delegated scope until it completes. Continue only with the non-overlapping local task selected before delegation.
309- Do not search, read, test, review, edit, or otherwise reproduce the worker's task. You may dispatch additional workers only for genuinely distinct scopes.
310- 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.
311- Integrate and, when appropriate, verify completed worker results before answering. You may apply a completed worktree patch without redoing the delegated task.`
312
313// StrictOrchestratorSystemAddendum is used only by explicit headless
314// orchestration. The parent coordinates workers and never becomes an
315// implementer itself.
316const StrictOrchestratorSystemAddendum = `Strict subagent orchestration is enabled. You are the primary-agent orchestrator, not an implementer.
317
318- 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.
319- 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.
320- 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.
321- 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.
322- Give every worker a self-contained task and synthesize the results.`
323
324const 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.
325
326Use 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.
327
328Completion 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.
329
330When 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.`
331
332// OnDemandSubagentsSystemAddendum keeps the canonical subagent tools
333// available without enabling proactive delegation.
334const 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.
335
336Mark 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.`
337
338const 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.`
339
340const 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.`
341
342// ProactiveSubagentsSystemAddendumFor returns the interactive collaboration
343// contract with guidance only for manager actions exposed at launch time.
344func ProactiveSubagentsSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string {
345	base := ProactiveSubagentsSystemAddendum
346	if spawnToolAllowed {
347		base = ProactiveSubagentsRoutingAddendum + "\n\n" + base
348	}
349	return subagentsSystemAddendumFor(base, ProactiveSubagentsDelegationUnavailableAddendum, false, spawnToolAllowed, stopToolAllowed, resumeToolAllowed)
350}
351
352// StrictOrchestratorSystemAddendumFor returns the headless manager-only
353// contract with guidance only for manager actions exposed at launch time.
354func StrictOrchestratorSystemAddendumFor(spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string {
355	return subagentsSystemAddendumFor(StrictOrchestratorSystemAddendum, StrictOrchestratorDelegationUnavailableAddendum, true, spawnToolAllowed, stopToolAllowed, resumeToolAllowed)
356}
357
358func subagentsSystemAddendumFor(base, unavailable string, strict, spawnToolAllowed, stopToolAllowed, resumeToolAllowed bool) string {
359	addendum := base + "\n\n" + subagentLifecycleAddendum
360	switch {
361	case stopToolAllowed && resumeToolAllowed:
362		addendum += "\n\nManager lifecycle actions are available: use the subagent tool's stop action to request termination of a stuck worker, and its resume action with an agent id and follow-up prompt to continue an idle worker or restart a stopped worker with its existing session context."
363	case stopToolAllowed:
364		addendum += "\n\nA manager lifecycle action is available: use the subagent tool's stop action to request termination of a stuck worker."
365	case resumeToolAllowed:
366		addendum += "\n\nA manager lifecycle action is available: use the subagent tool's resume action with an agent id and follow-up prompt to continue an idle worker or restart a stopped worker with its existing session context."
367	}
368	if !spawnToolAllowed {
369		if stopToolAllowed || resumeToolAllowed {
370			addendum += "\n\nSpawning new workers is unavailable in this session. Use only the enabled manager lifecycle actions for existing workers."
371			if strict {
372				addendum += " Do not implement, debug, test, or review directly."
373			} else {
374				addendum += " Continue non-delegated work locally."
375			}
376		} else {
377			addendum += "\n\n" + unavailable
378		}
379	}
380	return addendum
381}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close