feat(tui): layout config to render full width

This commit is contained in:
adamdotdevin 2025-07-16 12:42:52 -05:00
parent fdd6d6600f
commit cdc1d8a94d
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
16 changed files with 465 additions and 434 deletions

View file

@ -27,8 +27,8 @@ import (
type EditorComponent interface {
tea.Model
View(width int) string
Content(width int) string
tea.ViewModel
Content() string
Lines() int
Value() string
Length() int
@ -46,6 +46,7 @@ type EditorComponent interface {
type editorComponent struct {
app *app.App
width int
textarea textarea.Model
spinner spinner.Model
interruptKeyInDebounce bool
@ -61,6 +62,12 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width = min(msg.Width-4, app.MAX_CONTAINER_WIDTH)
if m.app.Config.Layout == opencode.LayoutConfigStretch {
m.width = msg.Width - 4
}
return m, nil
case spinner.TickMsg:
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
@ -227,7 +234,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}
func (m *editorComponent) Content(width int) string {
func (m *editorComponent) Content() string {
t := theme.CurrentTheme()
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
@ -236,7 +243,7 @@ func (m *editorComponent) Content(width int) string {
Bold(true)
prompt := promptStyle.Render(">")
m.textarea.SetWidth(width - 6)
m.textarea.SetWidth(m.width - 6)
textarea := lipgloss.JoinHorizontal(
lipgloss.Top,
prompt,
@ -248,7 +255,7 @@ func (m *editorComponent) Content(width int) string {
}
textarea = styles.NewStyle().
Background(t.BackgroundElement()).
Width(width).
Width(m.width).
PaddingTop(1).
PaddingBottom(1).
BorderStyle(lipgloss.ThickBorder()).
@ -284,7 +291,7 @@ func (m *editorComponent) Content(width int) string {
model = muted(m.app.Provider.Name) + base(" "+m.app.Model.Name)
}
space := width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
space := m.width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
spacer := styles.NewStyle().Background(t.Background()).Width(space).Render("")
info := hint + spacer + model
@ -294,10 +301,10 @@ func (m *editorComponent) Content(width int) string {
return content
}
func (m *editorComponent) View(width int) string {
func (m *editorComponent) View() string {
if m.Lines() > 1 {
return lipgloss.Place(
width,
m.width,
5,
lipgloss.Center,
lipgloss.Center,
@ -305,7 +312,7 @@ func (m *editorComponent) View(width int) string {
styles.WhitespaceStyle(theme.CurrentTheme().Background()),
)
}
return m.Content(width)
return m.Content()
}
func (m *editorComponent) Focused() bool {

View file

@ -10,6 +10,7 @@ import (
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/charmbracelet/x/ansi"
"github.com/muesli/reflow/truncate"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/diff"
@ -319,11 +320,19 @@ func renderToolDetails(
if diffField != nil {
patch := diffField.(string)
var formattedDiff string
formattedDiff, _ = diff.FormatUnifiedDiff(
filename,
patch,
diff.WithWidth(width-2),
)
if width < 120 {
formattedDiff, _ = diff.FormatUnifiedDiff(
filename,
patch,
diff.WithWidth(width-2),
)
} else {
formattedDiff, _ = diff.FormatDiff(
filename,
patch,
diff.WithWidth(width-2),
)
}
body = strings.TrimSpace(formattedDiff)
style := styles.NewStyle().
Background(backgroundColor).
@ -551,6 +560,8 @@ func renderToolTitle(
toolName := renderToolName(toolCall.Tool)
title = fmt.Sprintf("%s %s", toolName, toolArgs)
}
title = truncate.StringWithTail(title, uint(width-6), "...")
return title
}

View file

@ -19,8 +19,7 @@ import (
type MessagesComponent interface {
tea.Model
View(width, height int) string
SetWidth(width int) tea.Cmd
tea.ViewModel
PageUp() (tea.Model, tea.Cmd)
PageDown() (tea.Model, tea.Cmd)
HalfPageUp() (tea.Model, tea.Cmd)
@ -32,8 +31,9 @@ type MessagesComponent interface {
}
type messagesComponent struct {
width int
width, height int
app *app.App
header string
viewport viewport.Model
cache *PartCache
rendering bool
@ -53,6 +53,17 @@ func (m *messagesComponent) Init() tea.Cmd {
func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
effectiveWidth := msg.Width - 4
// Clear cache on resize since width affects rendering
if m.width != effectiveWidth {
m.cache.Clear()
}
m.width = effectiveWidth
m.height = msg.Height - 7
m.viewport.SetWidth(m.width)
m.header = m.renderHeader()
return m, m.Reload()
case app.SendMsg:
m.viewport.GotoBottom()
m.tail = true
@ -82,21 +93,18 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case opencode.EventListResponseEventSessionUpdated:
if msg.Properties.Info.ID == m.app.Session.ID {
m.renderView(m.width)
if m.tail {
m.viewport.GotoBottom()
}
m.header = m.renderHeader()
}
case opencode.EventListResponseEventMessageUpdated:
if msg.Properties.Info.SessionID == m.app.Session.ID {
m.renderView(m.width)
m.renderView()
if m.tail {
m.viewport.GotoBottom()
}
}
case opencode.EventListResponseEventMessagePartUpdated:
if msg.Properties.Part.SessionID == m.app.Session.ID {
m.renderView(m.width)
m.renderView()
if m.tail {
m.viewport.GotoBottom()
}
@ -111,10 +119,12 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(cmds...)
}
func (m *messagesComponent) renderView(width int) {
func (m *messagesComponent) renderView() {
measure := util.Measure("messages.renderView")
defer measure("messageCount", len(m.app.Messages))
m.header = m.renderHeader()
t := theme.CurrentTheme()
blocks := make([]string, 0)
m.partCount = 0
@ -122,6 +132,11 @@ func (m *messagesComponent) renderView(width int) {
orphanedToolCalls := make([]opencode.ToolPart, 0)
width := min(m.width, app.MAX_CONTAINER_WIDTH)
if m.app.Config.Layout == opencode.LayoutConfigStretch {
width = m.width
}
for _, message := range m.app.Messages {
var content string
var cached bool
@ -185,6 +200,12 @@ func (m *messagesComponent) renderView(width int) {
width,
files,
)
content = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
content,
styles.WhitespaceStyle(t.Background()),
)
m.cache.Set(key, content)
}
if content != "" {
@ -246,6 +267,12 @@ func (m *messagesComponent) renderView(width int) {
"",
toolCallParts...,
)
content = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
content,
styles.WhitespaceStyle(t.Background()),
)
m.cache.Set(key, content)
}
} else {
@ -259,6 +286,12 @@ func (m *messagesComponent) renderView(width int) {
"",
toolCallParts...,
)
content = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
content,
styles.WhitespaceStyle(t.Background()),
)
}
if content != "" {
m.partCount++
@ -273,6 +306,13 @@ func (m *messagesComponent) renderView(width int) {
continue
}
width := width
if m.app.Config.Layout == opencode.LayoutConfigAuto &&
part.Tool == "edit" &&
part.State.Error == "" {
width = min(m.width, app.EDIT_DIFF_MAX_WIDTH)
}
if part.State.Status == opencode.ToolPartStateStatusCompleted || part.State.Status == opencode.ToolPartStateStatusError {
key := m.cache.GenerateKey(casted.ID,
part.ID,
@ -286,6 +326,12 @@ func (m *messagesComponent) renderView(width int) {
part,
width,
)
content = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
content,
styles.WhitespaceStyle(t.Background()),
)
m.cache.Set(key, content)
}
} else {
@ -295,6 +341,12 @@ func (m *messagesComponent) renderView(width int) {
part,
width,
)
content = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
content,
styles.WhitespaceStyle(t.Background()),
)
}
if content != "" {
m.partCount++
@ -333,22 +385,27 @@ func (m *messagesComponent) renderView(width int) {
}
}
m.viewport.SetHeight(m.height - lipgloss.Height(m.header))
m.viewport.SetContent("\n" + strings.Join(blocks, "\n\n"))
}
func (m *messagesComponent) header(width int) string {
func (m *messagesComponent) renderHeader() string {
if m.app.Session.ID == "" {
return ""
}
headerWidth := min(m.width, app.MAX_CONTAINER_WIDTH)
if m.app.Config.Layout == opencode.LayoutConfigStretch {
headerWidth = m.width
}
t := theme.CurrentTheme()
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
headerLines := []string{}
headerLines = append(
headerLines,
util.ToMarkdown("# "+m.app.Session.Title, width-6, t.Background()),
util.ToMarkdown("# "+m.app.Session.Title, headerWidth-6, t.Background()),
)
share := ""
@ -397,7 +454,7 @@ func (m *messagesComponent) header(width int) string {
Direction: layout.Row,
Justify: layout.JustifySpaceBetween,
Align: layout.AlignStretch,
Width: width - 6,
Width: headerWidth - 6,
},
layout.FlexItem{
View: share,
@ -408,12 +465,10 @@ func (m *messagesComponent) header(width int) string {
)
headerLines = append(headerLines, share)
header := strings.Join(headerLines, "\n")
header = styles.NewStyle().
Background(t.Background()).
Width(width).
Width(headerWidth).
PaddingLeft(2).
PaddingRight(2).
BorderLeft(true).
@ -422,6 +477,12 @@ func (m *messagesComponent) header(width int) string {
BorderForeground(t.BackgroundElement()).
BorderStyle(lipgloss.ThickBorder()).
Render(header)
header = lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
header,
styles.WhitespaceStyle(t.Background()),
)
return "\n" + header + "\n"
}
@ -473,44 +534,27 @@ func formatTokensAndCost(
)
}
func (m *messagesComponent) View(width, height int) string {
func (m *messagesComponent) View() string {
t := theme.CurrentTheme()
if m.rendering {
return lipgloss.Place(
width,
height,
m.width,
m.height,
lipgloss.Center,
lipgloss.Center,
styles.NewStyle().Background(t.Background()).Render(""),
styles.WhitespaceStyle(t.Background()),
)
}
header := m.header(width)
m.viewport.SetWidth(width)
m.viewport.SetHeight(height - lipgloss.Height(header))
return styles.NewStyle().
Background(t.Background()).
Render(header + "\n" + m.viewport.View())
}
func (m *messagesComponent) SetWidth(width int) tea.Cmd {
if m.width == width {
return nil
}
// Clear cache on resize since width affects rendering
if m.width != width {
m.cache.Clear()
}
m.width = width
m.viewport.SetWidth(width)
m.renderView(width)
return nil
Render(m.header + "\n" + m.viewport.View())
}
func (m *messagesComponent) Reload() tea.Cmd {
return func() tea.Msg {
m.renderView(m.width)
m.renderView()
return renderFinishedMsg{}
}
}