Browse Source

wip: refactoring tui

adamdottv 9 months ago
parent
commit
26606ccbf7
3 changed files with 195 additions and 264 deletions
  1. 162 168
      internal/tui/components/chat/message.go
  2. 28 93
      internal/tui/components/chat/messages.go
  3. 5 3
      internal/tui/tui.go

+ 162 - 168
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
 }

+ 28 - 93
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,
 	}
 }

+ 5 - 3
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