summaryrefslogtreecommitdiffhomepage
path: root/packages/tui/internal/components/chat
diff options
context:
space:
mode:
authoradamdotdevin <[email protected]>2025-07-16 12:42:52 -0500
committeradamdotdevin <[email protected]>2025-07-16 12:43:02 -0500
commitcdc1d8a94d12a31d5d162cb37744305738177ac2 (patch)
treea9a9494ddc5b4a1a30c28e9ef19c99541292e630 /packages/tui/internal/components/chat
parentfdd6d6600f090ad4f15b2bbfc0bb75c36a722f4e (diff)
downloadopencode-cdc1d8a94d12a31d5d162cb37744305738177ac2.tar.gz
opencode-cdc1d8a94d12a31d5d162cb37744305738177ac2.zip
feat(tui): layout config to render full width
Diffstat (limited to 'packages/tui/internal/components/chat')
-rw-r--r--packages/tui/internal/components/chat/editor.go25
-rw-r--r--packages/tui/internal/components/chat/message.go21
-rw-r--r--packages/tui/internal/components/chat/messages.go122
3 files changed, 115 insertions, 53 deletions
diff --git a/packages/tui/internal/components/chat/editor.go b/packages/tui/internal/components/chat/editor.go
index 67f1f75ea..de74b4895 100644
--- a/packages/tui/internal/components/chat/editor.go
+++ b/packages/tui/internal/components/chat/editor.go
@@ -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 {
diff --git a/packages/tui/internal/components/chat/message.go b/packages/tui/internal/components/chat/message.go
index 35ff675bd..d3263b053 100644
--- a/packages/tui/internal/components/chat/message.go
+++ b/packages/tui/internal/components/chat/message.go
@@ -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
}
diff --git a/packages/tui/internal/components/chat/messages.go b/packages/tui/internal/components/chat/messages.go
index 739f69109..e17847645 100644
--- a/packages/tui/internal/components/chat/messages.go
+++ b/packages/tui/internal/components/chat/messages.go
@@ -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{}
}
}