diff options
Diffstat (limited to 'packages/tui/internal')
| -rw-r--r-- | packages/tui/internal/app/app.go | 95 | ||||
| -rw-r--r-- | packages/tui/internal/commands/command.go | 7 | ||||
| -rw-r--r-- | packages/tui/internal/components/status/status.go | 135 | ||||
| -rw-r--r-- | packages/tui/internal/config/config.go | 21 | ||||
| -rw-r--r-- | packages/tui/internal/tui/tui.go | 11 |
5 files changed, 215 insertions, 54 deletions
diff --git a/packages/tui/internal/app/app.go b/packages/tui/internal/app/app.go index bd16036af..b41bbaeaf 100644 --- a/packages/tui/internal/app/app.go +++ b/packages/tui/internal/app/app.go @@ -23,11 +23,15 @@ import ( type App struct { Info opencode.App + Modes []opencode.Mode + Providers []opencode.Provider Version string StatePath string Config *opencode.Config Client *opencode.Client State *config.State + ModeIndex int + Mode *opencode.Mode Provider *opencode.Provider Model *opencode.Model Session *opencode.Session @@ -64,6 +68,7 @@ func New( ctx context.Context, version string, appInfo opencode.App, + modes []opencode.Mode, httpClient *opencode.Client, model *string, prompt *string, @@ -87,14 +92,33 @@ func New( config.SaveState(appStatePath, appState) } + if appState.ModeModel == nil { + appState.ModeModel = make(map[string]config.ModeModel) + } + if configInfo.Theme != "" { appState.Theme = configInfo.Theme } - if configInfo.Model != "" { - splits := strings.Split(configInfo.Model, "/") - appState.Provider = splits[0] - appState.Model = strings.Join(splits[1:], "/") + var modeIndex int + var mode *opencode.Mode + modeName := "build" + if appState.Mode != "" { + modeName = appState.Mode + } + for i, m := range modes { + if m.Name == modeName { + modeIndex = i + break + } + } + mode = &modes[modeIndex] + + if mode.Model.ModelID != "" { + appState.ModeModel[mode.Name] = config.ModeModel{ + ProviderID: mode.Model.ProviderID, + ModelID: mode.Model.ModelID, + } } if err := theme.LoadThemesFromDirectories( @@ -119,11 +143,14 @@ func New( app := &App{ Info: appInfo, + Modes: modes, Version: version, StatePath: appStatePath, Config: configInfo, State: appState, Client: httpClient, + ModeIndex: modeIndex, + Mode: mode, Session: &opencode.Session{}, Messages: []opencode.MessageUnion{}, Commands: commands.LoadFromConfig(configInfo), @@ -162,6 +189,45 @@ func (a *App) SetClipboard(text string) tea.Cmd { return tea.Sequence(cmds...) } +func (a *App) SwitchMode() (*App, tea.Cmd) { + a.ModeIndex++ + if a.ModeIndex >= len(a.Modes) { + a.ModeIndex = 0 + } + a.Mode = &a.Modes[a.ModeIndex] + + modelID := a.Mode.Model.ModelID + providerID := a.Mode.Model.ProviderID + if modelID == "" { + if model, ok := a.State.ModeModel[a.Mode.Name]; ok { + modelID = model.ModelID + providerID = model.ProviderID + } + } + + if modelID != "" { + for _, provider := range a.Providers { + if provider.ID == providerID { + a.Provider = &provider + for _, model := range provider.Models { + if model.ID == modelID { + a.Model = &model + break + } + } + break + } + } + } + + a.State.Mode = a.Mode.Name + + return a, func() tea.Msg { + a.SaveState() + return nil + } +} + func (a *App) InitializeProvider() tea.Cmd { providersResponse, err := a.Client.Config.Providers(context.Background()) if err != nil { @@ -198,6 +264,14 @@ func (a *App) InitializeProvider() tea.Cmd { return nil } + a.Providers = providers + + // retains backwards compatibility with old state format + if model, ok := a.State.ModeModel[a.State.Mode]; ok { + a.State.Provider = model.ProviderID + a.State.Model = model.ModelID + } + var currentProvider *opencode.Provider var currentModel *opencode.Model for _, provider := range providers { @@ -322,10 +396,14 @@ func (a *App) CompactSession(ctx context.Context) tea.Cmd { a.compactCancel = nil }() - _, err := a.Client.Session.Summarize(compactCtx, a.Session.ID, opencode.SessionSummarizeParams{ - ProviderID: opencode.F(a.Provider.ID), - ModelID: opencode.F(a.Model.ID), - }) + _, err := a.Client.Session.Summarize( + compactCtx, + a.Session.ID, + opencode.SessionSummarizeParams{ + ProviderID: opencode.F(a.Provider.ID), + ModelID: opencode.F(a.Model.ID), + }, + ) if err != nil { if compactCtx.Err() != context.Canceled { slog.Error("Failed to compact session", "error", err) @@ -417,6 +495,7 @@ func (a *App) SendChatMessage( Parts: opencode.F(parts), ProviderID: opencode.F(a.Provider.ID), ModelID: opencode.F(a.Model.ID), + Mode: opencode.F(a.Mode.Name), }) if err != nil { errormsg := fmt.Sprintf("failed to send message: %v", err) diff --git a/packages/tui/internal/commands/command.go b/packages/tui/internal/commands/command.go index 791f74759..1659adb85 100644 --- a/packages/tui/internal/commands/command.go +++ b/packages/tui/internal/commands/command.go @@ -86,6 +86,7 @@ func (r CommandRegistry) Matches(msg tea.KeyPressMsg, leader bool) []Command { const ( AppHelpCommand CommandName = "app_help" + SwitchModeCommand CommandName = "switch_mode" EditorOpenCommand CommandName = "editor_open" SessionNewCommand CommandName = "session_new" SessionListCommand CommandName = "session_list" @@ -153,6 +154,12 @@ func LoadFromConfig(config *opencode.Config) CommandRegistry { Trigger: []string{"help"}, }, { + Name: SwitchModeCommand, + Description: "switch mode", + Keybindings: parseBindings("tab"), + Trigger: []string{"mode"}, + }, + { Name: EditorOpenCommand, Description: "open editor", Keybindings: parseBindings("<leader>e"), diff --git a/packages/tui/internal/components/status/status.go b/packages/tui/internal/components/status/status.go index d0d61b173..0809114d0 100644 --- a/packages/tui/internal/components/status/status.go +++ b/packages/tui/internal/components/status/status.go @@ -7,8 +7,9 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" - "github.com/sst/opencode-sdk-go" + "github.com/charmbracelet/lipgloss/v2/compat" "github.com/sst/opencode/internal/app" + "github.com/sst/opencode/internal/commands" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" ) @@ -55,7 +56,12 @@ func (m statusComponent) logo() string { Render(open + code + version) } -func formatTokensAndCost(tokens float64, contextWindow float64, cost float64, isSubscriptionModel bool) string { +func formatTokensAndCost( + tokens float64, + contextWindow float64, + cost float64, + isSubscriptionModel bool, +) string { // Format tokens in human-readable format (e.g., 110K, 1.2M) var formattedTokens string switch { @@ -104,50 +110,103 @@ func (m statusComponent) View() string { Padding(0, 1). Render(m.cwd) - sessionInfo := "" - if m.app.Session.ID != "" { - tokens := float64(0) - cost := float64(0) - contextWindow := m.app.Model.Limit.Context - - for _, message := range m.app.Messages { - if assistant, ok := message.(opencode.AssistantMessage); ok { - cost += assistant.Cost - usage := assistant.Tokens - if usage.Output > 0 { - if assistant.Summary { - tokens = usage.Output - continue - } - tokens = (usage.Input + - usage.Cache.Write + - usage.Cache.Read + - usage.Output + - usage.Reasoning) - } - } - } - - // Check if current model is a subscription model (cost is 0 for both input and output) - isSubscriptionModel := m.app.Model != nil && - m.app.Model.Cost.Input == 0 && m.app.Model.Cost.Output == 0 - - sessionInfo = styles.NewStyle(). - Foreground(t.TextMuted()). - Background(t.BackgroundElement()). - Padding(0, 1). - Render(formatTokensAndCost(tokens, contextWindow, cost, isSubscriptionModel)) + // sessionInfo := "" + // if m.app.Session.ID != "" { + // tokens := float64(0) + // cost := float64(0) + // contextWindow := m.app.Model.Limit.Context + // + // for _, message := range m.app.Messages { + // if assistant, ok := message.(opencode.AssistantMessage); ok { + // cost += assistant.Cost + // usage := assistant.Tokens + // if usage.Output > 0 { + // if assistant.Summary { + // tokens = usage.Output + // continue + // } + // tokens = (usage.Input + + // usage.Cache.Write + + // usage.Cache.Read + + // usage.Output + + // usage.Reasoning) + // } + // } + // } + // + // // Check if current model is a subscription model (cost is 0 for both input and output) + // isSubscriptionModel := m.app.Model != nil && + // m.app.Model.Cost.Input == 0 && m.app.Model.Cost.Output == 0 + // + // sessionInfo = styles.NewStyle(). + // Foreground(t.TextMuted()). + // Background(t.BackgroundElement()). + // Padding(0, 1). + // Render(formatTokensAndCost(tokens, contextWindow, cost, isSubscriptionModel)) + // } + + var modeBackground compat.AdaptiveColor + var modeForeground compat.AdaptiveColor + switch m.app.ModeIndex { + case 0: + modeBackground = t.BackgroundElement() + modeForeground = t.TextMuted() + case 1: + modeBackground = t.Secondary() + modeForeground = t.BackgroundPanel() + case 2: + modeBackground = t.Accent() + modeForeground = t.BackgroundPanel() + case 3: + modeBackground = t.Success() + modeForeground = t.BackgroundPanel() + case 4: + modeBackground = t.Warning() + modeForeground = t.BackgroundPanel() + case 5: + modeBackground = t.Primary() + modeForeground = t.BackgroundPanel() + case 6: + modeBackground = t.Error() + modeForeground = t.BackgroundPanel() + default: + modeBackground = t.Secondary() + modeForeground = t.BackgroundPanel() + } + + command := m.app.Commands[commands.SwitchModeCommand] + kb := command.Keybindings[0] + key := kb.Key + if kb.RequiresLeader { + key = m.app.Config.Keybinds.Leader + " " + kb.Key } - // diagnostics := styles.Padded().Background(t.BackgroundElement()).Render(m.projectDiagnostics()) + modeStyle := styles.NewStyle().Background(modeBackground).Foreground(modeForeground) + modeNameStyle := modeStyle.Bold(true).Render + modeDescStyle := modeStyle.Render + mode := modeNameStyle(strings.ToUpper(m.app.Mode.Name)) + modeDescStyle(" MODE") + mode = modeStyle. + Padding(0, 1). + BorderLeft(true). + BorderStyle(lipgloss.ThickBorder()). + BorderForeground(modeBackground). + BorderBackground(t.BackgroundPanel()). + Render(mode) + + mode = styles.NewStyle(). + Faint(true). + Background(t.BackgroundPanel()). + Foreground(t.TextMuted()). + Render(key+" ") + + mode space := max( 0, - m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(sessionInfo), + m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(mode), ) spacer := styles.NewStyle().Background(t.BackgroundPanel()).Width(space).Render("") - status := logo + cwd + spacer + sessionInfo + status := logo + cwd + spacer + mode blank := styles.NewStyle().Background(t.Background()).Width(m.width).Render("") return blank + "\n" + status diff --git a/packages/tui/internal/config/config.go b/packages/tui/internal/config/config.go index 3dd6fcf59..7004b85b1 100644 --- a/packages/tui/internal/config/config.go +++ b/packages/tui/internal/config/config.go @@ -16,18 +16,27 @@ type ModelUsage struct { LastUsed time.Time `toml:"last_used"` } +type ModeModel struct { + ProviderID string `toml:"provider_id"` + ModelID string `toml:"model_id"` +} + type State struct { - Theme string `toml:"theme"` - Provider string `toml:"provider"` - Model string `toml:"model"` - RecentlyUsedModels []ModelUsage `toml:"recently_used_models"` - MessagesRight bool `toml:"messages_right"` - SplitDiff bool `toml:"split_diff"` + Theme string `toml:"theme"` + ModeModel map[string]ModeModel `toml:"mode_model"` + Provider string `toml:"provider"` + Model string `toml:"model"` + Mode string `toml:"mode"` + RecentlyUsedModels []ModelUsage `toml:"recently_used_models"` + MessagesRight bool `toml:"messages_right"` + SplitDiff bool `toml:"split_diff"` } func NewState() *State { return &State{ Theme: "opencode", + Mode: "build", + ModeModel: make(map[string]ModeModel), RecentlyUsedModels: make([]ModelUsage, 0), } } diff --git a/packages/tui/internal/tui/tui.go b/packages/tui/internal/tui/tui.go index 770e8ac01..0a075a146 100644 --- a/packages/tui/internal/tui/tui.go +++ b/packages/tui/internal/tui/tui.go @@ -23,6 +23,7 @@ import ( "github.com/sst/opencode/internal/components/modal" "github.com/sst/opencode/internal/components/status" "github.com/sst/opencode/internal/components/toast" + "github.com/sst/opencode/internal/config" "github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" @@ -524,8 +525,10 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case app.ModelSelectedMsg: a.app.Provider = &msg.Provider a.app.Model = &msg.Model - a.app.State.Provider = msg.Provider.ID - a.app.State.Model = msg.Model.ID + a.app.State.ModeModel[a.app.Mode.Name] = config.ModeModel{ + ProviderID: msg.Provider.ID, + ModelID: msg.Model.ID, + } a.app.State.UpdateModelUsage(msg.Provider.ID, msg.Model.ID) a.app.SaveState() case dialog.ThemeSelectedMsg: @@ -823,6 +826,10 @@ func (a appModel) executeCommand(command commands.Command) (tea.Model, tea.Cmd) case commands.AppHelpCommand: helpDialog := dialog.NewHelpDialog(a.app) a.modal = helpDialog + case commands.SwitchModeCommand: + updated, cmd := a.app.SwitchMode() + a.app = updated + cmds = append(cmds, cmd) case commands.EditorOpenCommand: if a.app.IsBusy() { // status.Warn("Agent is working, please wait...") |
