Browse Source

wip: refactoring tui

adamdottv 8 months ago
parent
commit
ca0ea3f94d

+ 8 - 4
packages/tui/internal/components/chat/message.go

@@ -258,16 +258,18 @@ func renderToolInvocation(
 	}
 
 	body := ""
+	error := ""
 	finished := result != nil && *result != ""
 	if finished {
 		body = *result
 	}
 
 	if metadata["error"] != nil && metadata["message"] != nil {
-		body = styles.BaseStyle().
-			Width(outerWidth).
+		body = ""
+		error = styles.BaseStyle().
 			Foreground(t.Error()).
 			Render(metadata["message"].(string))
+		error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
 	}
 
 	elapsed := ""
@@ -364,10 +366,12 @@ func renderToolInvocation(
 
 	content := style.Render(title)
 	content = lipgloss.PlaceHorizontal(layout.Current.Viewport.Width, lipgloss.Center, content)
-	// content = styles.ForceReplaceBackgroundWithLipgloss(content, t.Background())
-	if showResult && body != "" {
+	if showResult && body != "" && error == "" {
 		content += "\n" + body
 	}
+	if showResult && error != "" {
+		content += "\n" + error
+	}
 	return content
 }
 

+ 13 - 1
packages/tui/internal/components/chat/messages.go

@@ -122,6 +122,7 @@ const (
 	userTextBlock
 	assistantTextBlock
 	toolInvocationBlock
+	errorBlock
 )
 
 func (m *messagesComponent) renderView() {
@@ -129,6 +130,7 @@ func (m *messagesComponent) renderView() {
 		return
 	}
 
+	t := theme.CurrentTheme()
 	blocks := make([]string, 0)
 	previousBlockType := none
 	for _, message := range m.app.Messages {
@@ -211,9 +213,19 @@ func (m *messagesComponent) renderView() {
 				previousBlockType = toolInvocationBlock
 			}
 		}
+
+		error := ""
+		errorValue, _ := message.Metadata.Error.ValueByDiscriminator()
+		switch errorValue.(type) {
+		case client.UnknownError:
+			clientError := errorValue.(client.UnknownError)
+			error = clientError.Data.Message
+			error = renderContentBlock(error, WithBorderColor(t.Error()), WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
+			blocks = append(blocks, error)
+			previousBlockType = errorBlock
+		}
 	}
 
-	t := theme.CurrentTheme()
 	centered := []string{}
 	for _, block := range blocks {
 		centered = append(centered, lipgloss.PlaceHorizontal(

+ 3 - 3
packages/tui/internal/components/dialog/permission.go

@@ -257,7 +257,7 @@ func (p *permissionDialogComponent) renderBashContent() string {
 	// 	renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
 	// 		r := styles.GetMarkdownRenderer(p.width - 10)
 	// 		s, err := r.Render(content)
-	// 		return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
+	//    return s
 	// 	})
 	//
 	// 	finalContent := baseStyle.
@@ -317,7 +317,7 @@ func (p *permissionDialogComponent) renderFetchContent() string {
 	// 		renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
 	// 			r := styles.GetMarkdownRenderer(p.width - 10)
 	// 			s, err := r.Render(content)
-	// 			return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
+	//      return s
 	// 		})
 	//
 	// 		finalContent := baseStyle.
@@ -339,7 +339,7 @@ func (p *permissionDialogComponent) renderDefaultContent() string {
 	// 	renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
 	// 		r := styles.GetMarkdownRenderer(p.width - 10)
 	// 		s, err := r.Render(content)
-	// 		return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
+	//    return s
 	// 	})
 	//
 	// 	finalContent := baseStyle.

+ 52 - 51
packages/tui/internal/components/dialog/session.go

@@ -3,7 +3,7 @@ package dialog
 import (
 	"github.com/charmbracelet/bubbles/v2/key"
 	tea "github.com/charmbracelet/bubbletea/v2"
-	"github.com/charmbracelet/lipgloss/v2"
+	"github.com/sst/opencode/internal/components/modal"
 	utilComponents "github.com/sst/opencode/internal/components/util"
 	"github.com/sst/opencode/internal/layout"
 	"github.com/sst/opencode/internal/styles"
@@ -19,10 +19,11 @@ type CloseSessionDialogMsg struct {
 
 // SessionDialog interface for the session switching dialog
 type SessionDialog interface {
-	layout.ModelWithView
+	tea.Model
 	layout.Bindings
 	SetSessions(sessions []client.SessionInfo)
 	SetSelectedSession(sessionID string)
+	Render(background string) string
 }
 
 type sessionItem struct {
@@ -48,7 +49,8 @@ func (s sessionItem) Render(selected bool, width int) string {
 	return baseStyle.Padding(0, 1).Render(s.session.Title)
 }
 
-type sessionDialogComponent struct {
+// sessionDialogContent is the inner content of the session dialog
+type sessionDialogContent struct {
 	sessions          []client.SessionInfo
 	width             int
 	height            int
@@ -72,11 +74,11 @@ var sessionKeys = sessionKeyMap{
 	),
 }
 
-func (s *sessionDialogComponent) Init() tea.Cmd {
+func (s *sessionDialogContent) Init() tea.Cmd {
 	return nil
 }
 
-func (s *sessionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+func (s *sessionDialogContent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	switch msg := msg.(type) {
 	case tea.WindowSizeMsg:
 		s.width = msg.Width
@@ -110,17 +112,14 @@ func (s *sessionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 	return s, cmd
 }
 
-func (s *sessionDialogComponent) View() string {
+func (s *sessionDialogContent) View() string {
 	t := theme.CurrentTheme()
 	baseStyle := styles.BaseStyle().Background(t.BackgroundElement())
-	outerWidth := layout.Current.Container.Width - 8
-	width := outerWidth - 4
+	width := layout.Current.Container.Width - 12
 
 	if len(s.sessions) == 0 {
 		return baseStyle.Padding(1, 2).
-			Border(lipgloss.RoundedBorder()).
-			BorderBackground(t.Background()).
-			BorderForeground(t.TextMuted()).
+			Foreground(t.TextMuted()).
 			Width(width).
 			Render("No sessions available")
 	}
@@ -128,50 +127,46 @@ func (s *sessionDialogComponent) View() string {
 	// Set the max width for the list
 	s.list.SetMaxWidth(width)
 
-	title := baseStyle.
-		Foreground(t.Primary()).
-		Bold(true).
-		Width(width).
-		Padding(0, 1).
-		Render("Switch Session")
-
-	content := lipgloss.JoinVertical(
-		lipgloss.Left,
-		title,
-		s.list.View(),
-	)
-
-	style := styles.BaseStyle().
-		PaddingTop(1).
-		PaddingBottom(1).
-		PaddingLeft(2).
-		PaddingRight(2).
-		Background(t.BackgroundElement()).
-		Foreground(t.TextMuted()).
-		BorderStyle(lipgloss.ThickBorder())
-
-	style = style.
-		BorderLeft(true).
-		BorderRight(true).
-		BorderLeftForeground(t.BackgroundSubtle()).
-		BorderLeftBackground(t.Background()).
-		BorderRightForeground(t.BackgroundSubtle()).
-		BorderRightBackground(t.Background())
-
-	return style.
-		Width(outerWidth).
-		Render(content)
+	return s.list.View()
 }
 
-func (s *sessionDialogComponent) BindingKeys() []key.Binding {
+func (s *sessionDialogContent) BindingKeys() []key.Binding {
 	// Combine session dialog keys with list keys
 	dialogKeys := layout.KeyMapToSlice(sessionKeys)
 	listKeys := s.list.BindingKeys()
 	return append(dialogKeys, listKeys...)
 }
 
+// sessionDialogComponent wraps the content with a modal
+type sessionDialogComponent struct {
+	content *sessionDialogContent
+	modal   *modal.Modal
+}
+
+func (s *sessionDialogComponent) Init() tea.Cmd {
+	return s.modal.Init()
+}
+
+func (s *sessionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	m, cmd := s.modal.Update(msg)
+	s.modal = m.(*modal.Modal)
+	return s, cmd
+}
+
+func (s *sessionDialogComponent) View() string {
+	return s.modal.View()
+}
+
+func (s *sessionDialogComponent) Render(background string) string {
+	return s.modal.Render(background)
+}
+
+func (s *sessionDialogComponent) BindingKeys() []key.Binding {
+	return s.modal.BindingKeys()
+}
+
 func (s *sessionDialogComponent) SetSessions(sessions []client.SessionInfo) {
-	s.sessions = sessions
+	s.content.sessions = sessions
 
 	// Convert sessions to sessionItems
 	var sessionItems []sessionItem
@@ -180,16 +175,16 @@ func (s *sessionDialogComponent) SetSessions(sessions []client.SessionInfo) {
 		sessionItems = append(sessionItems, sessionItem{session: sess})
 	}
 
-	s.list.SetItems(sessionItems)
+	s.content.list.SetItems(sessionItems)
 }
 
 func (s *sessionDialogComponent) SetSelectedSession(sessionID string) {
-	s.selectedSessionID = sessionID
+	s.content.selectedSessionID = sessionID
 
 	// Update the selected index if sessions are already loaded
-	if len(s.sessions) > 0 {
+	if len(s.content.sessions) > 0 {
 		// Re-set the sessions to update the selection
-		s.SetSessions(s.sessions)
+		s.SetSessions(s.content.sessions)
 	}
 }
 
@@ -202,9 +197,15 @@ func NewSessionDialogCmp() SessionDialog {
 		true, // useAlphaNumericKeys
 	)
 
-	return &sessionDialogComponent{
+	content := &sessionDialogContent{
 		sessions:          []client.SessionInfo{},
 		selectedSessionID: "",
 		list:              list,
 	}
+
+	return &sessionDialogComponent{
+		content: content,
+		modal:   modal.New(content, modal.WithTitle("Switch Session")),
+	}
 }
+

+ 198 - 0
packages/tui/internal/components/modal/modal.go

@@ -0,0 +1,198 @@
+package modal
+
+import (
+	"github.com/charmbracelet/bubbles/v2/key"
+	tea "github.com/charmbracelet/bubbletea/v2"
+	"github.com/charmbracelet/lipgloss/v2"
+	"github.com/sst/opencode/internal/layout"
+	"github.com/sst/opencode/internal/styles"
+	"github.com/sst/opencode/internal/theme"
+)
+
+// Modal is a reusable modal component that handles frame rendering and overlay placement
+type Modal struct {
+	content       tea.Model
+	width         int
+	height        int
+	title         string
+	showBorder    bool
+	borderStyle   lipgloss.Border
+	maxWidth      int
+	maxHeight     int
+	centerContent bool
+}
+
+// ModalOption is a function that configures a Modal
+type ModalOption func(*Modal)
+
+// WithTitle sets the modal title
+func WithTitle(title string) ModalOption {
+	return func(m *Modal) {
+		m.title = title
+	}
+}
+
+// WithBorder enables/disables the border
+func WithBorder(show bool) ModalOption {
+	return func(m *Modal) {
+		m.showBorder = show
+	}
+}
+
+// WithBorderStyle sets the border style
+func WithBorderStyle(style lipgloss.Border) ModalOption {
+	return func(m *Modal) {
+		m.borderStyle = style
+	}
+}
+
+// WithMaxWidth sets the maximum width
+func WithMaxWidth(width int) ModalOption {
+	return func(m *Modal) {
+		m.maxWidth = width
+	}
+}
+
+// WithMaxHeight sets the maximum height
+func WithMaxHeight(height int) ModalOption {
+	return func(m *Modal) {
+		m.maxHeight = height
+	}
+}
+
+// WithCenterContent centers the content within the modal
+func WithCenterContent(center bool) ModalOption {
+	return func(m *Modal) {
+		m.centerContent = center
+	}
+}
+
+// New creates a new Modal with the given content and options
+func New(content tea.Model, opts ...ModalOption) *Modal {
+	m := &Modal{
+		content:       content,
+		showBorder:    true,
+		borderStyle:   lipgloss.ThickBorder(),
+		maxWidth:      0,
+		maxHeight:     0,
+		centerContent: false,
+	}
+
+	for _, opt := range opts {
+		opt(m)
+	}
+
+	return m
+}
+
+func (m *Modal) Init() tea.Cmd {
+	return m.content.Init()
+}
+
+func (m *Modal) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+	switch msg := msg.(type) {
+	case tea.WindowSizeMsg:
+		m.width = msg.Width
+		m.height = msg.Height
+	}
+
+	// Pass all messages to the content
+	var cmd tea.Cmd
+	m.content, cmd = m.content.Update(msg)
+	return m, cmd
+}
+
+func (m *Modal) View() string {
+	t := theme.CurrentTheme()
+	
+	// Get the content view
+	contentView := ""
+	if v, ok := m.content.(layout.ModelWithView); ok {
+		contentView = v.View()
+	}
+
+	// Calculate dimensions
+	outerWidth := layout.Current.Container.Width - 8
+	if m.maxWidth > 0 && outerWidth > m.maxWidth {
+		outerWidth = m.maxWidth
+	}
+	
+	innerWidth := outerWidth - 4
+	
+	// Base style for the modal
+	baseStyle := styles.BaseStyle().
+		Background(t.BackgroundElement()).
+		Foreground(t.TextMuted())
+
+	// Add title if provided
+	var finalContent string
+	if m.title != "" {
+		titleStyle := baseStyle.
+			Foreground(t.Primary()).
+			Bold(true).
+			Width(innerWidth).
+			Padding(0, 1)
+		
+		titleView := titleStyle.Render(m.title)
+		finalContent = lipgloss.JoinVertical(
+			lipgloss.Left,
+			titleView,
+			contentView,
+		)
+	} else {
+		finalContent = contentView
+	}
+
+	// Apply modal styling
+	modalStyle := baseStyle.
+		PaddingTop(1).
+		PaddingBottom(1).
+		PaddingLeft(2).
+		PaddingRight(2)
+
+	if m.showBorder {
+		modalStyle = modalStyle.
+			BorderStyle(m.borderStyle).
+			BorderLeft(true).
+			BorderRight(true).
+			BorderLeftForeground(t.BackgroundSubtle()).
+			BorderLeftBackground(t.Background()).
+			BorderRightForeground(t.BackgroundSubtle()).
+			BorderRightBackground(t.Background())
+	}
+
+	return modalStyle.
+		Width(outerWidth).
+		Render(finalContent)
+}
+
+// Render renders the modal centered on the screen
+func (m *Modal) Render(background string) string {
+	modalView := m.View()
+	
+	// Calculate position for centering
+	bgHeight := lipgloss.Height(background)
+	bgWidth := lipgloss.Width(background)
+	modalHeight := lipgloss.Height(modalView)
+	modalWidth := lipgloss.Width(modalView)
+	
+	row := (bgHeight - modalHeight) / 2
+	col := (bgWidth - modalWidth) / 2
+	
+	// Use PlaceOverlay to render the modal on top of the background
+	return layout.PlaceOverlay(
+		col,
+		row,
+		modalView,
+		background,
+		true, // shadow
+	)
+}
+
+// BindingKeys returns the key bindings from the content if it implements layout.Bindings
+func (m *Modal) BindingKeys() []key.Binding {
+	if b, ok := m.content.(layout.Bindings); ok {
+		return b.BindingKeys()
+	}
+	return []key.Binding{}
+}

+ 1 - 12
packages/tui/internal/tui/tui.go

@@ -836,18 +836,7 @@ func (a appModel) View() string {
 	}
 
 	if a.showSessionDialog {
-		overlay := a.sessionDialog.View()
-		row := lipgloss.Height(appView) / 2
-		row -= lipgloss.Height(overlay) / 2
-		col := lipgloss.Width(appView) / 2
-		col -= lipgloss.Width(overlay) / 2
-		appView = layout.PlaceOverlay(
-			col,
-			row,
-			overlay,
-			appView,
-			true,
-		)
+		appView = a.sessionDialog.Render(appView)
 	}
 
 	if a.showModelDialog {