176 lines
4.7 KiB
Go
176 lines
4.7 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"reasonix/internal/boot"
|
|
"reasonix/internal/i18n"
|
|
)
|
|
|
|
type workModeOption struct {
|
|
name string
|
|
desc string
|
|
}
|
|
|
|
func runtimeProfileDisplay(profile string) string {
|
|
switch boot.NormalizeTokenMode(profile) {
|
|
case boot.TokenModeEconomy:
|
|
return "economy"
|
|
case boot.TokenModeDelivery:
|
|
return "delivery"
|
|
default:
|
|
return "balanced"
|
|
}
|
|
}
|
|
|
|
func parseWorkMode(value string) (string, bool) {
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case "economy":
|
|
return boot.TokenModeEconomy, true
|
|
case "balanced", boot.TokenModeFull:
|
|
return boot.TokenModeFull, true
|
|
case "delivery":
|
|
return boot.TokenModeDelivery, true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
|
|
func workModeOptions() []workModeOption {
|
|
return []workModeOption{
|
|
{name: "economy", desc: i18n.M.WorkModeEconomyDesc},
|
|
{name: "balanced", desc: i18n.M.WorkModeBalancedDesc},
|
|
{name: "delivery", desc: i18n.M.WorkModeDeliveryDesc},
|
|
}
|
|
}
|
|
|
|
func renderWorkModes(width int, current string) string {
|
|
current = runtimeProfileDisplay(current)
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "%s\n", viewHeader(i18n.M.WorkModeListHeaderFmt, current))
|
|
for _, option := range workModeOptions() {
|
|
status := ""
|
|
if option.name == current {
|
|
status = " " + viewStatus(i18n.M.ArgModelCurrent)
|
|
}
|
|
nameWidth := viewPadWidth(option.name, 10)
|
|
desc := viewCompactText(option.desc, viewBudget(width, 2+nameWidth+1+visibleWidth(status)))
|
|
fmt.Fprintf(&b, " %-*s %s%s\n", nameWidth, option.name, viewMeta(desc), status)
|
|
}
|
|
b.WriteString(viewHint(i18n.M.WorkModeListHint))
|
|
return strings.TrimRight(b.String(), "\n")
|
|
}
|
|
|
|
// runWorkModeCommand changes the runtime profile for the current TUI session.
|
|
// Rebuilding is asynchronous and failure-atomic: the old controller and
|
|
// profile remain active until a fully initialized replacement is ready.
|
|
func (m *chatTUI) runWorkModeCommand(input string) tea.Cmd {
|
|
args := tokenizeArgs(input)
|
|
if len(args) == 1 {
|
|
m.commitLine(renderWorkModes(m.width, m.runtimeProfile))
|
|
return nil
|
|
}
|
|
if len(args) != 2 {
|
|
m.notice(i18n.M.WorkModeUsage)
|
|
return nil
|
|
}
|
|
target, ok := parseWorkMode(args[1])
|
|
if !ok {
|
|
m.notice(i18n.M.WorkModeUsage)
|
|
return nil
|
|
}
|
|
if m.buildController == nil || m.ctrl == nil {
|
|
m.notice(i18n.M.WorkModeSwitchUnavailable)
|
|
return nil
|
|
}
|
|
if m.modelSwitchPending {
|
|
m.notice(i18n.M.RuntimeSwitchPending)
|
|
return nil
|
|
}
|
|
if m.ctrl.Running() || m.pendingApproval != nil || m.chooser != nil || len(m.ctrl.Jobs()) > 0 {
|
|
m.notice(i18n.M.WorkModeSwitchBusy)
|
|
return nil
|
|
}
|
|
if boot.NormalizeTokenMode(m.runtimeProfile) == target {
|
|
m.notice(fmt.Sprintf(i18n.M.WorkModeAlreadyOnFmt, runtimeProfileDisplay(target)))
|
|
return nil
|
|
}
|
|
|
|
if err := m.ctrl.Snapshot(); err != nil {
|
|
m.notice("work-mode: snapshot failed: " + err.Error())
|
|
}
|
|
carried := m.ctrl.History()
|
|
resumePath := m.ctrl.SessionPath()
|
|
if err := m.rebindSessionLease(resumePath); err != nil {
|
|
m.notice("work-mode: " + sessionLeaseHeldNotice(err))
|
|
return nil
|
|
}
|
|
|
|
display := runtimeProfileDisplay(target)
|
|
m.notice(fmt.Sprintf(i18n.M.WorkModeSwitchingFmt, display))
|
|
oldCtrl := m.ctrl
|
|
build := m.buildController
|
|
ref := m.modelRef
|
|
m.modelSwitchPending = true
|
|
m.pendingModelSwitch = func() tea.Msg {
|
|
c, err := build(controllerBuildSpec{
|
|
ModelRef: ref,
|
|
RuntimeProfile: target,
|
|
ToolApprovalMode: oldCtrl.ToolApprovalMode(),
|
|
PlanMode: oldCtrl.PlanMode(),
|
|
}, carried, resumePath)
|
|
if err != nil {
|
|
return modelSwitchMsg{
|
|
ref: ref,
|
|
profile: target,
|
|
failurePrefix: "work-mode",
|
|
err: err,
|
|
}
|
|
}
|
|
return modelSwitchMsg{
|
|
ref: ref,
|
|
profile: target,
|
|
ctrl: c,
|
|
oldCtrl: oldCtrl,
|
|
label: c.Label(),
|
|
commands: c.Commands(),
|
|
skills: c.SlashSkills(),
|
|
host: c.Host(),
|
|
successNotice: fmt.Sprintf(i18n.M.WorkModeSwitchedFmt, display),
|
|
}
|
|
}
|
|
return m.pendingModelSwitch
|
|
}
|
|
|
|
func (m *chatTUI) workModeArgItems(val string) ([]compItem, int, bool) {
|
|
cmdEnd := strings.IndexAny(val, " \t")
|
|
if cmdEnd < 0 {
|
|
return nil, 0, false
|
|
}
|
|
cmd := val[:cmdEnd]
|
|
if cmd != "/work-mode" && cmd != "/profile" {
|
|
return nil, 0, false
|
|
}
|
|
from := strings.LastIndexAny(val, " \t") + 1
|
|
if len(strings.Fields(val[:from])) != 1 {
|
|
return nil, from, true
|
|
}
|
|
query := strings.ToLower(val[from:])
|
|
current := runtimeProfileDisplay(m.runtimeProfile)
|
|
var out []compItem
|
|
for _, option := range workModeOptions() {
|
|
if query != "" && !strings.HasPrefix(option.name, query) {
|
|
continue
|
|
}
|
|
hint := option.desc
|
|
if option.name == current {
|
|
hint = i18n.M.ArgModelCurrent + " · " + hint
|
|
}
|
|
out = append(out, compItem{label: option.name, insert: option.name, hint: hint})
|
|
}
|
|
return out, from, true
|
|
}
|