mirror of
https://github.com/sst/opencode.git
synced 2025-08-30 17:57:25 +00:00
712 lines
20 KiB
Go
712 lines
20 KiB
Go
package chat
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
"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"
|
|
"github.com/sst/opencode/internal/tui/theme"
|
|
"github.com/sst/opencode/pkg/client"
|
|
)
|
|
|
|
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 {
|
|
r := styles.GetMarkdownRenderer(width)
|
|
rendered, _ := r.Render(content)
|
|
return rendered
|
|
}
|
|
|
|
func renderMessage(msg string, isUser bool, isFocused bool, width int, info ...string) string {
|
|
t := theme.CurrentTheme()
|
|
|
|
style := styles.BaseStyle().
|
|
// Width(width - 1).
|
|
BorderLeft(true).
|
|
Foreground(t.TextMuted()).
|
|
BorderForeground(t.Primary()).
|
|
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()
|
|
// attachmentStyles := baseStyle.
|
|
// MarginLeft(1).
|
|
// Background(t.TextMuted()).
|
|
// Foreground(t.Text())
|
|
// for _, attachment := range msg.BinaryContent() {
|
|
// file := filepath.Base(attachment.Path)
|
|
// var filename string
|
|
// if len(file) > 10 {
|
|
// filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, file[0:7])
|
|
// } else {
|
|
// filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, file)
|
|
// }
|
|
// 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).
|
|
Foreground(t.TextMuted()).
|
|
Render(fmt.Sprintf(" %s (%s)", username, timestamp)),
|
|
)
|
|
|
|
content := ""
|
|
// if len(styledAttachments) > 0 {
|
|
// attachmentContent := baseStyle.Width(width).Render(lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...))
|
|
// content = renderMessage(msg.Content().String(), true, isFocused, width, append(info, attachmentContent)...)
|
|
// } else {
|
|
for _, p := range msg.Parts {
|
|
part, err := p.ValueByDiscriminator()
|
|
if err != nil {
|
|
continue //TODO: handle error?
|
|
}
|
|
|
|
switch part.(type) {
|
|
case client.MessagePartText:
|
|
textPart := part.(client.MessagePartText)
|
|
content = renderMessage(textPart.Text, true, isFocused, width, info...)
|
|
}
|
|
}
|
|
// content = renderMessage(msg.Parts, true, isFocused, width, info...)
|
|
|
|
userMsg := uiMessage{
|
|
ID: msg.Id,
|
|
messageType: userMessageType,
|
|
content: content,
|
|
}
|
|
return userMsg
|
|
}
|
|
|
|
// 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,
|
|
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{}
|
|
|
|
t := theme.CurrentTheme()
|
|
baseStyle := styles.BaseStyle()
|
|
|
|
// 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
|
|
}
|
|
|
|
info = append(info, baseStyle.
|
|
Width(width-1).
|
|
Foreground(t.TextMuted()).
|
|
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)"),
|
|
)
|
|
}
|
|
}
|
|
|
|
if content != "" || (finished && finishData.Reason == message.FinishReasonEndTurn) {
|
|
if content == "" {
|
|
content = "*Finished without output*"
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func findToolResponse(toolCallID string, futureMessages []message.Message) *message.ToolResult {
|
|
for _, msg := range futureMessages {
|
|
for _, result := range msg.ToolResults() {
|
|
if result.ToolCallID == toolCallID {
|
|
return &result
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func toolName(name string) string {
|
|
switch name {
|
|
// case agent.AgentToolName:
|
|
// return "Task"
|
|
case tools.BashToolName:
|
|
return "Bash"
|
|
case tools.EditToolName:
|
|
return "Edit"
|
|
case tools.FetchToolName:
|
|
return "Fetch"
|
|
case tools.GlobToolName:
|
|
return "Glob"
|
|
case tools.GrepToolName:
|
|
return "Grep"
|
|
case tools.LSToolName:
|
|
return "List"
|
|
case tools.ViewToolName:
|
|
return "View"
|
|
case tools.WriteToolName:
|
|
return "Write"
|
|
case tools.PatchToolName:
|
|
return "Patch"
|
|
case tools.BatchToolName:
|
|
return "Batch"
|
|
}
|
|
return name
|
|
}
|
|
|
|
func getToolAction(name string) string {
|
|
switch name {
|
|
// case agent.AgentToolName:
|
|
// return "Preparing prompt..."
|
|
case tools.BashToolName:
|
|
return "Building command..."
|
|
case tools.EditToolName:
|
|
return "Preparing edit..."
|
|
case tools.FetchToolName:
|
|
return "Writing fetch..."
|
|
case tools.GlobToolName:
|
|
return "Finding files..."
|
|
case tools.GrepToolName:
|
|
return "Searching content..."
|
|
case tools.LSToolName:
|
|
return "Listing directory..."
|
|
case tools.ViewToolName:
|
|
return "Reading file..."
|
|
case tools.WriteToolName:
|
|
return "Preparing write..."
|
|
case tools.PatchToolName:
|
|
return "Preparing patch..."
|
|
case tools.BatchToolName:
|
|
return "Running batch operations..."
|
|
}
|
|
return "Working..."
|
|
}
|
|
|
|
// renders params, params[0] (params[1]=params[2] ....)
|
|
func renderParams(paramsWidth int, params ...string) string {
|
|
if len(params) == 0 {
|
|
return ""
|
|
}
|
|
mainParam := params[0]
|
|
if len(mainParam) > paramsWidth {
|
|
mainParam = mainParam[:paramsWidth-3] + "..."
|
|
}
|
|
|
|
if len(params) == 1 {
|
|
return mainParam
|
|
}
|
|
otherParams := params[1:]
|
|
// create pairs of key/value
|
|
// if odd number of params, the last one is a key without value
|
|
if len(otherParams)%2 != 0 {
|
|
otherParams = append(otherParams, "")
|
|
}
|
|
parts := make([]string, 0, len(otherParams)/2)
|
|
for i := 0; i < len(otherParams); i += 2 {
|
|
key := otherParams[i]
|
|
value := otherParams[i+1]
|
|
if value == "" {
|
|
continue
|
|
}
|
|
parts = append(parts, fmt.Sprintf("%s=%s", key, value))
|
|
}
|
|
|
|
partsRendered := strings.Join(parts, ", ")
|
|
remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 5 // for the space
|
|
if remainingWidth < 30 {
|
|
// No space for the params, just show the main
|
|
return mainParam
|
|
}
|
|
|
|
if len(parts) > 0 {
|
|
mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
|
|
}
|
|
|
|
return ansi.Truncate(mainParam, paramsWidth, "...")
|
|
}
|
|
|
|
func removeWorkingDirPrefix(path string) string {
|
|
wd := config.WorkingDirectory()
|
|
if strings.HasPrefix(path, wd) {
|
|
path = strings.TrimPrefix(path, wd)
|
|
}
|
|
if strings.HasPrefix(path, "/") {
|
|
path = strings.TrimPrefix(path, "/")
|
|
}
|
|
if strings.HasPrefix(path, "./") {
|
|
path = strings.TrimPrefix(path, "./")
|
|
}
|
|
if strings.HasPrefix(path, "../") {
|
|
path = strings.TrimPrefix(path, "../")
|
|
}
|
|
return path
|
|
}
|
|
|
|
func renderToolParams(paramWidth int, toolCall message.ToolCall) string {
|
|
params := ""
|
|
switch toolCall.Name {
|
|
// case agent.AgentToolName:
|
|
// var params agent.AgentParams
|
|
// json.Unmarshal([]byte(toolCall.Input), ¶ms)
|
|
// prompt := strings.ReplaceAll(params.Prompt, "\n", " ")
|
|
// return renderParams(paramWidth, prompt)
|
|
case tools.BashToolName:
|
|
var params tools.BashParams
|
|
json.Unmarshal([]byte(toolCall.Input), ¶ms)
|
|
command := strings.ReplaceAll(params.Command, "\n", " ")
|
|
return renderParams(paramWidth, command)
|
|
case tools.EditToolName:
|
|
var params tools.EditParams
|
|
json.Unmarshal([]byte(toolCall.Input), ¶ms)
|
|
filePath := removeWorkingDirPrefix(params.FilePath)
|
|
return renderParams(paramWidth, filePath)
|
|
case tools.FetchToolName:
|
|
var params tools.FetchParams
|
|
json.Unmarshal([]byte(toolCall.Input), ¶ms)
|
|
url := params.URL
|
|
toolParams := []string{
|
|
url,
|
|
}
|
|
if params.Format != "" {
|
|
toolParams = append(toolParams, "format", params.Format)
|
|
}
|
|
if params.Timeout != 0 {
|
|
toolParams = append(toolParams, "timeout", (time.Duration(params.Timeout) * time.Second).String())
|
|
}
|
|
return renderParams(paramWidth, toolParams...)
|
|
case tools.GlobToolName:
|
|
var params tools.GlobParams
|
|
json.Unmarshal([]byte(toolCall.Input), ¶ms)
|
|
pattern := params.Pattern
|
|
toolParams := []string{
|
|
pattern,
|
|
}
|
|
if params.Path != "" {
|
|
toolParams = append(toolParams, "path", params.Path)
|
|
}
|
|
return renderParams(paramWidth, toolParams...)
|
|
case tools.GrepToolName:
|
|
var params tools.GrepParams
|
|
json.Unmarshal([]byte(toolCall.Input), ¶ms)
|
|
pattern := params.Pattern
|
|
toolParams := []string{
|
|
pattern,
|
|
}
|
|
if params.Path != "" {
|
|
toolParams = append(toolParams, "path", params.Path)
|
|
}
|
|
if params.Include != "" {
|
|
toolParams = append(toolParams, "include", params.Include)
|
|
}
|
|
if params.LiteralText {
|
|
toolParams = append(toolParams, "literal", "true")
|
|
}
|
|
return renderParams(paramWidth, toolParams...)
|
|
case tools.LSToolName:
|
|
var params tools.LSParams
|
|
json.Unmarshal([]byte(toolCall.Input), ¶ms)
|
|
path := params.Path
|
|
if path == "" {
|
|
path = "."
|
|
}
|
|
return renderParams(paramWidth, path)
|
|
case tools.ViewToolName:
|
|
var params tools.ViewParams
|
|
json.Unmarshal([]byte(toolCall.Input), ¶ms)
|
|
filePath := removeWorkingDirPrefix(params.FilePath)
|
|
toolParams := []string{
|
|
filePath,
|
|
}
|
|
if params.Limit != 0 {
|
|
toolParams = append(toolParams, "limit", fmt.Sprintf("%d", params.Limit))
|
|
}
|
|
if params.Offset != 0 {
|
|
toolParams = append(toolParams, "offset", fmt.Sprintf("%d", params.Offset))
|
|
}
|
|
return renderParams(paramWidth, toolParams...)
|
|
case tools.WriteToolName:
|
|
var params tools.WriteParams
|
|
json.Unmarshal([]byte(toolCall.Input), ¶ms)
|
|
filePath := removeWorkingDirPrefix(params.FilePath)
|
|
return renderParams(paramWidth, filePath)
|
|
case tools.BatchToolName:
|
|
var params tools.BatchParams
|
|
json.Unmarshal([]byte(toolCall.Input), ¶ms)
|
|
return renderParams(paramWidth, fmt.Sprintf("%d parallel calls", len(params.Calls)))
|
|
default:
|
|
input := strings.ReplaceAll(toolCall.Input, "\n", " ")
|
|
params = renderParams(paramWidth, input)
|
|
}
|
|
return params
|
|
}
|
|
|
|
func truncateHeight(content string, height int) string {
|
|
lines := strings.Split(content, "\n")
|
|
if len(lines) > height {
|
|
return strings.Join(lines[:height], "\n")
|
|
}
|
|
return content
|
|
}
|
|
|
|
func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, width int) string {
|
|
t := theme.CurrentTheme()
|
|
baseStyle := styles.BaseStyle()
|
|
|
|
if response.IsError {
|
|
errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " "))
|
|
errContent = ansi.Truncate(errContent, width-1, "...")
|
|
return baseStyle.
|
|
Width(width).
|
|
Foreground(t.Error()).
|
|
Render(errContent)
|
|
}
|
|
|
|
resultContent := truncateHeight(response.Content, maxResultHeight)
|
|
switch toolCall.Name {
|
|
// case agent.AgentToolName:
|
|
// return styles.ForceReplaceBackgroundWithLipgloss(
|
|
// toMarkdown(resultContent, false, width),
|
|
// t.Background(),
|
|
// )
|
|
case tools.BashToolName:
|
|
resultContent = fmt.Sprintf("```bash\n%s\n```", resultContent)
|
|
return styles.ForceReplaceBackgroundWithLipgloss(
|
|
toMarkdown(resultContent, true, width),
|
|
t.Background(),
|
|
)
|
|
case tools.EditToolName:
|
|
metadata := tools.EditResponseMetadata{}
|
|
json.Unmarshal([]byte(response.Metadata), &metadata)
|
|
formattedDiff, _ := diff.FormatDiff(metadata.Diff, diff.WithTotalWidth(width))
|
|
return formattedDiff
|
|
case tools.FetchToolName:
|
|
var params tools.FetchParams
|
|
json.Unmarshal([]byte(toolCall.Input), ¶ms)
|
|
mdFormat := "markdown"
|
|
switch params.Format {
|
|
case "text":
|
|
mdFormat = "text"
|
|
case "html":
|
|
mdFormat = "html"
|
|
}
|
|
resultContent = fmt.Sprintf("```%s\n%s\n```", mdFormat, resultContent)
|
|
return styles.ForceReplaceBackgroundWithLipgloss(
|
|
toMarkdown(resultContent, true, width),
|
|
t.Background(),
|
|
)
|
|
case tools.GlobToolName:
|
|
return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
|
|
case tools.GrepToolName:
|
|
return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
|
|
case tools.LSToolName:
|
|
return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
|
|
case tools.ViewToolName:
|
|
metadata := tools.ViewResponseMetadata{}
|
|
json.Unmarshal([]byte(response.Metadata), &metadata)
|
|
ext := filepath.Ext(metadata.FilePath)
|
|
if ext == "" {
|
|
ext = ""
|
|
} else {
|
|
ext = strings.ToLower(ext[1:])
|
|
}
|
|
resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(metadata.Content, maxResultHeight))
|
|
return styles.ForceReplaceBackgroundWithLipgloss(
|
|
toMarkdown(resultContent, true, width),
|
|
t.Background(),
|
|
)
|
|
case tools.WriteToolName:
|
|
params := tools.WriteParams{}
|
|
json.Unmarshal([]byte(toolCall.Input), ¶ms)
|
|
metadata := tools.WriteResponseMetadata{}
|
|
json.Unmarshal([]byte(response.Metadata), &metadata)
|
|
ext := filepath.Ext(params.FilePath)
|
|
if ext == "" {
|
|
ext = ""
|
|
} else {
|
|
ext = strings.ToLower(ext[1:])
|
|
}
|
|
resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(params.Content, maxResultHeight))
|
|
return styles.ForceReplaceBackgroundWithLipgloss(
|
|
toMarkdown(resultContent, true, width),
|
|
t.Background(),
|
|
)
|
|
case tools.BatchToolName:
|
|
var batchResult tools.BatchResult
|
|
if err := json.Unmarshal([]byte(resultContent), &batchResult); err != nil {
|
|
return baseStyle.Width(width).Foreground(t.Error()).Render(fmt.Sprintf("Error parsing batch result: %s", err))
|
|
}
|
|
|
|
var toolCalls []string
|
|
for i, result := range batchResult.Results {
|
|
toolName := toolName(result.ToolName)
|
|
|
|
// Format the tool input as a string
|
|
inputStr := string(result.ToolInput)
|
|
|
|
// Format the result
|
|
var resultStr string
|
|
if result.Error != "" {
|
|
resultStr = fmt.Sprintf("Error: %s", result.Error)
|
|
} else {
|
|
var toolResponse tools.ToolResponse
|
|
if err := json.Unmarshal(result.Result, &toolResponse); err != nil {
|
|
resultStr = "Error parsing tool response"
|
|
} else {
|
|
resultStr = truncateHeight(toolResponse.Content, 3)
|
|
}
|
|
}
|
|
|
|
// Format the tool call
|
|
toolCall := fmt.Sprintf("%d. %s: %s\n %s", i+1, toolName, inputStr, resultStr)
|
|
toolCalls = append(toolCalls, toolCall)
|
|
}
|
|
|
|
return baseStyle.Width(width).Foreground(t.TextMuted()).Render(strings.Join(toolCalls, "\n\n"))
|
|
default:
|
|
resultContent = fmt.Sprintf("```text\n%s\n```", resultContent)
|
|
return styles.ForceReplaceBackgroundWithLipgloss(
|
|
toMarkdown(resultContent, true, width),
|
|
t.Background(),
|
|
)
|
|
}
|
|
}
|
|
|
|
func renderToolMessage(
|
|
toolCall message.ToolCall,
|
|
allMessages []message.Message,
|
|
messagesService message.Service,
|
|
focusedUIMessageId string,
|
|
nested bool,
|
|
width int,
|
|
position int,
|
|
) uiMessage {
|
|
if nested {
|
|
width = width - 3
|
|
}
|
|
|
|
t := theme.CurrentTheme()
|
|
baseStyle := styles.BaseStyle()
|
|
|
|
style := baseStyle.
|
|
Width(width - 1).
|
|
BorderLeft(true).
|
|
BorderStyle(lipgloss.ThickBorder()).
|
|
PaddingLeft(1).
|
|
BorderForeground(t.TextMuted())
|
|
|
|
response := findToolResponse(toolCall.ID, allMessages)
|
|
toolNameText := baseStyle.Foreground(t.TextMuted()).
|
|
Render(fmt.Sprintf("%s: ", toolName(toolCall.Name)))
|
|
|
|
if !toolCall.Finished {
|
|
// Get a brief description of what the tool is doing
|
|
toolAction := getToolAction(toolCall.Name)
|
|
|
|
progressText := baseStyle.
|
|
Width(width - 2 - lipgloss.Width(toolNameText)).
|
|
Foreground(t.TextMuted()).
|
|
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
|
|
}
|
|
|
|
params := renderToolParams(width-1-lipgloss.Width(toolNameText), toolCall)
|
|
responseContent := ""
|
|
if response != nil {
|
|
responseContent = renderToolResponse(toolCall, *response, width-2)
|
|
responseContent = strings.TrimSuffix(responseContent, "\n")
|
|
} else {
|
|
responseContent = baseStyle.
|
|
Italic(true).
|
|
Width(width - 2).
|
|
Foreground(t.TextMuted()).
|
|
Render("Waiting for response...")
|
|
}
|
|
|
|
parts := []string{}
|
|
if !nested {
|
|
formattedParams := baseStyle.
|
|
Width(width - 2 - lipgloss.Width(toolNameText)).
|
|
Foreground(t.TextMuted()).
|
|
Render(params)
|
|
|
|
parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, formattedParams))
|
|
} else {
|
|
prefix := baseStyle.
|
|
Foreground(t.TextMuted()).
|
|
Render(" └ ")
|
|
formattedParams := baseStyle.
|
|
Width(width - 2 - lipgloss.Width(toolNameText)).
|
|
Foreground(t.TextMuted()).
|
|
Render(params)
|
|
parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolNameText, formattedParams))
|
|
}
|
|
|
|
// if toolCall.Name == agent.AgentToolName {
|
|
// taskMessages, _ := messagesService.List(context.Background(), toolCall.ID)
|
|
// toolCalls := []message.ToolCall{}
|
|
// for _, v := range taskMessages {
|
|
// toolCalls = append(toolCalls, v.ToolCalls()...)
|
|
// }
|
|
// for _, call := range toolCalls {
|
|
// rendered := renderToolMessage(call, []message.Message{}, messagesService, focusedUIMessageId, true, width, 0)
|
|
// parts = append(parts, rendered.content)
|
|
// }
|
|
// }
|
|
if responseContent != "" && !nested {
|
|
parts = append(parts, responseContent)
|
|
}
|
|
|
|
content := style.Render(
|
|
lipgloss.JoinVertical(
|
|
lipgloss.Left,
|
|
parts...,
|
|
),
|
|
)
|
|
if nested {
|
|
content = lipgloss.JoinVertical(
|
|
lipgloss.Left,
|
|
parts...,
|
|
)
|
|
}
|
|
toolMsg := uiMessage{
|
|
messageType: toolMessageType,
|
|
// position: position,
|
|
// height: lipgloss.Height(content),
|
|
content: content,
|
|
}
|
|
return toolMsg
|
|
}
|