|
|
@@ -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
|
|
|
}
|