162 lines
4.4 KiB
Go
162 lines
4.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"reasonix/internal/config"
|
|
)
|
|
|
|
func (m *chatTUI) runEffortCommand(input string) tea.Cmd {
|
|
entry, ref, err := m.currentConfigProvider()
|
|
if err != nil {
|
|
m.notice("effort: " + err.Error())
|
|
return nil
|
|
}
|
|
cap := config.EffortCapabilityForEntry(entry)
|
|
if !cap.Supported {
|
|
m.notice(fmt.Sprintf("effort is not configurable for %s", entry.Name))
|
|
return nil
|
|
}
|
|
|
|
args := tokenizeArgs(input)
|
|
if len(args) < 2 {
|
|
current := config.EffortDisplay(entry)
|
|
options := strings.Join(cap.Levels, "|")
|
|
m.notice(fmt.Sprintf("effort for %s: %s (default: %s; options: %s)", entry.Name, current, cap.Default, options))
|
|
return nil
|
|
}
|
|
if len(args) > 2 {
|
|
m.notice("usage: /effort " + strings.Join(cap.Levels, "|"))
|
|
return nil
|
|
}
|
|
effort, err := config.NormalizeEffort(entry, args[1])
|
|
if err != nil {
|
|
m.notice(err.Error())
|
|
return nil
|
|
}
|
|
if m.buildController == nil {
|
|
m.notice("model switching is unavailable in this session")
|
|
return nil
|
|
}
|
|
if m.ctrl.Running() {
|
|
m.notice("finish or cancel the current turn before changing effort")
|
|
return nil
|
|
}
|
|
if m.modelSwitchPending {
|
|
m.notice("wait for the current runtime switch to finish")
|
|
return nil
|
|
}
|
|
|
|
path := config.UserConfigPath()
|
|
if path == "" {
|
|
m.notice("effort: cannot resolve user config directory")
|
|
return nil
|
|
}
|
|
// Lock only the load-modify-save cycle; the snapshot and controller
|
|
// rebuild below run off-lock.
|
|
if err := func() error {
|
|
unlock := config.LockUserConfigEdits()
|
|
defer unlock()
|
|
edit := config.LoadForEdit(path)
|
|
if _, ok := edit.Provider(entry.Name); !ok {
|
|
if err := edit.UpsertProvider(*entry); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if entry.Kind == "anthropic" && effort != "" && entry.Thinking == "" {
|
|
if err := edit.SetProviderThinking(entry.Name, "adaptive"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := edit.SetProviderEffort(entry.Name, effort); err != nil {
|
|
return err
|
|
}
|
|
return edit.SaveTo(path)
|
|
}(); err != nil {
|
|
m.notice("effort: " + err.Error())
|
|
return nil
|
|
}
|
|
|
|
display := effort
|
|
if display == "" {
|
|
display = "auto"
|
|
}
|
|
m.notice(fmt.Sprintf("setting effort for %s to %s…", entry.Name, display))
|
|
if err := m.ctrl.Snapshot(); err != nil {
|
|
m.notice("effort: snapshot: " + err.Error())
|
|
}
|
|
// Capture the resume path and history only after Snapshot: a snapshot
|
|
// conflict can retarget the controller to a recovery branch (or adopt the
|
|
// newer disk transcript), and a pre-snapshot capture would bind the rebuilt
|
|
// controller back to the original file, re-conflicting on every later save.
|
|
carried := m.ctrl.History()
|
|
prevPath := m.ctrl.SessionPath()
|
|
// Move the lease before the rebuilt controller binds prevPath for writing
|
|
// (AdoptHistory resumes there): after a snapshot retarget the lease still
|
|
// guards the old path, and the async build must not open an unguarded
|
|
// writer on the recovery branch.
|
|
if err := m.rebindSessionLease(prevPath); err != nil {
|
|
m.notice("effort: " + sessionLeaseHeldNotice(err))
|
|
return nil
|
|
}
|
|
oldCtrl := m.ctrl
|
|
build := m.buildController
|
|
m.modelSwitchPending = true
|
|
m.pendingModelSwitch = func() tea.Msg {
|
|
c, err := build(controllerBuildSpec{
|
|
ModelRef: ref,
|
|
RuntimeProfile: m.runtimeProfile,
|
|
ToolApprovalMode: oldCtrl.ToolApprovalMode(),
|
|
PlanMode: oldCtrl.PlanMode(),
|
|
}, carried, prevPath)
|
|
if err != nil {
|
|
return modelSwitchMsg{ref: ref, err: err}
|
|
}
|
|
return modelSwitchMsg{
|
|
ref: ref,
|
|
ctrl: c,
|
|
oldCtrl: oldCtrl,
|
|
label: c.Label(),
|
|
commands: c.Commands(),
|
|
skills: c.SlashSkills(),
|
|
host: c.Host(),
|
|
}
|
|
}
|
|
m.notice(fmt.Sprintf("effort for %s set to %s", entry.Name, display))
|
|
return m.pendingModelSwitch
|
|
}
|
|
|
|
func (m *chatTUI) currentConfigProvider() (*config.ProviderEntry, string, error) {
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
ref := m.modelRef
|
|
if strings.TrimSpace(ref) == "" {
|
|
ref = cfg.DefaultModel
|
|
}
|
|
entry, ok := cfg.ResolveModel(ref)
|
|
if !ok {
|
|
return nil, "", fmt.Errorf("unknown model %q", ref)
|
|
}
|
|
if ref == entry.Name || !strings.Contains(ref, "/") {
|
|
ref = entry.Name + "/" + entry.Model
|
|
}
|
|
return entry, ref, nil
|
|
}
|
|
|
|
func (m *chatTUI) refreshEffortStatus() {
|
|
m.effortLevel = ""
|
|
entry, _, err := m.currentConfigProvider()
|
|
if err != nil {
|
|
return
|
|
}
|
|
if !config.EffortCapabilityForEntry(entry).Supported {
|
|
return
|
|
}
|
|
m.effortLevel = config.EffortDisplay(entry)
|
|
}
|