fix(tui): modal visuals

This commit is contained in:
adamdottv
2025-06-18 15:12:18 -05:00
parent 5944ae2023
commit ac777b77cf
6 changed files with 44 additions and 38 deletions

View File

@@ -83,6 +83,6 @@ type HelpDialog interface {
func NewHelpDialog(commands []commands.Command) HelpDialog { func NewHelpDialog(commands []commands.Command) HelpDialog {
return &helpDialog{ return &helpDialog{
commands: commands, commands: commands,
modal: modal.New(), modal: modal.New(modal.WithTitle("Help")),
} }
} }

View File

@@ -180,6 +180,7 @@ func (m *modelDialog) switchProvider(offset int) {
m.hScrollOffset = newOffset m.hScrollOffset = newOffset
m.provider = m.availableProviders[m.hScrollOffset] m.provider = m.availableProviders[m.hScrollOffset]
m.modal.SetTitle(fmt.Sprintf("Select %s Model", m.provider.Name))
m.setupModelsForProvider(m.provider.Id) m.setupModelsForProvider(m.provider.Id)
} }
@@ -189,14 +190,6 @@ func (m *modelDialog) View() string {
Background(t.BackgroundElement()). Background(t.BackgroundElement()).
Foreground(t.Text()) Foreground(t.Text())
// Capitalize first letter of provider name
title := baseStyle.
Foreground(t.Primary()).
Bold(true).
Width(maxDialogWidth).
Padding(0, 0, 1).
Render(fmt.Sprintf("Select %s Model", m.provider.Name))
// Render visible models // Render visible models
endIdx := min(m.scrollOffset+numVisibleModels, len(m.provider.Models)) endIdx := min(m.scrollOffset+numVisibleModels, len(m.provider.Models))
modelItems := make([]string, 0, endIdx-m.scrollOffset) modelItems := make([]string, 0, endIdx-m.scrollOffset)
@@ -217,8 +210,9 @@ func (m *modelDialog) View() string {
content := lipgloss.JoinVertical( content := lipgloss.JoinVertical(
lipgloss.Left, lipgloss.Left,
title, baseStyle.
baseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)), Width(maxDialogWidth).
Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
scrollIndicator, scrollIndicator,
) )
@@ -238,12 +232,7 @@ func (m *modelDialog) getScrollIndicators(maxWidth int) string {
} }
if m.hScrollPossible { if m.hScrollPossible {
if m.hScrollOffset > 0 { indicator = "← " + indicator + "→"
indicator = "← " + indicator
}
if m.hScrollOffset < len(m.availableProviders)-1 {
indicator += "→"
}
} }
if indicator == "" { if indicator == "" {
@@ -313,6 +302,6 @@ func NewModelDialog(app *app.App) ModelDialog {
hScrollOffset: 0, hScrollOffset: 0,
hScrollPossible: len(availableProviders) > 1, hScrollPossible: len(availableProviders) > 1,
provider: availableProviders[0], provider: availableProviders[0],
modal: modal.New(), modal: modal.New(modal.WithTitle(fmt.Sprintf("Select %s Model", availableProviders[0].Name))),
} }
} }

View File

