Explorar o código

fix(tui): cleanup modal visuals

adamdottv hai 1 ano
pai
achega
dc1947838c

+ 67 - 149
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(_ string) {
-	m.selectedIdx = 0
-	m.scrollOffset = 0
-
-	// cfg := config.Get()
-	// agentCfg := cfg.Agents[config.AgentPrimary]
-	// selectedModelId := agentCfg.Model
+func (m *modelDialog) setupModelsForProvider(providerId string) {
+	models := m.models()
+	modelNames := make([]string, len(models))
+	for i, model := range models {
+		modelNames[i] = model.Name
+	}
 
-	// m.provider = provider
-	// m.models = getModelsForProvider(provider)
+	m.modelList = list.NewStringList(modelNames, numVisibleModels, "No models available", true)
+	m.modelList.SetMaxWidth(maxDialogWidth)
 
-	// 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{
+	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:      0,
+		hScrollOffset:      hScrollOffset,
 		hScrollPossible:    len(availableProviders) > 1,
-		provider:           availableProviders[0],
-		modal:              modal.New(modal.WithTitle(fmt.Sprintf("Select %s Model", availableProviders[0].Name))),
+		provider:           currentProvider,
+		modal: modal.New(
+			modal.WithTitle(fmt.Sprintf("Select %s Model", currentProvider.Name)),
+			modal.WithMaxWidth(maxDialogWidth+4),
+		),
 	}
+
+	dialog.setupModelsForProvider(currentProvider.Id)
+	return dialog
 }

+ 22 - 38
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),
+		),
 	}
 }

+ 4 - 1
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

+ 2 - 2
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)