okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffaufrefactor(cli): remove unused state fields5924d14

main
13.1 KiB407 linesraw
1package tui
2
3import (
4	"albedo/cli/internal/config"
5	"albedo/cli/internal/daemon"
6	"context"
7	"errors"
8	"fmt"
9	"maps"
10	"slices"
11	"strings"
12
13	tea "charm.land/bubbletea/v2"
14)
15
16func (m *AppModel) handleCommandResults(msg tea.Msg) (tea.Cmd, bool) {
17	switch msg := msg.(type) {
18	case modelChangedMsg:
19		if msg.Gen != m.ModelGen || m.ActiveSession == nil || msg.SessionID != m.ActiveSession.ID {
20			return nil, true
21		}
22		if msg.Selection != nil {
23			selected := msg.Selection
24			m.ActiveSession.Model, m.Chat.Model = selected.Model, selected.Model
25			m.ActiveSession.Effort, m.Chat.Effort = selected.Effort, selected.Effort
26			m.ActiveSession.Provider, m.Chat.Provider = selected.Provider, selected.Provider
27			m.ActiveSession.Protocol = selected.Protocol
28			m.ActiveSession.ETag = selected.ETag
29			m.updateSession(*m.ActiveSession)
30			if selected.DefaultETag != "" {
31				m.Profiles.ETag = selected.DefaultETag
32				m.Profiles.Active = selected.Provider
33				profile := m.Profiles.Providers[selected.Provider]
34				profile.Model = selected.Model
35				profile.Effort = nil
36				if selected.Effort != "" {
37					profile.Effort = new(selected.Effort)
38				}
39				if m.Profiles.Providers != nil {
40					m.Profiles.Providers[selected.Provider] = profile
41				}
42			}
43			if selected.ModelsETag != "" {
44				if m.SettingsETags == nil {
45					m.SettingsETags = map[string]string{}
46				}
47				m.SettingsETags["models"] = selected.ModelsETag
48			}
49			m.ModelPicker.Saving = false
50			if m.State == AppStateModelPicker {
51				m.State = AppStateChat
52			}
53			m.ClearNotices()
54		}
55		if msg.Err != nil {
56			message := operationError(msg.Err, "Model settings error: ", "Model settings may have changed; check them before trying again.")
57			if m.State == AppStateModelPicker {
58				m.ModelPicker.Saving = false
59				m.ModelPicker.Error = message
60			} else {
61				m.AddError(message)
62			}
63		}
64		return nil, true
65	case commandExecutedMsg:
66		if msg.Gen != m.CommandGen || (msg.SessionID != "" && (m.ActiveSession == nil || msg.SessionID != m.ActiveSession.ID)) {
67			return nil, true
68		}
69		if msg.ModelsETag != "" {
70			if m.SettingsETags == nil {
71				m.SettingsETags = map[string]string{}
72			}
73			m.SettingsETags["models"] = msg.ModelsETag
74		}
75		m.ClearNotices()
76		if len(msg.Available) > 0 && m.State == AppStateChat {
77			m.Chat.openEffortSelector(msg.Available)
78			return nil, true
79		}
80		if msg.Err != nil {
81			m.AddError(operationError(msg.Err, "Command error: ", msg.Name+" may have run; check its result before running it again."))
82			return nil, true
83		}
84		if msg.Page != nil && m.State == AppStateChat && m.ActiveSession != nil {
85			m.PageView = NewPageViewModel(m.Conn, m.ActiveSession.ID, msg.Name)
86			m.PageView.setDoc(msg.Page)
87			m.PageView.Busy = false
88			m.PageView.SetSize(m.Width, m.Height)
89			m.State = AppStatePageView
90			return nil, true
91		}
92		m.AddNotice(msg.Message)
93		if msg.EffortChanged && m.ActiveSession != nil {
94			m.ActiveSession.Effort = msg.Effort
95			m.ActiveSession.ETag = msg.ETag
96			m.Chat.Effort = msg.Effort
97			m.updateSession(*m.ActiveSession)
98		}
99		return nil, true
100	case profilesLoadedMsg:
101		if msg.Gen != m.ProfileGen {
102			return nil, true
103		}
104		m.ClearNotices()
105		if msg.Err != nil {
106			m.AddError("Could not load providers: " + msg.Err.Error())
107		} else {
108			m.Profiles = msg.Profiles
109			notice := fmt.Sprintf("%s selected for new sessions", msg.Provider)
110			if m.ActiveSession != nil {
111				notice += fmt.Sprintf("; use /model to switch this session from %s", m.ActiveSession.Provider)
112			}
113			m.AddNotice(notice)
114		}
115		if m.StandaloneLogin {
116			return tea.Quit, true
117		}
118		if msg.Err == nil && m.ActiveSession == nil && len(m.Sessions) == 0 {
119			return m.newSessionCmd(), true
120		}
121		if m.ActiveSession == nil {
122			m.State = AppStateSessionPicker
123			m.updateSessionPickerItems()
124			return nil, true
125		}
126		return m.returnToChat(), true
127	}
128	return nil, false
129}
130
131func (m *AppModel) loadProfilesCmd(provider string, gen int) tea.Cmd {
132	conn := m.Conn
133	return func() tea.Msg {
134		p, err := daemon.ProviderProfiles(context.Background(), conn)
135		return profilesLoadedMsg{Profiles: p, Provider: provider, Err: err, Gen: gen}
136	}
137}
138
139func (m *AppModel) loadCommandCatalogCmd(gen int) tea.Cmd {
140	conn, hasSession := m.Conn, m.ActiveSession != nil
141	var sessionID string
142	if hasSession {
143		sessionID = m.ActiveSession.ID
144	}
145	return func() tea.Msg {
146		if conn == nil || !hasSession {
147			return commandCatalogLoadedMsg{Gen: gen}
148		}
149
150		cmds, err := daemon.ListSessionCommands(context.Background(), conn, sessionID)
151		return commandCatalogLoadedMsg{Commands: cmds, Err: err, Gen: gen}
152	}
153}
154
155func (m *AppModel) changeModelCmd(model, provider, effort string, raiseCap *bool, gen int, capKey string) tea.Cmd {
156	conn, hasSession := m.Conn, m.ActiveSession != nil
157	var sessionID, currentProvider, etag string
158	if hasSession {
159		sessionID, currentProvider, etag = m.ActiveSession.ID, m.ActiveSession.Provider, m.ActiveSession.ETag
160	}
161	modelsETag := m.SettingsETags["models"]
162	providersETag := m.Profiles.ETag
163	profiles := maps.Clone(m.Profiles.Providers)
164	saveCap := raiseCap != nil
165	var capEnabled bool
166	if saveCap {
167		capEnabled = *raiseCap
168	}
169	return func() tea.Msg {
170		if conn == nil || !hasSession {
171			return modelChangedMsg{Err: errors.New("no active session or connection"), SessionID: sessionID, Gen: gen}
172		}
173
174		if provider == "" {
175			resolved, selected, resolveErr := daemon.ResolveSessionModel(config.Profiles{Active: currentProvider, Providers: profiles}, model)
176			if resolveErr != nil {
177				return modelChangedMsg{Err: resolveErr, SessionID: sessionID, Gen: gen}
178			}
179			provider, model = resolved, selected
180		}
181		selection, err := daemon.ChangeModel(context.Background(), conn, sessionID, daemon.ModelChangeRequest{Model: model, Provider: provider, Effort: effort, ETag: etag, DefaultETag: providersETag, MakeDefault: true})
182		if selection.Model == "" {
183			return modelChangedMsg{Err: err, SessionID: sessionID, Gen: gen}
184		}
185		if profile, ok := profiles[selection.Provider]; ok {
186			selection.Protocol = profile.Protocol
187		}
188		changed := modelChangedMsg{Selection: &selection, SessionID: sessionID, Gen: gen, Err: err}
189		if err != nil {
190			return changed
191		}
192		// Keep the confirmed switch if the following cap update fails.
193		if saveCap {
194			updatedETag, err := daemon.SetModelContextCap(context.Background(), conn, daemon.ModelContextCapRequest{CapKey: capKey, ETag: modelsETag, Enabled: capEnabled})
195			if err != nil {
196				changed.Err = fmt.Errorf("switched model; context cap update: %w", err)
197				return changed
198			}
199			selection.ModelsETag = updatedETag
200		}
201
202		return changed
203	}
204}
205
206func (m *AppModel) executeCommandCmd(name, args string, gen int) tea.Cmd {
207	var captured daemon.Session
208	if m.ActiveSession != nil {
209		captured = *m.ActiveSession
210	}
211	catalog := slices.Clone(m.CommandCatalog)
212	conn, hasSession := m.Conn, m.ActiveSession != nil
213	modelsETag := m.SettingsETags["models"]
214	currentProfile := profilesForSession(m.Profiles, captured)
215	var sessionID string
216	if hasSession {
217		sessionID = m.ActiveSession.ID
218	}
219	return func() tea.Msg {
220		if conn == nil || !hasSession {
221			return commandExecutedMsg{Name: name, Err: errors.New("no active session or connection"), Gen: gen}
222		}
223
224		snapshot := captured
225		var res daemon.CommandResult
226		var err error
227		var acknowledgedModelsETag string
228		switch name {
229		case "/effort":
230			var effort daemon.EffortResult
231			if args == "" {
232				effort, err = daemon.ReadEffort(context.Background(), conn, snapshot)
233			} else {
234				effort, err = daemon.SelectEffort(context.Background(), conn, snapshot, args)
235			}
236			res.Effort = &effort
237		case "/raise-cap":
238			enabled := true
239			switch strings.TrimSpace(args) {
240			case "", "on", "true":
241			case "off", "false":
242				enabled = false
243			default:
244				err = errors.New("use /raise-cap [on|off]")
245			}
246			if err != nil {
247				break
248			}
249			models, readErr := daemon.ListProfileModels(context.Background(), conn, currentProfile)
250			if readErr != nil {
251				err = readErr
252				break
253			}
254			capKey := ""
255			for _, model := range models {
256				if model.ID == snapshot.Model {
257					capKey = model.CapKey
258					break
259				}
260			}
261			acknowledgedModelsETag, err = daemon.SetModelContextCap(context.Background(), conn, daemon.ModelContextCapRequest{CapKey: capKey, ETag: modelsETag, Enabled: enabled})
262			res.Message = "Context cap preference saved."
263		case "/reload":
264			target := strings.TrimSpace(args)
265			if target == "" {
266				target = "both"
267			}
268			var reloaded daemon.SessionReloadResult
269			reloaded, err = daemon.ReloadSession(context.Background(), conn, sessionID, daemon.ReloadRequest{Target: target})
270			res.Message = reloaded.Message()
271		case "/kernel":
272			var upgraded daemon.KernelUpgradeResult
273			upgraded, err = daemon.UpgradeKernel(context.Background(), conn, sessionID)
274			res.Message = upgraded.Message()
275		case "/compact":
276			var compacted daemon.CompactionResult
277			compacted, err = daemon.CompactSession(context.Background(), conn, sessionID, strings.TrimSpace(args))
278			res.Message = compacted.Message()
279		default:
280			// A name can carry a read and a change; typed text picks the one
281			// that takes arguments.
282			index := slices.IndexFunc(catalog, func(item daemon.SessionCommand) bool {
283				return item.Name == name && args != "" && len(item.Arguments) > 0
284			})
285			if index < 0 {
286				index = slices.IndexFunc(catalog, func(item daemon.SessionCommand) bool { return item.Name == name })
287			}
288			if index < 0 {
289				err = fmt.Errorf("command %s is not in the loaded catalog", name)
290				break
291			}
292			command := catalog[index]
293			arguments, parseErr := daemon.ParseCommandArguments(command, args)
294			if parseErr != nil {
295				err = parseErr
296				break
297			}
298			res, err = daemon.InvokeDeclaredCommand(context.Background(), conn, snapshot, command, arguments)
299		}
300
301		if err != nil {
302			return commandExecutedMsg{Name: name, Err: err, Gen: gen}
303		}
304
305		msg := fmt.Sprintf("%s done", name)
306		var newEffort string
307		if res.Effort != nil {
308			if args == "" {
309				return commandExecutedMsg{Name: name, Available: res.Effort.Available, SessionID: sessionID, Gen: gen}
310			}
311			newEffort = res.Effort.Effort
312			if res.Effort.Message != "" {
313				msg = res.Effort.Message
314			}
315		} else if res.Message != "" {
316			msg = res.Message
317		}
318
319		return commandExecutedMsg{Name: name, Message: msg, Page: res.Page, Effort: newEffort, ModelsETag: acknowledgedModelsETag, ETag: func() string {
320			if res.Effort != nil {
321				return res.Effort.ETag
322			}
323			return ""
324		}(), EffortChanged: res.Effort != nil, SessionID: sessionID, Gen: gen}
325	}
326}
327
328func profilesForSession(profiles config.Profiles, session daemon.Session) config.Settings {
329	profile := profiles.Providers[session.Provider]
330	profile.ProfileName = session.Provider
331	return profile
332}
333
334func (m *AppModel) handleCommandInvocation(msg tea.Msg) (tea.Cmd, bool) {
335	switch msg := msg.(type) {
336	case ChatExecuteCommandMsg:
337		if slices.ContainsFunc(m.CommandCatalog, func(command daemon.SessionCommand) bool {
338			return command.Name == msg.Name && command.Delivery == "input"
339		}) {
340			var commands []tea.Cmd
341			m.Chat.handleSubmittedCommand(strings.TrimSpace(msg.Name+" "+msg.Args), &commands)
342			return tea.Batch(commands...), true
343		}
344		switch msg.Name {
345		case "/login":
346			return m.openLogin(msg.Args), true
347		case "/model":
348			if msg.Args != "" {
349				m.ModelGen++
350				return m.changeModelCmd(msg.Args, "", "", nil, m.ModelGen, ""), true
351			}
352			return m.openModelPicker(), true
353		case "/work", "/paperclips":
354			if m.ActiveSession == nil {
355				return nil, true
356			}
357			if strings.TrimSpace(msg.Args) == "" {
358				return func() tea.Msg { return ChatOpenPageMsg{Command: msg.Name} }, true
359			}
360			m.PageView = NewPageViewModel(m.Conn, m.ActiveSession.ID, msg.Name)
361			m.PageView.SetSize(m.Width, m.Height)
362			m.State = AppStatePageView
363			return m.PageView.shortcutCmd(msg.Args), true
364		case "/link":
365			if m.ActiveSession == nil {
366				return nil, true
367			}
368			action, target, _ := strings.Cut(strings.TrimSpace(msg.Args), " ")
369			if action == "add" && strings.TrimSpace(target) != "" {
370				snapshot := *m.ActiveSession
371				conn := m.Conn
372				m.PageView = NewPageViewModel(conn, snapshot.ID, "/links")
373				m.PageView.SetSize(m.Width, m.Height)
374				m.State = AppStatePageView
375				gen := m.PageView.Generation
376				return func() tea.Msg {
377					doc, err := daemon.PrepareLinkMerge(context.Background(), conn, snapshot, strings.TrimSpace(target))
378					return pageLoadedMsg{Doc: doc, Err: err, Gen: gen}
379				}, true
380			}
381			return func() tea.Msg { return ChatOpenPageMsg{Command: "/links"} }, true
382		case "/new":
383			return m.newSessionCmd(), true
384		case "/agents":
385			return func() tea.Msg { return ChatOpenAgentsMsg{} }, true
386		case "/sessions", "/a":
387			return m.openSessions(), true
388		case "/extensions", "/plugins":
389			return m.openExtensionPicker(), true
390		case "/tree":
391			return m.openTreePicker(), true
392		case "/quota":
393			return m.openQuota(), true
394		case "/cd":
395			if msg.Args == "" || m.ActiveSession == nil {
396				return m.openFolderPicker(nil), true
397			}
398			return moveCmd(daemonFolders{conn: m.Conn, condition: daemon.SessionCondition{ETag: m.ActiveSession.ETag, FamilyRevision: m.ActiveSession.FamilyRevision}}, m.ActiveSession.ID, m.ActiveSession.Workspace, msg.Args, nil), true
399		case "/context":
400			return m.openContextInspector(), true
401		default:
402			m.CommandGen++
403			return m.executeCommandCmd(msg.Name, msg.Args, m.CommandGen), true
404		}
405	}
406	return nil, false
407}