v0.0.11 fdef8ac

build.go RawHistoryFolder

1package agent
2
3import (
4	"fmt"
5
6	"github.com/patriceckhart/zot/internal/agent/tools"
7	"github.com/patriceckhart/zot/internal/core"
8	"github.com/patriceckhart/zot/internal/provider"
9)
10
11// Resolved is the effective configuration after merging CLI, config, defaults.
12type Resolved struct {
13	Provider   string
14	Model      string
15	Credential string // api key or oauth access token
16	AuthMethod string // "apikey" | "oauth" | "" (no credential yet)
17	AccountID  string // ChatGPT account id (for openai oauth), "" otherwise
18	BaseURL    string
19	CWD        string
20	Reasoning  string
21
22	ToolRegistry core.Registry
23	ToolSummary  []ToolSummary
24	SystemPrompt string
25	MaxSteps     int
26	Sandbox      *tools.Sandbox
27}
28
29// HasCredential reports whether a credential was resolved.
30func (r Resolved) HasCredential() bool { return r.Credential != "" }
31
32// Resolve merges args, config, and env into a Resolved set.
33//
34// Unlike the earlier version, Resolve NEVER returns an error for
35// missing credentials: the TUI can start without them and launch a
36// login flow. requireCred controls whether missing credentials are a
37// hard error (used by print/json modes).
38func Resolve(args Args, requireCred bool) (Resolved, error) {
39	cfg, _ := LoadConfig()
40
41	// User-requested provider (explicit > config > default).
42	provName := firstNonEmpty(args.Provider, cfg.Provider, "anthropic")
43	if provName != "anthropic" && provName != "openai" {
44		return Resolved{}, fmt.Errorf("provider must be anthropic or openai (got %q)", provName)
45	}
46
47	// Try the requested provider first.
48	cred, method, accountID, credErr := ResolveCredentialFull(provName, args.APIKey)
49
50	// If the user did NOT explicitly pick a provider and the default one
51	// has no credentials, auto-fall-back to whichever provider is actually
52	// logged in. That way running plain `zot` after `/login` (any provider)
53	// never shows a "not logged in" banner.
54	userPickedProvider := args.Provider != ""
55	if credErr != nil && !userPickedProvider {
56		other := "openai"
57		if provName == "openai" {
58			other = "anthropic"
59		}
60		if c, m, a, err := ResolveCredentialFull(other, args.APIKey); err == nil {
61			provName = other
62			cred, method, accountID, credErr = c, m, a, err
63		}
64	}
65
66	model := firstNonEmpty(args.Model, cfg.Model)
67	if model == "" {
68		if provName == "openai" {
69			model = "gpt-5"
70		} else {
71			model = provider.DefaultModel.ID
72		}
73	}
74	// If the resolved model belongs to a different provider (e.g. config
75	// says gpt-5 but we fell back to anthropic), pick that provider's default.
76	if m, err := provider.FindModel("", model); err == nil && m.Provider != provName {
77		if provName == "openai" {
78			model = "gpt-5"
79		} else {
80			model = provider.DefaultModel.ID
81		}
82	}
83	if _, err := provider.FindModel(provName, model); err != nil {
84		return Resolved{}, err
85	}
86
87	if credErr != nil && requireCred {
88		return Resolved{}, fmt.Errorf("%w; set %s_API_KEY, pass --api-key, or run `zot` and /login",
89			credErr, envVarName(provName))
90	}
91
92	sandbox := tools.NewSandbox(args.CWD)
93	reg := buildToolRegistry(args, args.CWD, sandbox)
94	summaries := toolSummaries(reg, args)
95
96	sys := BuildSystemPrompt(SystemPromptOpts{
97		CWD:    args.CWD,
98		Tools:  summaries,
99		Custom: args.SystemPrompt,
100		Append: args.AppendSystemPrompt,
101	})
102
103	reasoning := firstNonEmpty(args.Reasoning, cfg.Reasoning)
104
105	max := args.MaxSteps
106	if max <= 0 {
107		max = 50
108	}
109
110	return Resolved{
111		Provider:     provName,
112		Model:        model,
113		Credential:   cred,
114		AuthMethod:   method,
115		AccountID:    accountID,
116		BaseURL:      args.BaseURL,
117		CWD:          args.CWD,
118		Reasoning:    reasoning,
119		ToolRegistry: reg,
120		ToolSummary:  summaries,
121		SystemPrompt: sys,
122		MaxSteps:     max,
123		Sandbox:      sandbox,
124	}, nil
125}
126
127// NewClient returns a provider.Client for r, choosing the auth mode
128// based on r.AuthMethod. Panics if no credential is present; callers
129// must check HasCredential() first.
130func (r Resolved) NewClient() provider.Client {
131	if !r.HasCredential() {
132		panic("NewClient called without credential; check HasCredential first")
133	}
134	if r.Provider == "openai" {
135		if r.AuthMethod == "oauth" {
136			return provider.NewOpenAICodex(r.Credential, r.AccountID, r.BaseURL)
137		}
138		return provider.NewOpenAI(r.Credential, r.BaseURL)
139	}
140	if r.AuthMethod == "oauth" {
141		return provider.NewAnthropicOAuth(r.Credential, r.BaseURL)
142	}
143	return provider.NewAnthropic(r.Credential, r.BaseURL)
144}
145
146// UseSandbox replaces the sandbox pointer that every tool in r's
147// registry references. Used to keep the /lock state stable across
148// agent rebuilds (e.g. /login, /model switching providers).
149func (r *Resolved) UseSandbox(s *tools.Sandbox) {
150	if s == nil || r == nil {
151		return
152	}
153	r.Sandbox = s
154	for name, t := range r.ToolRegistry {
155		switch v := t.(type) {
156		case *tools.ReadTool:
157			v.Sandbox = s
158		case *tools.WriteTool:
159			v.Sandbox = s
160		case *tools.EditTool:
161			v.Sandbox = s
162		case *tools.BashTool:
163			v.Sandbox = s
164		}
165		_ = name
166	}
167}
168
169// NewAgent constructs a core.Agent from r. Requires a credential.
170func (r Resolved) NewAgent() *core.Agent {
171	a := core.NewAgent(r.NewClient(), r.Model, r.SystemPrompt, r.ToolRegistry)
172	a.MaxSteps = r.MaxSteps
173	a.Reasoning = r.Reasoning
174	return a
175}
176
177func buildToolRegistry(args Args, cwd string, sandbox *tools.Sandbox) core.Registry {
178	if args.NoTools {
179		return core.Registry{}
180	}
181	all := map[string]core.Tool{
182		"read":  &tools.ReadTool{CWD: cwd, Sandbox: sandbox},
183		"write": &tools.WriteTool{CWD: cwd, Sandbox: sandbox},
184		"edit":  &tools.EditTool{CWD: cwd, Sandbox: sandbox},
185		"bash":  &tools.BashTool{CWD: cwd, Sandbox: sandbox},
186	}
187	reg := core.Registry{}
188	if len(args.Tools) == 0 {
189		for _, t := range all {
190			reg[t.Name()] = t
191		}
192		return reg
193	}
194	for _, name := range args.Tools {
195		if t, ok := all[name]; ok {
196			reg[name] = t
197		}
198	}
199	return reg
200}
201
202func toolSummaries(reg core.Registry, args Args) []ToolSummary {
203	order := []string{"read", "write", "edit", "bash"}
204	var out []ToolSummary
205	for _, name := range order {
206		if t, ok := reg[name]; ok {
207			out = append(out, ToolSummary{Name: t.Name(), Description: t.Description()})
208		}
209	}
210	return out
211}
212
213func firstNonEmpty(vals ...string) string {
214	for _, v := range vals {
215		if v != "" {
216			return v
217		}
218	}
219	return ""
220}
221
222func envVarName(provider string) string {
223	switch provider {
224	case "openai":
225		return "OPENAI"
226	default:
227		return "ANTHROPIC"
228	}
229}

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