返回 DeepSeek-Reasonix
work_mode.go
根目录 / internal / cli / work_mode.go
1 package cli
2
3 import (
4 "fmt"
5 "strings"
6
7 tea "charm.land/bubbletea/v2"
8
9 "reasonix/internal/boot"
10 "reasonix/internal/i18n"
11 )
12
13 type workModeOption struct {
14 name string
15 desc string
16 }
17
18 func runtimeProfileName(profile string) string {
19 switch boot.NormalizeTokenMode(profile) {
20 case boot.TokenModeEconomy:
21 return "economy"
22 case boot.TokenModeDelivery:
23 return "delivery"
24 default:
25 return "balanced"
26 }
27 }
28
29 func runtimeProfileDisplay(profile string) string {
30 switch boot.NormalizeTokenMode(profile) {
31 case boot.TokenModeEconomy:
32 return i18n.M.WorkModeEconomyLabel
33 case boot.TokenModeDelivery:
34 return i18n.M.WorkModeDeliveryLabel
35 default:
36 return i18n.M.WorkModeBalancedLabel
37 }
38 }
39
40 func parseWorkMode(value string) (string, bool) {
41 switch strings.ToLower(strings.TrimSpace(value)) {
42 case "economy":
43 return boot.TokenModeEconomy, true
44 case "balanced", boot.TokenModeFull:
45 return boot.TokenModeFull, true
46 case "delivery":
47 return boot.TokenModeDelivery, true
48 default:
49 return "", false
50 }
51 }
52
53 func workModeOptions() []workModeOption {
54 return []workModeOption{
55 {name: "economy", desc: i18n.M.WorkModeEconomyDesc},
56 {name: "balanced", desc: i18n.M.WorkModeBalancedDesc},
57 {name: "delivery", desc: i18n.M.WorkModeDeliveryDesc},
58 }
59 }
60
61 func renderWorkModes(width int, current string) string {
62 currentName := runtimeProfileName(current)
63 var b strings.Builder
64 fmt.Fprintf(&b, "%s\n", viewHeader(i18n.M.WorkModeListHeaderFmt, runtimeProfileDisplay(current)))
65 for _, option := range workModeOptions() {
66 status := ""
67 if option.name == currentName {
68 status = " " + viewStatus(i18n.M.ArgModelCurrent)
69 }
70 nameWidth := viewPadWidth(option.name, 10)
71 desc := viewCompactText(option.desc, viewBudget(width, 2+nameWidth+1+visibleWidth(status)))
72 fmt.Fprintf(&b, " %-*s %s%s\n", nameWidth, option.name, viewMeta(desc), status)
73 }
74 b.WriteString(viewHint(i18n.M.WorkModeListHint))
75 return strings.TrimRight(b.String(), "\n")
76 }
77
78 // runWorkModeCommand changes the runtime profile for the current TUI session.
79 // Rebuilding is asynchronous and failure-atomic: the old controller and
80 // profile remain active until a fully initialized replacement is ready.
81 func (m *chatTUI) runWorkModeCommand(input string) tea.Cmd {
82 args := tokenizeArgs(input)
83 if len(args) == 1 {
84 m.commitLine(renderWorkModes(m.width, m.runtimeProfile))
85 return nil
86 }
87 if len(args) != 2 {
88 m.notice(i18n.M.WorkModeUsage)
89 return nil
90 }
91 target, ok := parseWorkMode(args[1])
92 if !ok {
93 m.notice(i18n.M.WorkModeUsage)
94 return nil
95 }
96 if m.buildController == nil || m.ctrl == nil {
97 m.notice(i18n.M.WorkModeSwitchUnavailable)
98 return nil
99 }
100 if m.modelSwitchPending {
101 m.notice(i18n.M.RuntimeSwitchPending)
102 return nil
103 }
104 if m.runtimeSwitchBusy() {
105 m.notice(i18n.M.WorkModeSwitchBusy)
106 return nil
107 }
108 if boot.NormalizeTokenMode(m.runtimeProfile) == target {
109 m.notice(fmt.Sprintf(i18n.M.WorkModeAlreadyOnFmt, runtimeProfileDisplay(target)))
110 return nil
111 }
112
113 if err := m.ctrl.Snapshot(); err != nil {
114 m.notice("work-mode: snapshot failed: " + err.Error())
115 }
116 carried := m.ctrl.History()
117 resumePath := m.ctrl.SessionPath()
118 if err := m.rebindSessionLease(resumePath); err != nil {
119 m.notice("work-mode: " + sessionLeaseHeldNotice(err))
120 return nil
121 }
122
123 display := runtimeProfileDisplay(target)
124 m.notice(fmt.Sprintf(i18n.M.WorkModeSwitchingFmt, display))
125 oldCtrl := m.ctrl
126 build := m.buildController
127 ref := m.modelRef
128 m.modelSwitchPending = true
129 m.pendingModelSwitch = func() tea.Msg {
130 c, err := build(controllerBuildSpec{
131 ModelRef: ref,
132 RuntimeProfile: target,
133 ToolApprovalMode: oldCtrl.ToolApprovalMode(),
134 PlanMode: oldCtrl.PlanMode(),
135 }, carried, resumePath, oldCtrl)
136 if err != nil {
137 return modelSwitchMsg{
138 ref: ref,
139 profile: target,
140 failurePrefix: "work-mode",
141 err: err,
142 }
143 }
144 return modelSwitchMsg{
145 ref: ref,
146 profile: target,
147 ctrl: c,
148 oldCtrl: oldCtrl,
149 label: c.Label(),
150 commands: c.Commands(),
151 skills: c.SlashSkills(),
152 host: c.Host(),
153 successNotice: fmt.Sprintf(i18n.M.WorkModeSwitchedFmt, display),
154 }
155 }
156 return m.pendingModelSwitch
157 }
158
159 func (m *chatTUI) workModeArgItems(val string) ([]compItem, int, bool) {
160 cmdEnd := strings.IndexAny(val, " \t")
161 if cmdEnd < 0 {
162 return nil, 0, false
163 }
164 cmd := val[:cmdEnd]
165 if cmd != "/work-mode" && cmd != "/profile" {
166 return nil, 0, false
167 }
168 from := strings.LastIndexAny(val, " \t") + 1
169 if len(strings.Fields(val[:from])) != 1 {
170 return nil, from, true
171 }
172 query := strings.ToLower(val[from:])
173 current := runtimeProfileName(m.runtimeProfile)
174 var out []compItem
175 for _, option := range workModeOptions() {
176 if query != "" && !strings.HasPrefix(option.name, query) {
177 continue
178 }
179 hint := option.desc
180 if option.name == current {
181 hint = i18n.M.ArgModelCurrent + " · " + hint
182 }
183 out = append(out, compItem{label: option.name, insert: option.name, hint: hint})
184 }
185 return out, from, true
186 }
187
187 lines GO