v0.20.0 29be61c

settings_store.go RawHistoryFolder

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}

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 476

↑↓ moveenter openesc close