chore(tui): simplify messages component, remove navigate, add copy last message

This commit is contained in:
adamdotdevin 2025-07-15 13:56:14 -05:00
parent f45deb37f0
commit d41aa2bc72
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
3 changed files with 51 additions and 160 deletions

View file

@ -11,9 +11,7 @@ import (
"github.com/charmbracelet/lipgloss/v2/compat"
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/commands"
"github.com/sst/opencode/internal/components/diff"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
"github.com/sst/opencode/internal/util"
@ -109,7 +107,6 @@ func WithPaddingBottom(padding int) renderingOption {
func renderContentBlock(
app *app.App,
content string,
highlight bool,
width int,
options ...renderingOption,
) string {
@ -158,18 +155,6 @@ func renderContentBlock(
BorderRightBackground(t.Background())
}
if highlight {
style = style.
BorderLeftForeground(borderColor).
BorderRightForeground(borderColor)
}
}
if highlight {
style = style.
Foreground(t.Text()).
Background(t.BackgroundElement()).
Bold(true)
}
content = style.Render(content)
@ -184,32 +169,6 @@ func renderContentBlock(
}
}
if highlight {
copy := app.Key(commands.MessagesCopyCommand)
// revert := app.Key(commands.MessagesRevertCommand)
background := t.Background()
header := layout.Render(
layout.FlexOptions{
Background: &background,
Direction: layout.Row,
Justify: layout.JustifyCenter,
Align: layout.AlignStretch,
Width: width - 2,
Gap: 5,
},
layout.FlexItem{
View: copy,
},
// layout.FlexItem{
// View: revert,
// },
)
header = styles.NewStyle().Background(t.Background()).Padding(0, 1).Render(header)
content = "\n\n\n" + header + "\n\n" + content + "\n\n\n"
}
return content
}
@ -219,7 +178,6 @@ func renderText(
text string,
author string,
showToolDetails bool,
highlight bool,
width int,
extra string,
toolCalls ...opencode.ToolPart,
@ -228,9 +186,6 @@ func renderText(
var ts time.Time
backgroundColor := t.BackgroundPanel()
if highlight {
backgroundColor = t.BackgroundElement()
}
var content string
switch casted := message.(type) {
case opencode.AssistantMessage:
@ -277,7 +232,6 @@ func renderText(
return renderContentBlock(
app,
content,
highlight,
width,
WithTextColor(t.Text()),
WithBorderColorRight(t.Secondary()),
@ -286,7 +240,6 @@ func renderText(
return renderContentBlock(
app,
content,
highlight,
width,
WithBorderColor(t.Accent()),
)
@ -297,7 +250,6 @@ func renderText(
func renderToolDetails(
app *app.App,
toolCall opencode.ToolPart,
highlight bool,
width int,
) string {
ignoredTools := []string{"todoread"}
@ -307,7 +259,7 @@ func renderToolDetails(
if toolCall.State.Status == opencode.ToolPartStateStatusPending {
title := renderToolTitle(toolCall, width)
return renderContentBlock(app, title, highlight, width)
return renderContentBlock(app, title, width)
}
var result *string
@ -332,10 +284,6 @@ func renderToolDetails(
t := theme.CurrentTheme()
backgroundColor := t.BackgroundPanel()
borderColor := t.BackgroundPanel()
if highlight {
backgroundColor = t.BackgroundElement()
borderColor = t.BorderActive()
}
if toolCall.State.Metadata != nil {
metadata := toolCall.State.Metadata.(map[string]any)
@ -370,9 +318,6 @@ func renderToolDetails(
Foreground(t.TextMuted()).
Padding(1, 2).
Width(width - 4)
if highlight {
style = style.Foreground(t.Text()).Bold(true)
}
if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
diagnostics = style.Render(diagnostics)
@ -385,7 +330,6 @@ func renderToolDetails(
content = renderContentBlock(
app,
content,
highlight,
width,
WithPadding(0),
WithBorderColor(borderColor),
@ -486,7 +430,7 @@ func renderToolDetails(
title := renderToolTitle(toolCall, width)
content := title + "\n\n" + body
return renderContentBlock(app, content, highlight, width, WithBorderColor(borderColor))
return renderContentBlock(app, content, width, WithBorderColor(borderColor))
}
func renderToolName(name string) string {

View file

@ -10,6 +10,7 @@ import (
"github.com/sst/opencode-sdk-go"
"github.com/sst/opencode/internal/app"
"github.com/sst/opencode/internal/components/dialog"
"github.com/sst/opencode/internal/components/toast"
"github.com/sst/opencode/internal/layout"
"github.com/sst/opencode/internal/styles"
"github.com/sst/opencode/internal/theme"
@ -24,12 +25,10 @@ type MessagesComponent interface {
PageDown() (tea.Model, tea.Cmd)
HalfPageUp() (tea.Model, tea.Cmd)
HalfPageDown() (tea.Model, tea.Cmd)
First() (tea.Model, tea.Cmd)
Last() (tea.Model, tea.Cmd)
Previous() (tea.Model, tea.Cmd)
Next() (tea.Model, tea.Cmd)
ToolDetailsVisible() bool
Selected() string
GotoTop() (tea.Model, tea.Cmd)
GotoBottom() (tea.Model, tea.Cmd)
CopyLastMessage() (tea.Model, tea.Cmd)
}
type messagesComponent struct {
@ -42,13 +41,8 @@ type messagesComponent struct {
tail bool
partCount int
lineCount int
selectedPart int
selectedText string
}
type renderFinishedMsg struct{}
type selectedMessagePartChangedMsg struct {
part int
}
type ToggleToolDetailsMsg struct{}
@ -56,17 +50,12 @@ func (m *messagesComponent) Init() tea.Cmd {
return tea.Batch(m.viewport.Init())
}
func (m *messagesComponent) Selected() string {
return m.selectedText
}
func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case app.SendMsg:
m.viewport.GotoBottom()
m.tail = true
m.selectedPart = -1
return m, nil
case app.OptimisticMessageAddedMsg:
m.tail = true
@ -90,8 +79,7 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.tail {
m.viewport.GotoBottom()
}
case selectedMessagePartChangedMsg:
return m, m.Reload()
case opencode.EventListResponseEventSessionUpdated:
if msg.Properties.Info.ID == m.app.Session.ID {
m.renderView(m.width)
@ -183,7 +171,7 @@ func (m *messagesComponent) renderView(width int) {
flexItems...,
)
key := m.cache.GenerateKey(casted.ID, part.Text, width, m.selectedPart == m.partCount, files)
key := m.cache.GenerateKey(casted.ID, part.Text, width, files)
content, cached = m.cache.Get(key)
if !cached {
content = renderText(
@ -192,14 +180,14 @@ func (m *messagesComponent) renderView(width int) {
part.Text,
m.app.Config.Username,
m.showToolDetails,
m.partCount == m.selectedPart,
width,
files,
)
m.cache.Set(key, content)
}
if content != "" {
m = m.updateSelected(content, part.Text)
m.partCount++
m.lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
}
// Only render the first text part
@ -236,7 +224,7 @@ func (m *messagesComponent) renderView(width int) {
remaining = false
case opencode.ToolPart:
toolCallParts = append(toolCallParts, part)
if part.State.Status != opencode.ToolPartStateStatusCompleted || part.State.Status != opencode.ToolPartStateStatusError {
if part.State.Status != opencode.ToolPartStateStatusCompleted && part.State.Status != opencode.ToolPartStateStatusError {
// i don't think there's a case where a tool call isn't in result state
// and the message time is 0, but just in case
finished = false
@ -245,7 +233,7 @@ func (m *messagesComponent) renderView(width int) {
}
if finished {
key := m.cache.GenerateKey(casted.ID, part.Text, width, m.showToolDetails, m.selectedPart == m.partCount)
key := m.cache.GenerateKey(casted.ID, part.Text, width, m.showToolDetails)
content, cached = m.cache.Get(key)
if !cached {
content = renderText(
@ -254,7 +242,6 @@ func (m *messagesComponent) renderView(width int) {
part.Text,
casted.ModelID,
m.showToolDetails,
m.partCount == m.selectedPart,
width,
"",
toolCallParts...,
@ -268,14 +255,14 @@ func (m *messagesComponent) renderView(width int) {
part.Text,
casted.ModelID,
m.showToolDetails,
m.partCount == m.selectedPart,
width,
"",
toolCallParts...,
)
}
if content != "" {
m = m.updateSelected(content, part.Text)
m.partCount++
m.lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
}
case opencode.ToolPart:
@ -291,14 +278,12 @@ func (m *messagesComponent) renderView(width int) {
part.ID,
m.showToolDetails,
width,
m.partCount == m.selectedPart,
)
content, cached = m.cache.Get(key)
if !cached {
content = renderToolDetails(
m.app,
part,
m.partCount == m.selectedPart,
width,
)
m.cache.Set(key, content)
@ -308,12 +293,12 @@ func (m *messagesComponent) renderView(width int) {
content = renderToolDetails(
m.app,
part,
m.partCount == m.selectedPart,
width,
)
}
if content != "" {
m = m.updateSelected(content, "")
m.partCount++
m.lineCount += lipgloss.Height(content) + 1
blocks = append(blocks, content)
}
}
@ -340,7 +325,6 @@ func (m *messagesComponent) renderView(width int) {
error = renderContentBlock(
m.app,
error,
false,
width,
WithBorderColor(t.Error()),
)
@ -350,22 +334,9 @@ func (m *messagesComponent) renderView(width int) {
}
m.viewport.SetContent("\n" + strings.Join(blocks, "\n\n"))
if m.selectedPart == m.partCount {
m.viewport.GotoBottom()
}
}
func (m *messagesComponent) updateSelected(content string, selectedText string) *messagesComponent {
if m.selectedPart == m.partCount {
m.viewport.SetYOffset(m.lineCount - (m.viewport.Height() / 2) + 4)
m.selectedText = selectedText
}
m.partCount++
m.lineCount += lipgloss.Height(content) + 1
return m
}
func (m *messagesComponent) header(width int) string {
if m.app.Session.ID == "" {
return ""
@ -561,51 +532,40 @@ func (m *messagesComponent) HalfPageDown() (tea.Model, tea.Cmd) {
return m, nil
}
func (m *messagesComponent) Previous() (tea.Model, tea.Cmd) {
m.tail = false
if m.selectedPart < 0 {
m.selectedPart = m.partCount
}
m.selectedPart--
if m.selectedPart < 0 {
m.selectedPart = 0
}
return m, util.CmdHandler(selectedMessagePartChangedMsg{
part: m.selectedPart,
})
}
func (m *messagesComponent) Next() (tea.Model, tea.Cmd) {
m.tail = false
m.selectedPart++
if m.selectedPart >= m.partCount {
m.selectedPart = m.partCount
}
return m, util.CmdHandler(selectedMessagePartChangedMsg{
part: m.selectedPart,
})
}
func (m *messagesComponent) First() (tea.Model, tea.Cmd) {
m.selectedPart = 0
m.tail = false
return m, util.CmdHandler(selectedMessagePartChangedMsg{
part: m.selectedPart,
})
}
func (m *messagesComponent) Last() (tea.Model, tea.Cmd) {
m.selectedPart = m.partCount - 1
m.tail = true
return m, util.CmdHandler(selectedMessagePartChangedMsg{
part: m.selectedPart,
})
}
func (m *messagesComponent) ToolDetailsVisible() bool {
return m.showToolDetails
}
func (m *messagesComponent) GotoTop() (tea.Model, tea.Cmd) {
m.viewport.GotoTop()
return m, nil
}
func (m *messagesComponent) GotoBottom() (tea.Model, tea.Cmd) {
m.viewport.GotoBottom()
return m, nil
}
func (m *messagesComponent) CopyLastMessage() (tea.Model, tea.Cmd) {
if len(m.app.Messages) == 0 {
return m, nil
}
lastMessage := m.app.Messages[len(m.app.Messages)-1]
var lastTextPart *opencode.TextPart
for _, part := range lastMessage.Parts {
if p, ok := part.(opencode.TextPart); ok {
lastTextPart = &p
}
}
if lastTextPart == nil {
return m, nil
}
var cmds []tea.Cmd
cmds = append(cmds, m.app.SetClipboard(lastTextPart.Text))
cmds = append(cmds, toast.NewSuccessToast("Message copied to clipboard"))
return m, tea.Batch(cmds...)
}
func NewMessagesComponent(app *app.App) MessagesComponent {
vp := viewport.New()
vp.KeyMap = viewport.KeyMap{}
@ -616,6 +576,5 @@ func NewMessagesComponent(app *app.App) MessagesComponent {
showToolDetails: true,
cache: NewMessageCache(),
tail: true,
selectedPart: -1,
}
}