@@ -19,14 +19,12 @@ type SessionDialog interface {
layout.Modal layout.Modal
} }
type sessionItem struct { type sessionItem client.SessionInfo
session client.SessionInfo
}
func (s sessionItem) Render(selected bool, width int) string { func (s sessionItem) Render(selected bool, width int) string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
baseStyle := styles.BaseStyle(). baseStyle := styles.BaseStyle().
Width(width - 2). Width(width - 4).
Background(t.BackgroundElement()) Background(t.BackgroundElement())
if selected { if selected {
@@ -39,7 +37,7 @@ func (s sessionItem) Render(selected bool, width int) string {
Foreground(t.Text()) Foreground(t.Text())
} }
return baseStyle.Padding(0, 1).Render(s.session.Title) return baseStyle.Padding(0, 1).Render(s.Title)
} }
type sessionDialog struct { type sessionDialog struct {
@@ -60,15 +58,14 @@ func (s *sessionDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
s.width = msg.Width s.width = msg.Width
s.height = msg.Height s.height = msg.Height
s.list.SetMaxWidth(layout.Current.Container.Width - 12) s.list.SetMaxWidth(layout.Current.Container.Width - 12)
case tea.KeyMsg: case tea.KeyPressMsg:
switch msg.String() { switch msg.String() {
case "enter": case "enter":
if item, idx := s.list.GetSelectedItem(); idx >= 0 { if item, idx := s.list.GetSelectedItem(); idx >= 0 {
selectedSession := item.session s.selectedSessionID = item.Id
s.selectedSessionID = selectedSession.Id
return s, tea.Sequence( return s, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}), util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler(app.SessionSelectedMsg(&selectedSession)), util.CmdHandler(app.SessionSelectedMsg(&item)),
) )
} }
} }
@@ -94,7 +91,10 @@ func NewSessionDialog(app *app.App) SessionDialog {
var sessionItems []sessionItem var sessionItems []sessionItem
for _, sess := range sessions { for _, sess := range sessions {
sessionItems = append(sessionItems, sessionItem{session: sess}) if sess.ParentID != nil {
continue
}
sessionItems = append(sessionItems, sessionItem(sess))
} }
list := list.NewListComponent( list := list.NewListComponent(

View File

@@ -1,9 +1,10 @@
package list package list
import ( import (
"strings"
"github.com/charmbracelet/bubbles/v2/key" "github.com/charmbracelet/bubbles/v2/key"
tea "github.com/charmbracelet/bubbletea/v2" tea "github.com/charmbracelet/bubbletea/v2"
"github.com/charmbracelet/lipgloss/v2"
) )
type ListItem interface { type ListItem interface {
@@ -148,7 +149,7 @@ func (c *listComponent[T]) View() string {
listItems = append(listItems, title) listItems = append(listItems, title)
} }
return lipgloss.JoinVertical(lipgloss.Left, listItems...) return strings.Join(listItems, "\n")
} }
func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[T] { func NewListComponent[T ListItem](items []T, maxVisibleItems int, fallbackMsg string, useAlphaNumericKeys bool) List[T] {

View File

@@ -1,6 +1,8 @@
package modal package modal
import ( import (
"strings"
"github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/lipgloss/v2"
"github.com/sst/opencode/internal/layout" "github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles" "github.com/sst/opencode/internal/styles"
@@ -66,6 +68,10 @@ func New(opts ...ModalOption) *Modal {
return m return m
} }
func (m *Modal) SetTitle(title string) {
m.title = title
}
// Render renders the modal centered on the screen // Render renders the modal centered on the screen
func (m *Modal) Render(contentView string, background string) string { func (m *Modal) Render(contentView string, background string) string {
t := theme.CurrentTheme() t := theme.CurrentTheme()
@@ -95,15 +101,21 @@ func (m *Modal) Render(contentView string, background string) string {
titleStyle := baseStyle. titleStyle := baseStyle.
Foreground(t.Primary()). Foreground(t.Primary()).
Bold(true). Bold(true).
Width(innerWidth).
Padding(0, 1) Padding(0, 1)
titleView := titleStyle.Render(m.title) // titleView := titleStyle.Render(m.title)
finalContent = lipgloss.JoinVertical( escStyle := baseStyle.Foreground(t.TextMuted()).Bold(false)
lipgloss.Left, escText := escStyle.Render("esc")
titleView,
contentView, // Calculate position for esc text
) titleWidth := lipgloss.Width(m.title)
escWidth := lipgloss.Width(escText)
spacesNeeded := max(0, innerWidth-titleWidth-escWidth-3)
spacer := strings.Repeat(" ", spacesNeeded)
titleLine := m.title + spacer + escText
titleLine = titleStyle.Render(titleLine)
finalContent = strings.Join([]string{titleLine, contentView}, "\n") + "\n"
} else { } else {
finalContent = contentView finalContent = contentView
} }

View File

@@ -204,8 +204,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
a.layout.SetSize(a.width, a.height) a.layout.SetSize(a.width, a.height)
case app.SessionSelectedMsg: case app.SessionSelectedMsg:
var err error
a.app.Session = msg a.app.Session = msg
a.app.Messages, _ = a.app.ListMessages(context.Background(), msg.Id) a.app.Messages, err = a.app.ListMessages(context.Background(), msg.Id)
if err != nil {
slog.Error("Failed to list messages", "error", err)
}
case app.ModelSelectedMsg: case app.ModelSelectedMsg:
a.app.Provider = &msg.Provider a.app.Provider = &msg.Provider
a.app.Model = &msg.Model a.app.Model = &msg.Model