summaryrefslogtreecommitdiffhomepage
path: root/internal
diff options
context:
space:
mode:
authoradamdottv <[email protected]>2025-05-29 13:12:39 -0500
committeradamdottv <[email protected]>2025-05-29 14:04:49 -0500
commit26606ccbf7be90a6cd7c4d80aa9a3333cc9db6a8 (patch)
tree235b3419aa44b697dca219c551797a69205a86da /internal
parentfce9e79d38f20d6e83a8e21c51372006a53d30d4 (diff)
downloadopencode-26606ccbf7be90a6cd7c4d80aa9a3333cc9db6a8.tar.gz
opencode-26606ccbf7be90a6cd7c4d80aa9a3333cc9db6a8.zip
wip: refactoring tui
Diffstat (limited to 'internal')
-rw-r--r--internal/tui/components/chat/message.go330
-rw-r--r--internal/tui/components/chat/messages.go121
-rw-r--r--internal/tui/tui.go8
3 files changed, 195 insertions, 264 deletions
diff --git a/internal/tui/components/chat/message.go b/internal/tui/components/chat/message.go
index 679041d8d..7317b2003 100644
--- a/internal/tui/components/chat/message.go
+++ b/internal/tui/components/chat/message.go
@@ -11,7 +11,6 @@ import (
"github.com/charmbracelet/x/ansi"
"github.com/sst/opencode/internal/config"
"github.com/sst/opencode/internal/diff"
- "github.com/sst/opencode/internal/llm/models"
"github.com/sst/opencode/internal/llm/tools"
"github.com/sst/opencode/internal/message"
"github.com/sst/opencode/internal/tui/styles"
@@ -22,64 +21,25 @@ import (
type uiMessageType int
const (
- userMessageType uiMessageType = iota
- assistantMessageType
- toolMessageType
-
maxResultHeight = 10
)
-type uiMessage struct {
- ID string
- messageType uiMessageType
- content string
-}
-
-func toMarkdown(content string, focused bool, width int) string {
+func toMarkdown(content string, width int) string {
r := styles.GetMarkdownRenderer(width)
rendered, _ := r.Render(content)
- return rendered
+ return strings.TrimSuffix(rendered, "\n")
}
-func renderMessage(msg string, isUser bool, isFocused bool, width int, info ...string) string {
+func renderUserMessage(msg client.MessageInfo, width int) string {
t := theme.CurrentTheme()
-
style := styles.BaseStyle().
- // Width(width - 1).
BorderLeft(true).
Foreground(t.TextMuted()).
- BorderForeground(t.Primary()).
+ BorderForeground(t.Secondary()).
BorderStyle(lipgloss.ThickBorder())
- if isUser {
- style = style.BorderForeground(t.Secondary())
- }
-
- // Apply markdown formatting and handle background color
- parts := []string{
- styles.ForceReplaceBackgroundWithLipgloss(toMarkdown(msg, isFocused, width), t.Background()),
- }
-
- // Remove newline at the end
- parts[0] = strings.TrimSuffix(parts[0], "\n")
- if len(info) > 0 {
- parts = append(parts, info...)
- }
-
- rendered := style.Render(
- lipgloss.JoinVertical(
- lipgloss.Left,
- parts...,
- ),
- )
-
- return rendered
-}
-
-func renderUserMessage(msg client.MessageInfo, isFocused bool, width int) uiMessage {
- // var styledAttachments []string
- t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
+ // var styledAttachments []string
// attachmentStyles := baseStyle.
// MarginLeft(1).
// Background(t.TextMuted()).
@@ -95,16 +55,12 @@ func renderUserMessage(msg client.MessageInfo, isFocused bool, width int) uiMess
// styledAttachments = append(styledAttachments, attachmentStyles.Render(filename))
// }
- info := []string{}
-
// Add timestamp info
timestamp := time.UnixMilli(int64(msg.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM")
username, _ := config.GetUsername()
- info = append(info, baseStyle.
- Width(width-1).
+ info := baseStyle.
Foreground(t.TextMuted()).
- Render(fmt.Sprintf(" %s (%s)", username, timestamp)),
- )
+ Render(fmt.Sprintf(" %s (%s)", username, timestamp))
content := ""
// if len(styledAttachments) > 0 {
@@ -120,125 +76,175 @@ func renderUserMessage(msg client.MessageInfo, isFocused bool, width int) uiMess
switch part.(type) {
case client.MessagePartText:
textPart := part.(client.MessagePartText)
- content = renderMessage(textPart.Text, true, isFocused, width, info...)
+ text := toMarkdown(textPart.Text, width)
+ content = style.Render(lipgloss.JoinVertical(lipgloss.Left, text, info))
}
}
- // content = renderMessage(msg.Parts, true, isFocused, width, info...)
- userMsg := uiMessage{
- ID: msg.Id,
- messageType: userMessageType,
- content: content,
+ return content
+}
+
+func convertToMap(input *any) (map[string]any, bool) {
+ if input == nil {
+ return nil, false // Handle nil pointer
}
- return userMsg
+ value := *input // Dereference the pointer to get the interface value
+ m, ok := value.(map[string]any) // Type assertion
+ return m, ok
}
-// Returns multiple uiMessages because of the tool calls
func renderAssistantMessage(
- msg message.Message,
- msgIndex int,
- allMessages []message.Message, // we need this to get tool results and the user message
- messagesService message.Service, // We need this to get the task tool messages
- focusedUIMessageId string,
+ msg client.MessageInfo,
width int,
- position int,
showToolMessages bool,
-) []uiMessage {
- messages := []uiMessage{}
- content := strings.TrimSpace(msg.Content().String())
- thinking := msg.IsThinking()
- thinkingContent := msg.ReasoningContent().Thinking
- finished := msg.IsFinished()
- finishData := msg.FinishPart()
- info := []string{}
-
+) string {
t := theme.CurrentTheme()
+ style := styles.BaseStyle().
+ BorderLeft(true).
+ Foreground(t.TextMuted()).
+ BorderForeground(t.Primary()).
+ BorderStyle(lipgloss.ThickBorder())
+ toolStyle := styles.BaseStyle().
+ BorderLeft(true).
+ Foreground(t.TextMuted()).
+ BorderForeground(t.TextMuted()).
+ BorderStyle(lipgloss.ThickBorder())
+
baseStyle := styles.BaseStyle()
+ messages := []string{}
- // Always add timestamp info
- timestamp := msg.CreatedAt.Local().Format("02 Jan 2006 03:04 PM")
- modelName := "Assistant"
- if msg.Model != "" {
- modelName = models.SupportedModels[msg.Model].Name
- }
+ // content := strings.TrimSpace(msg.Content().String())
+ // thinking := msg.IsThinking()
+ // thinkingContent := msg.ReasoningContent().Thinking
+ // finished := msg.IsFinished()
+ // finishData := msg.FinishPart()
- info = append(info, baseStyle.
- Width(width-1).
+ // Add timestamp info
+ timestamp := time.UnixMilli(int64(msg.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM")
+ modelName := msg.Metadata.Assistant.ModelID
+ info := baseStyle.
Foreground(t.TextMuted()).
- Render(fmt.Sprintf(" %s (%s)", modelName, timestamp)),
- )
+ Render(fmt.Sprintf(" %s (%s)", modelName, timestamp))
- if finished {
- // Add finish info if available
- switch finishData.Reason {
- case message.FinishReasonCanceled:
- info = append(info, baseStyle.
- Width(width-1).
- Foreground(t.Warning()).
- Render("(canceled)"),
- )
- case message.FinishReasonError:
- info = append(info, baseStyle.
- Width(width-1).
- Foreground(t.Error()).
- Render("(error)"),
- )
- case message.FinishReasonPermissionDenied:
- info = append(info, baseStyle.
- Width(width-1).
- Foreground(t.Info()).
- Render("(permission denied)"),
- )
+ for _, p := range msg.Parts {
+ part, err := p.ValueByDiscriminator()
+ if err != nil {
+ continue //TODO: handle error?
}
- }
- if content != "" || (finished && finishData.Reason == message.FinishReasonEndTurn) {
- if content == "" {
- content = "*Finished without output*"
- }
+ switch part.(type) {
+ case client.MessagePartText:
+ textPart := part.(client.MessagePartText)
+ text := toMarkdown(textPart.Text, width)
+ content := style.Render(lipgloss.JoinVertical(lipgloss.Left, text, info))
+ messages = append(messages, content)
+
+ case client.MessagePartToolInvocation:
+ if !showToolMessages {
+ continue
+ }
- content = renderMessage(content, false, true, width, info...)
- messages = append(messages, uiMessage{
- ID: msg.ID,
- messageType: assistantMessageType,
- // position: position,
- // height: lipgloss.Height(content),
- content: content,
- })
- // position += messages[0].height
- position++ // for the space
- } else if thinking && thinkingContent != "" {
- // Render the thinking content with timestamp
- content = renderMessage(thinkingContent, false, msg.ID == focusedUIMessageId, width, info...)
- messages = append(messages, uiMessage{
- ID: msg.ID,
- messageType: assistantMessageType,
- // position: position,
- // height: lipgloss.Height(content),
- content: content,
- })
- position += lipgloss.Height(content)
- position++ // for the space
+ toolInvocationPart := part.(client.MessagePartToolInvocation)
+ toolInvocation, _ := toolInvocationPart.ToolInvocation.ValueByDiscriminator()
+ switch toolInvocation.(type) {
+ case client.MessageToolInvocationToolCall:
+ toolCall := toolInvocation.(client.MessageToolInvocationToolCall)
+ toolName := toolName(toolCall.ToolName)
+ var toolArgs []string
+ toolMap, _ := convertToMap(toolCall.Args)
+ for _, arg := range toolMap {
+ toolArgs = append(toolArgs, fmt.Sprintf("%v", arg))
+ }
+ params := renderParams(width-lipgloss.Width(toolName)-1, toolArgs...)
+ title := styles.Padded().Render(fmt.Sprintf("%s: %s", toolName, params))
+
+ content := toolStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
+ title,
+ " In progress...",
+ ))
+ messages = append(messages, content)
+
+ case client.MessageToolInvocationToolResult:
+ toolInvocationResult := toolInvocation.(client.MessageToolInvocationToolResult)
+ toolName := toolName(toolInvocationResult.ToolName)
+ var toolArgs []string
+ toolMap, _ := convertToMap(toolInvocationResult.Args)
+ for _, arg := range toolMap {
+ toolArgs = append(toolArgs, fmt.Sprintf("%v", arg))
+ }
+ result := truncateHeight(strings.TrimSpace(toolInvocationResult.Result), 10)
+ params := renderParams(width-lipgloss.Width(toolName)-1, toolArgs...)
+ title := styles.Padded().Render(fmt.Sprintf("%s: %s", toolName, params))
+
+ markdown := toMarkdown(result, width)
+
+ content := toolStyle.Render(lipgloss.JoinVertical(lipgloss.Left,
+ title,
+ markdown,
+ ))
+ messages = append(messages, content)
+ }
+ }
}
+ // if finished {
+ // // Add finish info if available
+ // switch finishData.Reason {
+ // case message.FinishReasonCanceled:
+ // info = append(info, baseStyle.
+ // Width(width-1).
+ // Foreground(t.Warning()).
+ // Render("(canceled)"),
+ // )
+ // case message.FinishReasonError:
+ // info = append(info, baseStyle.
+ // Width(width-1).
+ // Foreground(t.Error()).
+ // Render("(error)"),
+ // )
+ // case message.FinishReasonPermissionDenied:
+ // info = append(info, baseStyle.
+ // Width(width-1).
+ // Foreground(t.Info()).
+ // Render("(permission denied)"),
+ // )
+ // }
+ // }
+
+ // if content != "" || (finished && finishData.Reason == message.FinishReasonEndTurn) {
+ // if content == "" {
+ // content = "*Finished without output*"
+ // }
+ //
+ // content = renderMessage(content, false, width, info...)
+ // messages = append(messages, content)
+ // // position += messages[0].height
+ // position++ // for the space
+ // } else if thinking && thinkingContent != "" {
+ // // Render the thinking content with timestamp
+ // content = renderMessage(thinkingContent, false, width, info...)
+ // messages = append(messages, content)
+ // position += lipgloss.Height(content)
+ // position++ // for the space
+ // }
+
// Only render tool messages if they should be shown
if showToolMessages {
- for i, toolCall := range msg.ToolCalls() {
- toolCallContent := renderToolMessage(
- toolCall,
- allMessages,
- messagesService,
- focusedUIMessageId,
- false,
- width,
- i+1,
- )
- messages = append(messages, toolCallContent)
- // position += toolCallContent.height
- position++ // for the space
- }
- }
- return messages
+ // for i, toolCall := range msg.ToolCalls() {
+ // toolCallContent := renderToolMessage(
+ // toolCall,
+ // allMessages,
+ // messagesService,
+ // focusedUIMessageId,
+ // false,
+ // width,
+ // i+1,
+ // )
+ // messages = append(messages, toolCallContent)
+ // }
+ }
+
+ return strings.Join(messages, "\n\n")
}
func findToolResponse(toolCallID string, futureMessages []message.Message) *message.ToolResult {
@@ -497,7 +503,7 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
case tools.BashToolName:
resultContent = fmt.Sprintf("```bash\n%s\n```", resultContent)
return styles.ForceReplaceBackgroundWithLipgloss(
- toMarkdown(resultContent, true, width),
+ toMarkdown(resultContent, width),
t.Background(),
)
case tools.EditToolName:
@@ -517,7 +523,7 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
}
resultContent = fmt.Sprintf("```%s\n%s\n```", mdFormat, resultContent)
return styles.ForceReplaceBackgroundWithLipgloss(
- toMarkdown(resultContent, true, width),
+ toMarkdown(resultContent, width),
t.Background(),
)
case tools.GlobToolName:
@@ -537,7 +543,7 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
}
resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(metadata.Content, maxResultHeight))
return styles.ForceReplaceBackgroundWithLipgloss(
- toMarkdown(resultContent, true, width),
+ toMarkdown(resultContent, width),
t.Background(),
)
case tools.WriteToolName:
@@ -553,7 +559,7 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
}
resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(params.Content, maxResultHeight))
return styles.ForceReplaceBackgroundWithLipgloss(
- toMarkdown(resultContent, true, width),
+ toMarkdown(resultContent, width),
t.Background(),
)
case tools.BatchToolName:
@@ -591,7 +597,7 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
default:
resultContent = fmt.Sprintf("```text\n%s\n```", resultContent)
return styles.ForceReplaceBackgroundWithLipgloss(
- toMarkdown(resultContent, true, width),
+ toMarkdown(resultContent, width),
t.Background(),
)
}
@@ -605,7 +611,7 @@ func renderToolMessage(
nested bool,
width int,
position int,
-) uiMessage {
+) string {
if nested {
width = width - 3
}
@@ -634,13 +640,7 @@ func renderToolMessage(
Render(fmt.Sprintf("%s", toolAction))
content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText))
- toolMsg := uiMessage{
- messageType: toolMessageType,
- // position: position,
- // height: lipgloss.Height(content),
- content: content,
- }
- return toolMsg
+ return content
}
params := renderToolParams(width-1-lipgloss.Width(toolNameText), toolCall)
@@ -702,11 +702,5 @@ func renderToolMessage(
parts...,
)
}
- toolMsg := uiMessage{
- messageType: toolMessageType,
- // position: position,
- // height: lipgloss.Height(content),
- content: content,
- }
- return toolMsg
+ return content
}
diff --git a/internal/tui/components/chat/messages.go b/internal/tui/components/chat/messages.go
index 5fe355071..267356f90 100644
--- a/internal/tui/components/chat/messages.go
+++ b/internal/tui/components/chat/messages.go
@@ -1,8 +1,6 @@
package chat
import (
- "fmt"
- "math"
"time"
"github.com/charmbracelet/bubbles/key"
@@ -20,18 +18,11 @@ import (
"github.com/sst/opencode/pkg/client"
)
-type cacheItem struct {
- width int
- content []uiMessage
-}
-
type messagesCmp struct {
app *app.App
width, height int
viewport viewport.Model
- uiMessages []uiMessage
currentMsgID string
- cachedContent map[string]cacheItem
spinner spinner.Model
rendering bool
attachments viewport.Model
@@ -71,17 +62,17 @@ func (m *messagesCmp) Init() tea.Cmd {
}
func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- m.renderView()
+ // m.renderView()
var cmds []tea.Cmd
switch msg := msg.(type) {
case dialog.ThemeChangedMsg:
- m.rerender()
+ m.renderView()
return m, nil
case ToggleToolMessagesMsg:
m.showToolMessages = !m.showToolMessages
// Clear the cache to force re-rendering of all messages
- m.cachedContent = make(map[string]cacheItem)
+ // m.cachedContent = make(map[string]cacheItem)
m.renderView()
return m, nil
case state.SessionSelectedMsg:
@@ -171,93 +162,43 @@ func (m *messagesCmp) IsAgentWorking() bool {
return m.app.PrimaryAgentOLD.IsSessionBusy(m.app.CurrentSessionOLD.ID)
}
-func formatTimeDifference(unixTime1, unixTime2 int64) string {
- diffSeconds := float64(math.Abs(float64(unixTime2 - unixTime1)))
-
- if diffSeconds < 60 {
- return fmt.Sprintf("%.1fs", diffSeconds)
- }
-
- minutes := int(diffSeconds / 60)
- seconds := int(diffSeconds) % 60
- return fmt.Sprintf("%dm%ds", minutes, seconds)
-}
-
func (m *messagesCmp) renderView() {
- m.uiMessages = make([]uiMessage, 0)
- baseStyle := styles.BaseStyle()
-
if m.width == 0 {
return
}
+ t := theme.CurrentTheme()
+ messages := make([]string, 0)
for _, msg := range m.app.Messages {
switch msg.Role {
case client.User:
- if cache, ok := m.cachedContent[msg.Id]; ok && cache.width == m.width {
- m.uiMessages = append(m.uiMessages, cache.content...)
- continue
- }
- userMsg := renderUserMessage(
+ content := renderUserMessage(
msg,
- msg.Id == m.currentMsgID,
m.width,
)
- m.uiMessages = append(m.uiMessages, userMsg)
- m.cachedContent[msg.Id] = cacheItem{
- width: m.width,
- content: []uiMessage{userMsg},
- }
- // pos += userMsg.height + 1 // + 1 for spacing
+ messages = append(messages, content+"\n")
case client.Assistant:
- if cache, ok := m.cachedContent[msg.Id]; ok && cache.width == m.width {
- m.uiMessages = append(m.uiMessages, cache.content...)
- continue
- }
- // assistantMessages := renderAssistantMessage(
- // msg,
- // inx,
- // m.app.Messages,
- // m.app.MessagesOLD,
- // m.currentMsgID,
- // m.width,
- // pos,
- // m.showToolMessages,
- // )
- // for _, msg := range assistantMessages {
- // m.uiMessages = append(m.uiMessages, msg)
- // // pos += msg.height + 1 // + 1 for spacing
- // }
- // m.cachedContent[msg.Id] = cacheItem{
- // width: m.width,
- // content: assistantMessages,
- // }
+ content := renderAssistantMessage(
+ msg,
+ m.width,
+ m.showToolMessages,
+ )
+ messages = append(messages, content+"\n")
}
}
- messages := make([]string, 0)
- for _, v := range m.uiMessages {
- messages = append(messages, lipgloss.JoinVertical(lipgloss.Left, v.content),
- baseStyle.
+ m.viewport.SetContent(
+ styles.ForceReplaceBackgroundWithLipgloss(
+ styles.BaseStyle().
Width(m.width).
Render(
- "",
- ),
- )
- }
-
- // temp, _ := json.MarshalIndent(m.app.State, "", " ")
-
- m.viewport.SetContent(
- baseStyle.
- Width(m.width).
- Render(
- // string(temp),
- lipgloss.JoinVertical(
- lipgloss.Top,
- messages...,
+ lipgloss.JoinVertical(
+ lipgloss.Top,
+ messages...,
+ ),
),
- ),
+ t.Background(),
+ ),
)
}
@@ -416,13 +357,6 @@ func (m *messagesCmp) initialScreen() string {
)
}
-func (m *messagesCmp) rerender() {
- for _, msg := range m.app.Messages {
- delete(m.cachedContent, msg.Id)
- }
- m.renderView()
-}
-
func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
if m.width == width && m.height == height {
return nil
@@ -433,7 +367,7 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
m.viewport.Height = height - 2
m.attachments.Width = width + 40
m.attachments.Height = 3
- m.rerender()
+ m.renderView()
return nil
}
@@ -453,7 +387,7 @@ func (m *messagesCmp) Reload(session *session.Session) tea.Cmd {
if len(m.app.Messages) > 0 {
m.currentMsgID = m.app.Messages[len(m.app.Messages)-1].Id
}
- delete(m.cachedContent, m.currentMsgID)
+ // delete(m.cachedContent, m.currentMsgID)
m.rendering = true
return func() tea.Msg {
m.renderView()
@@ -476,18 +410,19 @@ func NewMessagesCmp(app *app.App) tea.Model {
FPS: time.Second / 3,
}
s := spinner.New(spinner.WithSpinner(customSpinner))
+
vp := viewport.New(0, 0)
- attachmets := viewport.New(0, 0)
+ attachments := viewport.New(0, 0)
vp.KeyMap.PageUp = messageKeys.PageUp
vp.KeyMap.PageDown = messageKeys.PageDown
vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown
+
return &messagesCmp{
app: app,
- cachedContent: make(map[string]cacheItem),
viewport: vp,
spinner: s,
- attachments: attachmets,
+ attachments: attachments,
showToolMessages: true,
}
}
diff --git a/internal/tui/tui.go b/internal/tui/tui.go
index 745fdc61e..a5ea0271a 100644
--- a/internal/tui/tui.go
+++ b/internal/tui/tui.go
@@ -285,7 +285,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.app.Session = &sessionInfo
}
- return a, nil
+
+ return a.updateAllPages(state.StateUpdatedMsg{State: a.app.State})
}
if parts[0] == "session" && parts[1] == "message" {
@@ -303,7 +304,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.Id == messageId {
a.app.Messages[i] = message
slog.Debug("Updated message", "message", message)
- return a, nil
+ return a.updateAllPages(state.StateUpdatedMsg{State: a.app.State})
}
}
@@ -316,7 +317,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// a.app.CurrentSession.Cost += message.Cost
// a.app.CurrentSession.UpdatedAt = message.CreatedAt
}
- return a, nil
+
+ return a.updateAllPages(state.StateUpdatedMsg{State: a.app.State})
}
// log key and content