From dc1947838c2403b63f2c9c83f0729b5f7c946f76 Mon Sep 17 00:00:00 2001 From: adamdottv <2363879+adamdottv@users.noreply.github.com> Date: Sun, 22 Jun 2025 06:09:23 -0500 Subject: [PATCH] fix(tui): cleanup modal visuals --- .../tui/internal/components/dialog/models.go | 220 ++++++------------ .../tui/internal/components/dialog/session.go | 60 ++--- packages/tui/internal/components/list/list.go | 5 +- .../tui/internal/components/modal/modal.go | 4 +- 4 files changed, 97 insertions(+), 192 deletions(-) diff --git a/packages/tui/internal/components/dialog/models.go b/packages/tui/internal/components/dialog/models.go index 786b092c..5da3c9ee 100644 --- a/packages/tui/internal/components/dialog/models.go +++ b/packages/tui/internal/components/dialog/models.go @@ -11,6 +11,7 @@ import ( tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" "github.com/sst/opencode/internal/app" + "github.com/sst/opencode/internal/components/list" "github.com/sst/opencode/internal/components/modal" "github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/styles" @@ -33,20 +34,15 @@ type modelDialog struct { app *app.App availableProviders []client.ProviderInfo provider client.ProviderInfo - - selectedIdx int - width int - height int - scrollOffset int - hScrollOffset int - hScrollPossible bool - - modal *modal.Modal + width int + height int + hScrollOffset int + hScrollPossible bool + modal *modal.Modal + modelList list.List[list.StringItem] } type modelKeyMap struct { - Up key.Binding - Down key.Binding Left key.Binding Right key.Binding Enter key.Binding @@ -54,14 +50,6 @@ type modelKeyMap struct { } var modelKeys = modelKeyMap{ - Up: key.NewBinding( - key.WithKeys("up", "k"), - key.WithHelp("↑", "previous model"), - ), - Down: key.NewBinding( - key.WithKeys("down", "j"), - key.WithHelp("↓", "next model"), - ), Left: key.NewBinding( key.WithKeys("left", "h"), key.WithHelp("←", "scroll left"), @@ -81,15 +69,7 @@ var modelKeys = modelKeyMap{ } func (m *modelDialog) Init() tea.Cmd { - // cfg := config.Get() - // modelInfo := GetSelectedModel(cfg) - // m.availableProviders = getEnabledProviders(cfg) - // m.hScrollPossible = len(m.availableProviders) > 1 - - // m.provider = modelInfo.Provider - // m.hScrollOffset = findProviderIndex(m.availableProviders, m.provider) - - // m.setupModelsForProvider(m.provider) + m.setupModelsForProvider(m.provider.Id) return nil } @@ -97,26 +77,32 @@ func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch { - case key.Matches(msg, modelKeys.Up): - m.moveSelectionUp() - case key.Matches(msg, modelKeys.Down): - m.moveSelectionDown() case key.Matches(msg, modelKeys.Left): if m.hScrollPossible { m.switchProvider(-1) } + return m, nil case key.Matches(msg, modelKeys.Right): if m.hScrollPossible { m.switchProvider(1) } + return m, nil case key.Matches(msg, modelKeys.Enter): + selectedItem, _ := m.modelList.GetSelectedItem() models := m.models() + var selectedModel client.ModelInfo + for _, model := range models { + if model.Name == string(selectedItem) { + selectedModel = model + break + } + } return m, tea.Sequence( util.CmdHandler(modal.CloseModalMsg{}), util.CmdHandler( app.ModelSelectedMsg{ Provider: m.provider, - Model: models[m.selectedIdx], + Model: selectedModel, }), ) case key.Matches(msg, modelKeys.Escape): @@ -127,7 +113,10 @@ func (m *modelDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.height = msg.Height } - return m, nil + // Update the list component + updatedList, cmd := m.modelList.Update(msg) + m.modelList = updatedList.(list.List[list.StringItem]) + return m, cmd } func (m *modelDialog) models() []client.ModelInfo { @@ -137,40 +126,9 @@ func (m *modelDialog) models() []client.ModelInfo { return models } -// moveSelectionUp moves the selection up or wraps to bottom -func (m *modelDialog) moveSelectionUp() { - if m.selectedIdx > 0 { - m.selectedIdx-- - } else { - m.selectedIdx = len(m.provider.Models) - 1 - m.scrollOffset = max(0, len(m.provider.Models)-numVisibleModels) - } - - // Keep selection visible - if m.selectedIdx < m.scrollOffset { - m.scrollOffset = m.selectedIdx - } -} - -// moveSelectionDown moves the selection down or wraps to top -func (m *modelDialog) moveSelectionDown() { - if m.selectedIdx < len(m.provider.Models)-1 { - m.selectedIdx++ - } else { - m.selectedIdx = 0 - m.scrollOffset = 0 - } - - // Keep selection visible - if m.selectedIdx >= m.scrollOffset+numVisibleModels { - m.scrollOffset = m.selectedIdx - (numVisibleModels - 1) - } -} - func (m *modelDialog) switchProvider(offset int) { newOffset := m.hScrollOffset + offset - // Ensure we stay within bounds if newOffset < 0 { newOffset = len(m.availableProviders) - 1 } @@ -185,105 +143,46 @@ func (m *modelDialog) switchProvider(offset int) { } func (m *modelDialog) View() string { - t := theme.CurrentTheme() - baseStyle := lipgloss.NewStyle(). - Background(t.BackgroundElement()). - Foreground(t.Text()) - - // Render visible models - endIdx := min(m.scrollOffset+numVisibleModels, len(m.provider.Models)) - modelItems := make([]string, 0, endIdx-m.scrollOffset) - - models := m.models() - for i := m.scrollOffset; i < endIdx; i++ { - itemStyle := baseStyle.Width(maxDialogWidth) - if i == m.selectedIdx { - itemStyle = itemStyle. - Background(t.Primary()). - Foreground(t.BackgroundElement()). - Bold(true) - } - modelItems = append(modelItems, itemStyle.Render(models[i].Name)) - } - + listView := m.modelList.View() scrollIndicator := m.getScrollIndicators(maxDialogWidth) - - content := lipgloss.JoinVertical( - lipgloss.Left, - baseStyle. - Width(maxDialogWidth). - Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)), - scrollIndicator, - ) - - return content + return strings.Join([]string{listView, scrollIndicator}, "\n") } func (m *modelDialog) getScrollIndicators(maxWidth int) string { var indicator string - - if len(m.provider.Models) > numVisibleModels { - if m.scrollOffset > 0 { - indicator += "↑ " - } - if m.scrollOffset+numVisibleModels < len(m.provider.Models) { - indicator += "↓ " - } - } - if m.hScrollPossible { - indicator = "← " + indicator + "→" + indicator = "← → (switch provider) " } - if indicator == "" { return "" } t := theme.CurrentTheme() - baseStyle := styles.BaseStyle() - - return baseStyle. - Foreground(t.Primary()). + return styles.BaseStyle(). + Foreground(t.TextMuted()). Width(maxWidth). Align(lipgloss.Right). - Bold(true). Render(indicator) } -// findProviderIndex returns the index of the provider in the list, or -1 if not found -// func findProviderIndex(providers []string, provider string) int { -// for i, p := range providers { -// if p == provider { -// return i -// } -// } -// return -1 -// } +func (m *modelDialog) setupModelsForProvider(providerId string) { + models := m.models() + modelNames := make([]string, len(models)) + for i, model := range models { + modelNames[i] = model.Name + } -func (m *modelDialog) setupModelsForProvider(_ string) { - m.selectedIdx = 0 - m.scrollOffset = 0 + m.modelList = list.NewStringList(modelNames, numVisibleModels, "No models available", true) + m.modelList.SetMaxWidth(maxDialogWidth) - // cfg := config.Get() - // agentCfg := cfg.Agents[config.AgentPrimary] - // selectedModelId := agentCfg.Model - - // m.provider = provider - // m.models = getModelsForProvider(provider) - - // Try to select the current model if it belongs to this provider - // if provider == models.SupportedModels[selectedModelId].Provider { - // for i, model := range m.models { - // if model.ID == selectedModelId { - // m.selectedIdx = i - // // Adjust scroll position to keep selected model visible - // if m.selectedIdx >= numVisibleModels { - // m.scrollOffset = m.selectedIdx - (numVisibleModels - 1) - // } - // break - // } - // } - // } + if m.app.Provider != nil && m.app.Model != nil && m.app.Provider.Id == providerId { + for i, model := range models { + if model.Id == m.app.Model.Id { + m.modelList.SetSelectedIndex(i) + break + } + } + } } func (m *modelDialog) Render(background string) string { @@ -297,11 +196,30 @@ func (s *modelDialog) Close() tea.Cmd { func NewModelDialog(app *app.App) ModelDialog { availableProviders, _ := app.ListProviders(context.Background()) - return &modelDialog{ - availableProviders: availableProviders, - hScrollOffset: 0, - hScrollPossible: len(availableProviders) > 1, - provider: availableProviders[0], - modal: modal.New(modal.WithTitle(fmt.Sprintf("Select %s Model", availableProviders[0].Name))), + currentProvider := availableProviders[0] + hScrollOffset := 0 + if app.Provider != nil { + for i, provider := range availableProviders { + if provider.Id == app.Provider.Id { + currentProvider = provider + hScrollOffset = i + break + } + } } + + dialog := &modelDialog{ + app: app, + availableProviders: availableProviders, + hScrollOffset: hScrollOffset, + hScrollPossible: len(availableProviders) > 1, + provider: currentProvider, + modal: modal.New( + modal.WithTitle(fmt.Sprintf("Select %s Model", currentProvider.Name)), + modal.WithMaxWidth(maxDialogWidth+4), + ), + } + + dialog.setupModelsForProvider(currentProvider.Id) + return dialog } diff --git a/packages/tui/internal/components/dialog/session.go b/packages/tui/internal/components/dialog/session.go index 52eac1e2..2887ae73 100644 --- a/packages/tui/internal/components/dialog/session.go +++ b/packages/tui/internal/components/dialog/session.go @@ -8,8 +8,6 @@ import ( "github.com/sst/opencode/internal/components/list" "github.com/sst/opencode/internal/components/modal" "github.com/sst/opencode/internal/layout" - "github.com/sst/opencode/internal/styles" - "github.com/sst/opencode/internal/theme" "github.com/sst/opencode/internal/util" "github.com/sst/opencode/pkg/client" ) @@ -19,33 +17,12 @@ type SessionDialog interface { layout.Modal } -type sessionItem client.SessionInfo - -func (s sessionItem) Render(selected bool, width int) string { - t := theme.CurrentTheme() - baseStyle := styles.BaseStyle(). - Width(width - 4). - Background(t.BackgroundElement()) - - if selected { - baseStyle = baseStyle. - Background(t.Primary()). - Foreground(t.BackgroundElement()). - Bold(true) - } else { - baseStyle = baseStyle. - Foreground(t.Text()) - } - - return baseStyle.Padding(0, 1).Render(s.Title) -} - type sessionDialog struct { - width int - height int - modal *modal.Modal - selectedSessionID string - list list.List[sessionItem] + width int + height int + modal *modal.Modal + sessions []client.SessionInfo + list list.List[list.StringItem] } func (s *sessionDialog) Init() tea.Cmd { @@ -61,11 +38,11 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyPressMsg: switch msg.String() { case "enter": - if item, idx := s.list.GetSelectedItem(); idx >= 0 { - s.selectedSessionID = item.Id + if _, idx := s.list.GetSelectedItem(); idx >= 0 && idx < len(s.sessions) { + selectedSession := s.sessions[idx] return s, tea.Sequence( util.CmdHandler(modal.CloseModalMsg{}), - util.CmdHandler(app.SessionSelectedMsg(&item)), + util.CmdHandler(app.SessionSelectedMsg(&selectedSession)), ) } } @@ -73,7 +50,7 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd listModel, cmd := s.list.Update(msg) - s.list = listModel.(list.List[sessionItem]) + s.list = listModel.(list.List[list.StringItem]) return s, cmd } @@ -89,23 +66,30 @@ func (s *sessionDialog) Close() tea.Cmd { func NewSessionDialog(app *app.App) SessionDialog { sessions, _ := app.ListSessions(context.Background()) - var sessionItems []sessionItem + var filteredSessions []client.SessionInfo + var sessionTitles []string for _, sess := range sessions { if sess.ParentID != nil { continue } - sessionItems = append(sessionItems, sessionItem(sess)) + filteredSessions = append(filteredSessions, sess) + sessionTitles = append(sessionTitles, sess.Title) } - list := list.NewListComponent( - sessionItems, + list := list.NewStringList( + sessionTitles, 10, // maxVisibleSessions "No sessions available", true, // useAlphaNumericKeys ) + list.SetMaxWidth(layout.Current.Container.Width - 12) return &sessionDialog{ - list: list, - modal: modal.New(modal.WithTitle("Switch Session"), modal.WithMaxWidth(80)), + sessions: filteredSessions, + list: list, + modal: modal.New( + modal.WithTitle("Switch Session"), + modal.WithMaxWidth(layout.Current.Container.Width-8), + ), } } diff --git a/packages/tui/internal/components/list/list.go b/packages/tui/internal/components/list/list.go index c6ddbaf7..d5cc4b4f 100644 --- a/packages/tui/internal/components/list/list.go +++ b/packages/tui/internal/components/list/list.go @@ -6,6 +6,7 @@ import ( "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" "github.com/charmbracelet/lipgloss/v2" + "github.com/muesli/reflow/truncate" "github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/theme" ) @@ -175,6 +176,8 @@ func (s StringItem) Render(selected bool, width int) string { t := theme.CurrentTheme() baseStyle := styles.BaseStyle() + truncatedStr := truncate.StringWithTail(string(s), uint(width-1), "...") + var itemStyle lipgloss.Style if selected { itemStyle = baseStyle. @@ -187,7 +190,7 @@ func (s StringItem) Render(selected bool, width int) string { PaddingLeft(1) } - return itemStyle.Render(string(s)) + return itemStyle.Render(truncatedStr) } // NewStringList creates a new list component with string items diff --git a/packages/tui/internal/components/modal/modal.go b/packages/tui/internal/components/modal/modal.go index 98e34d62..62cafe84 100644 --- a/packages/tui/internal/components/modal/modal.go +++ b/packages/tui/internal/components/modal/modal.go @@ -103,13 +103,13 @@ func (m *Modal) Render(contentView string, background string) string { Bold(true). Padding(0, 1) - escStyle := baseStyle.Foreground(t.TextMuted()).Bold(false) + escStyle := baseStyle.Foreground(t.TextMuted()) escText := escStyle.Render("esc") // Calculate position for esc text titleWidth := lipgloss.Width(m.title) escWidth := lipgloss.Width(escText) - spacesNeeded := max(0, innerWidth-titleWidth-escWidth-3) + spacesNeeded := max(0, innerWidth-titleWidth-escWidth-2) spacer := strings.Repeat(" ", spacesNeeded) titleLine := m.title + spacer + escText titleLine = titleStyle.Render(titleLine)