返回 DeepSeek-Reasonix
effort.go
根目录 / internal / cli / effort.go
1 package cli
2
3 import (
4 "fmt"
5 "strings"
6
7 tea "charm.land/bubbletea/v2"
8
9 "reasonix/internal/config"
10 )
11
12 func (m *chatTUI) runEffortCommand(input string) tea.Cmd {
13 entry, ref, err := m.currentConfigProvider()
14 if err != nil {
15 m.notice("effort: " + err.Error())
16 return nil
17 }
18 cap := config.EffortCapabilityForEntry(entry)
19 if !cap.Supported {
20 m.notice(fmt.Sprintf("effort is not configurable for %s", entry.Name))
21 return nil
22 }
23
24 args := tokenizeArgs(input)
25 if len(args) < 2 {
26 current := config.EffortDisplay(entry)
27 options := strings.Join(cap.Levels, "|")
28 m.notice(fmt.Sprintf("effort for %s: %s (default: %s; options: %s)", entry.Name, current, cap.Default, options))
29 return nil
30 }
31 if len(args) > 2 {
32 m.notice("usage: /effort " + strings.Join(cap.Levels, "|"))
33 return nil
34 }
35 effort, err := config.NormalizeEffort(entry, args[1])
36 if err != nil {
37 m.notice(err.Error())
38 return nil
39 }
40 if m.buildController == nil {
41 m.notice("model switching is unavailable in this session")
42 return nil
43 }
44 if m.runtimeSwitchBusy() {
45 m.notice("finish or cancel active work and stop background jobs before changing effort")
46 return nil
47 }
48 if m.modelSwitchPending {
49 m.notice("wait for the current runtime switch to finish")
50 return nil
51 }
52
53 path := config.UserConfigPath()
54 if path == "" {
55 m.notice("effort: cannot resolve user config directory")
56 return nil
57 }
58 // Lock only the load-modify-save cycle; the snapshot and controller
59 // rebuild below run off-lock.
60 if err := func() error {
61 unlock := config.LockUserConfigEdits()
62 defer unlock()
63 edit := config.LoadForEdit(path)
64 if _, ok := edit.Provider(entry.Name); !ok {
65 if err := edit.UpsertProvider(*entry); err != nil {
66 return err
67 }
68 }
69 if entry.Kind == "anthropic" && effort != "" && entry.Thinking == "" {
70 if err := edit.SetProviderThinking(entry.Name, "adaptive"); err != nil {
71 return err
72 }
73 }
74 if err := edit.SetProviderEffort(entry.Name, effort); err != nil {
75 return err
76 }
77 return edit.SaveTo(path)
78 }(); err != nil {
79 m.notice("effort: " + err.Error())
80 return nil
81 }
82
83 display := effort
84 if display == "" {
85 display = "auto"
86 }
87 m.notice(fmt.Sprintf("setting effort for %s to %s…", entry.Name, display))
88 if err := m.ctrl.Snapshot(); err != nil {
89 m.notice("effort: snapshot: " + err.Error())
90 }
91 // Capture the resume path and history only after Snapshot: a snapshot
92 // conflict can retarget the controller to a recovery branch (or adopt the
93 // newer disk transcript), and a pre-snapshot capture would bind the rebuilt
94 // controller back to the original file, re-conflicting on every later save.
95 carried := m.ctrl.History()
96 prevPath := m.ctrl.SessionPath()
97 // Move the lease before the rebuilt controller binds prevPath for writing
98 // (AdoptHistory resumes there): after a snapshot retarget the lease still
99 // guards the old path, and the async build must not open an unguarded
100 // writer on the recovery branch.
101 if err := m.rebindSessionLease(prevPath); err != nil {
102 m.notice("effort: " + sessionLeaseHeldNotice(err))
103 return nil
104 }
105 oldCtrl := m.ctrl
106 build := m.buildController
107 m.modelSwitchPending = true
108 m.pendingModelSwitch = func() tea.Msg {
109 c, err := build(controllerBuildSpec{
110 ModelRef: ref,
111 RuntimeProfile: m.runtimeProfile,
112 ToolApprovalMode: oldCtrl.ToolApprovalMode(),
113 PlanMode: oldCtrl.PlanMode(),
114 EffortOverride: &effort,
115 }, carried, prevPath, oldCtrl)
116 if err != nil {
117 return modelSwitchMsg{ref: ref, err: err}
118 }
119 return modelSwitchMsg{
120 ref: ref,
121 ctrl: c,
122 oldCtrl: oldCtrl,
123 label: c.Label(),
124 commands: c.Commands(),
125 skills: c.SlashSkills(),
126 host: c.Host(),
127 }
128 }
129 m.notice(fmt.Sprintf("effort for %s set to %s", entry.Name, display))
130 return m.pendingModelSwitch
131 }
132
133 func (m *chatTUI) currentConfigProvider() (*config.ProviderEntry, string, error) {
134 cfg, err := config.Load()
135 if err != nil {
136 return nil, "", err
137 }
138 // When the per-tab ref is empty we are inheriting the configured
139 // default — let resolveModelForCLI fall through a keyless default to
140 // the next configured provider (issue #6996). When m.modelRef is
141 // already set we honor it verbatim: the user picked that model
142 // explicitly (via /model, on the model switcher, or in the bootstrap
143 // step) and we must not silently swap to a different provider just
144 // because the entry happens to be keyless.
145 ref := m.modelRef
146 if strings.TrimSpace(ref) == "" {
147 var rerr error
148 ref, _, rerr = resolveModelForCLI("", cfg)
149 if rerr != nil {
150 return nil, "", rerr
151 }
152 }
153 entry, ok := cfg.ResolveModel(ref)
154 if !ok {
155 return nil, "", fmt.Errorf("unknown model %q", ref)
156 }
157 if ref == entry.Name || !strings.Contains(ref, "/") {
158 ref = entry.Name + "/" + entry.Model
159 }
160 return entry, ref, nil
161 }
162
163 func (m *chatTUI) refreshEffortStatus() {
164 m.effortLevel = ""
165 entry, _, err := m.currentConfigProvider()
166 if err != nil {
167 return
168 }
169 if !config.EffortCapabilityForEntry(entry).Supported {
170 return
171 }
172 m.effortLevel = config.EffortDisplay(entry)
173 }
174
174 lines GO