Files
2026-07-13 13:00:08 +08:00

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)
}