main 4509ec7

settings_store.go RawHistoryFolder

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) SetWebSearchEnabled(enabled bool) error {
110	cfg, err := LoadConfig()
111	if err != nil {
112		return fmt.Errorf("load config for web search setting: %w", err)
113	}
114	cfg.WebSearchEnabled = &enabled
115	if err := SaveConfig(cfg); err != nil {
116		return fmt.Errorf("save web search setting: %w", err)
117	}
118	return nil
119}
120
121func (configSettingsStore) SetFastMode(enabled bool) error {
122	cfg, err := LoadConfig()
123	if err != nil {
124		return err
125	}
126	cfg.FastMode = &enabled
127	return SaveConfig(cfg)
128}
129
130func (configSettingsStore) SetLSPEnabled(enabled bool) error {
131	cfg, err := LoadConfig()
132	if err != nil {
133		return err
134	}
135	cfg.LSPEnabled = &enabled
136	return SaveConfig(cfg)
137}
138
139func (configSettingsStore) SetSubagentLSPEnabled(enabled bool) error {
140	cfg, err := LoadConfig()
141	if err != nil {
142		return err
143	}
144	cfg.SubagentLSPEnabled = &enabled
145	return SaveConfig(cfg)
146}
147
148func (configSettingsStore) SetAutoCompactThreshold(percent int) error {
149	cfg, err := LoadConfig()
150	if err != nil {
151		return err
152	}
153	cfg.AutoCompactThreshold = &percent
154	return SaveConfig(cfg)
155}
156
157func (configSettingsStore) SetJailByDefault(enabled bool) error {
158	cfg, err := LoadConfig()
159	if err != nil {
160		return err
161	}
162	cfg.JailByDefault = &enabled
163	return SaveConfig(cfg)
164}
165
166func (configSettingsStore) SetRecursiveFileSuggest(enabled bool) error {
167	cfg, err := LoadConfig()
168	if err != nil {
169		return err
170	}
171	cfg.RecursiveFileSuggest = &enabled
172	return SaveConfig(cfg)
173}
174
175func (configSettingsStore) SetRespectGitignore(enabled bool) error {
176	cfg, err := LoadConfig()
177	if err != nil {
178		return err
179	}
180	cfg.RespectGitignore = &enabled
181	return SaveConfig(cfg)
182}
183
184func (configSettingsStore) SetCompactMode(enabled bool) error {
185	cfg, err := LoadConfig()
186	if err != nil {
187		return err
188	}
189	cfg.CompactMode = &enabled
190	return SaveConfig(cfg)
191}
192
193func (configSettingsStore) SetShowInstructionsAtStartup(enabled bool) error {
194	cfg, err := LoadConfig()
195	if err != nil {
196		return err
197	}
198	cfg.ShowInstructionsAtStartup = &enabled
199	return SaveConfig(cfg)
200}
201
202func (configSettingsStore) SetTUIInputStyle(style string) error {
203	cfg, err := LoadConfig()
204	if err != nil {
205		return err
206	}
207	style = tui.NormalizeInputStyle(style)
208	if style == tui.InputStylePlain {
209		cfg.TUIInputStyle = ""
210	} else {
211		cfg.TUIInputStyle = style
212	}
213	return SaveConfig(cfg)
214}
215
216func (configSettingsStore) SetTUIStatusPosition(position string) error {
217	cfg, err := LoadConfig()
218	if err != nil {
219		return err
220	}
221	position = tui.NormalizeStatusPosition(position)
222	if position == tui.StatusPositionAboveInput {
223		cfg.TUIStatusPosition = ""
224	} else {
225		cfg.TUIStatusPosition = position
226	}
227	return SaveConfig(cfg)
228}
229
230func (configSettingsStore) SetTUIWorkingPosition(position string) error {
231	cfg, err := LoadConfig()
232	if err != nil {
233		return err
234	}
235	position = tui.NormalizeWorkingPosition(position)
236	if position == tui.WorkingPositionAboveInput {
237		cfg.TUIWorkingPosition = ""
238	} else {
239		cfg.TUIWorkingPosition = position
240	}
241	return SaveConfig(cfg)
242}
243
244func (configSettingsStore) SetReasoning(level string) error {
245	cfg, err := LoadConfig()
246	if err != nil {
247		return err
248	}
249	cfg.Reasoning = provider.NormalizeReasoning(level)
250	return SaveConfig(cfg)
251}
252
253func (configSettingsStore) SetTheme(name string) error {
254	cfg, err := LoadConfig()
255	if err != nil {
256		return err
257	}
258	if name == "auto" || name == "inherited" {
259		name = ""
260	}
261	cfg.Theme = name
262	return SaveConfig(cfg)
263}
264
265// AutoSubagentsEnabled reads whether the interactive primary agent may
266// delegate proactively. The canonical subagent tools remain available for
267// user-requested delegation and skill-mandated workflows when disabled.
268func AutoSubagentsEnabled() bool {
269	cfg, err := LoadConfig()
270	if err != nil {
271		return false
272	}
273	return cfg.AutoSubagentsEnabled != nil && *cfg.AutoSubagentsEnabled
274}
275
276// ProactiveSubagentsRoutingAddendum is prepended to the interactive
277// collaboration contract only when launch-time policy exposes the spawn
278// action: naming a profile is meaningless when the primary cannot dispatch.
279const ProactiveSubagentsRoutingAddendum = `Route every new task before you work:
280- Routing happens when the user states a request, not at process start. Re-route each new request, even late in a long session.
281- In the reply that starts the work, name each part, who owns it, and the [subagents_list] profile for each delegated part. Then spawn; do not narrate the tool calls that follow.
282- Spawn a clearly described general worker when no profile matches a scope.
283- Keep a task fully local only when it is small: an answer, a quick lookup, or an edit of a few lines. Say so in one line instead of forcing a worker.`
284
285// ProactiveSubagentsSystemAddendum makes the interactive primary agent an
286// orchestrator by default: substantial work goes to workers, while small
287// edits, exploration, and integration stay local.
288const ProactiveSubagentsSystemAddendum = `Proactive subagent delegation is enabled. You are the orchestrator of the user's task: delegate by default and keep work local by exception.
289
290Delegate:
291- Implementation, debugging, test writing and running, and code review that spans more than a few lines or more than one file.
292- Research that needs many reads or searches across packages or repositories.
293- Blocking work too. If your next step depends on a worker's result, spawn it with required:true and end or yield your turn; waiting for a worker is expected, not a reason to work locally.
294
295Keep local:
296- Answers, quick lookups, and the short exploration needed to write precise worker tasks.
297- Edits of a few lines in one file, and integrating, applying, or verifying worker results.
298
299Plan parallelism before spawning:
300- Map dependencies first: which parts are independent, which must run in order, and which files each part touches. Run independent parts in parallel and order dependent parts.
301- Split work by distinct question, responsibility, package, or disjoint file set, and give every worker a self-contained task with acceptance checks.
302- One worker editing code: shared workspace is fine. Several workers editing code at once: give each its own tree.
303- For a multi-part feature, create a feature worktree branch, then one sub-worktree per worker with the worktree tool's create action and base set to the feature branch, and tell each worker its exact path. Review each worker's result as a gate, commit on its branch, and merge it into the feature branch.
304- Use isolation:"worktree" for a throwaway parallel attempt whose patch you apply yourself.
305
306Clean up as soon as a worker's result is integrated:
307- Remove merged sub-worktrees and their branches with the worktree tool's cleanup action, setting into to the feature branch and delete_branch to true.
308- Before your final answer, check with the worktree tool's list action that no worktree you created is left behind unless the user should keep it.
309
310When you delegate:
311- A worker owns its scope until it completes. Do not search, read, test, review, edit, or otherwise reproduce it; only coordinate, dispatch disjoint work, or yield.
312- Integrate and verify completed 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 when your answer or a declared workflow depends on a worker. Failed, canceled, or interrupted required work stays unmet until a successful follow-up; never claim completion before it succeeds. After a host restart, do not retry an indeterminate outcome automatically: the user must reconcile it first. Required work has no automatic waiver path.
328
329Completion is host-event-driven: omit the wait field, then continue only non-overlapping work or end or yield your turn; the host injects a completion update. Set wait (1–300 seconds) only to block this turn on a spawn or resume; an expired wait leaves the child active, so do not retry it. Never use "bash sleep", "watch", "tail -f", polling loops, repeated "status" calls, or file checks solely to wait.
330
331Treat [auto-subagents update] and [required-subagents update] messages as observed worker state, not as new user requests; use their agent ID, status, error, and result to coordinate follow-ups.`
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, and to remove an idle isolation:\"worktree\" worker's temporary worktree once its patch is applied")
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}

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