| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024 |
- package chat
- import (
- "encoding/json"
- "fmt"
- "maps"
- "slices"
- "strings"
- "time"
- "github.com/charmbracelet/lipgloss/v2"
- "github.com/charmbracelet/lipgloss/v2/compat"
- "github.com/charmbracelet/x/ansi"
- "github.com/muesli/reflow/truncate"
- "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/styles"
- "github.com/sst/opencode/internal/theme"
- "github.com/sst/opencode/internal/util"
- "golang.org/x/text/cases"
- "golang.org/x/text/language"
- )
- type blockRenderer struct {
- textColor compat.AdaptiveColor
- backgroundColor compat.AdaptiveColor
- border bool
- borderColor *compat.AdaptiveColor
- borderLeft bool
- borderRight bool
- paddingTop int
- paddingBottom int
- paddingLeft int
- paddingRight int
- marginTop int
- marginBottom int
- }
- type renderingOption func(*blockRenderer)
- func WithTextColor(color compat.AdaptiveColor) renderingOption {
- return func(c *blockRenderer) {
- c.textColor = color
- }
- }
- func WithBackgroundColor(color compat.AdaptiveColor) renderingOption {
- return func(c *blockRenderer) {
- c.backgroundColor = color
- }
- }
- func WithNoBorder() renderingOption {
- return func(c *blockRenderer) {
- c.border = false
- c.paddingLeft++
- c.paddingRight++
- }
- }
- func WithBorderColor(color compat.AdaptiveColor) renderingOption {
- return func(c *blockRenderer) {
- c.borderColor = &color
- }
- }
- func WithBorderLeft() renderingOption {
- return func(c *blockRenderer) {
- c.borderLeft = true
- c.borderRight = false
- }
- }
- func WithBorderRight() renderingOption {
- return func(c *blockRenderer) {
- c.borderLeft = false
- c.borderRight = true
- }
- }
- func WithBorderBoth(value bool) renderingOption {
- return func(c *blockRenderer) {
- if value {
- c.borderLeft = true
- c.borderRight = true
- }
- }
- }
- func WithMarginTop(padding int) renderingOption {
- return func(c *blockRenderer) {
- c.marginTop = padding
- }
- }
- func WithMarginBottom(padding int) renderingOption {
- return func(c *blockRenderer) {
- c.marginBottom = padding
- }
- }
- func WithPadding(padding int) renderingOption {
- return func(c *blockRenderer) {
- c.paddingTop = padding
- c.paddingBottom = padding
- c.paddingLeft = padding
- c.paddingRight = padding
- }
- }
- func WithPaddingLeft(padding int) renderingOption {
- return func(c *blockRenderer) {
- c.paddingLeft = padding
- }
- }
- func WithPaddingRight(padding int) renderingOption {
- return func(c *blockRenderer) {
- c.paddingRight = padding
- }
- }
- func WithPaddingTop(padding int) renderingOption {
- return func(c *blockRenderer) {
- c.paddingTop = padding
- }
- }
- func WithPaddingBottom(padding int) renderingOption {
- return func(c *blockRenderer) {
- c.paddingBottom = padding
- }
- }
- func renderContentBlock(
- app *app.App,
- content string,
- width int,
- options ...renderingOption,
- ) string {
- t := theme.CurrentTheme()
- renderer := &blockRenderer{
- textColor: t.TextMuted(),
- backgroundColor: t.BackgroundPanel(),
- border: true,
- borderLeft: true,
- borderRight: false,
- paddingTop: 1,
- paddingBottom: 1,
- paddingLeft: 2,
- paddingRight: 2,
- }
- for _, option := range options {
- option(renderer)
- }
- borderColor := t.BackgroundPanel()
- if renderer.borderColor != nil {
- borderColor = *renderer.borderColor
- }
- style := styles.NewStyle().
- Foreground(renderer.textColor).
- Background(renderer.backgroundColor).
- PaddingTop(renderer.paddingTop).
- PaddingBottom(renderer.paddingBottom).
- PaddingLeft(renderer.paddingLeft).
- PaddingRight(renderer.paddingRight).
- AlignHorizontal(lipgloss.Left)
- if renderer.border {
- style = style.
- BorderStyle(lipgloss.ThickBorder()).
- BorderLeft(true).
- BorderRight(true).
- BorderLeftForeground(t.BackgroundPanel()).
- BorderLeftBackground(t.Background()).
- BorderRightForeground(t.BackgroundPanel()).
- BorderRightBackground(t.Background())
- if renderer.borderLeft {
- style = style.BorderLeftForeground(borderColor)
- }
- if renderer.borderRight {
- style = style.BorderRightForeground(borderColor)
- }
- } else {
- style = style.PaddingLeft(renderer.paddingLeft).PaddingRight(renderer.paddingRight)
- }
- content = style.Render(content)
- if renderer.marginTop > 0 {
- for range renderer.marginTop {
- content = "\n" + content
- }
- }
- if renderer.marginBottom > 0 {
- for range renderer.marginBottom {
- content = content + "\n"
- }
- }
- return content
- }
- func renderText(
- app *app.App,
- message opencode.MessageUnion,
- text string,
- author string,
- showToolDetails bool,
- width int,
- extra string,
- isThinking bool,
- isQueued bool,
- shimmer bool,
- fileParts []opencode.FilePart,
- agentParts []opencode.AgentPart,
- toolCalls ...opencode.ToolPart,
- ) string {
- t := theme.CurrentTheme()
- var ts time.Time
- backgroundColor := t.BackgroundPanel()
- var content string
- switch casted := message.(type) {
- case opencode.AssistantMessage:
- backgroundColor = t.Background()
- if isThinking {
- backgroundColor = t.BackgroundPanel()
- }
- ts = time.UnixMilli(int64(casted.Time.Created))
- if casted.Time.Completed > 0 {
- ts = time.UnixMilli(int64(casted.Time.Completed))
- }
- content = util.ToMarkdown(text, width, backgroundColor)
- if isThinking {
- var label string
- if shimmer {
- label = util.Shimmer("Thinking...", backgroundColor, t.TextMuted(), t.Accent())
- } else {
- label = styles.NewStyle().Background(backgroundColor).Foreground(t.TextMuted()).Render("Thinking...")
- }
- label = styles.NewStyle().Background(backgroundColor).Width(width - 6).Render(label)
- content = label + "\n\n" + content
- } else if strings.TrimSpace(text) == "Generating..." {
- label := util.Shimmer(text, backgroundColor, t.TextMuted(), t.Text())
- label = styles.NewStyle().Background(backgroundColor).Width(width - 6).Render(label)
- content = label
- }
- case opencode.UserMessage:
- ts = time.UnixMilli(int64(casted.Time.Created))
- base := styles.NewStyle().Foreground(t.Text()).Background(backgroundColor)
- var result strings.Builder
- lastEnd := int64(0)
- // Apply highlighting to filenames and base style to rest of text BEFORE wrapping
- textLen := int64(len(text))
- // Collect all parts to highlight (both file and agent parts)
- type highlightPart struct {
- start int64
- end int64
- color compat.AdaptiveColor
- }
- var highlights []highlightPart
- // Add file parts with secondary color
- for _, filePart := range fileParts {
- highlights = append(highlights, highlightPart{
- start: filePart.Source.Text.Start,
- end: filePart.Source.Text.End,
- color: t.Secondary(),
- })
- }
- // Add agent parts with secondary color (same as file parts)
- for _, agentPart := range agentParts {
- highlights = append(highlights, highlightPart{
- start: agentPart.Source.Start,
- end: agentPart.Source.End,
- color: t.Secondary(),
- })
- }
- // Sort highlights by start position
- slices.SortFunc(highlights, func(a, b highlightPart) int {
- if a.start < b.start {
- return -1
- }
- if a.start > b.start {
- return 1
- }
- return 0
- })
- // Merge overlapping highlights to prevent duplication
- merged := make([]highlightPart, 0)
- for _, part := range highlights {
- if len(merged) == 0 {
- merged = append(merged, part)
- continue
- }
- last := &merged[len(merged)-1]
- // If current part overlaps with the last one, merge them
- if part.start <= last.end {
- if part.end > last.end {
- last.end = part.end
- }
- } else {
- merged = append(merged, part)
- }
- }
- for _, part := range merged {
- highlight := base.Foreground(part.color)
- start, end := part.start, part.end
- if end > textLen {
- end = textLen
- }
- if start > textLen {
- start = textLen
- }
- if start > lastEnd {
- result.WriteString(base.Render(text[lastEnd:start]))
- }
- if start < end {
- result.WriteString(highlight.Render(text[start:end]))
- }
- lastEnd = end
- }
- if lastEnd < textLen {
- result.WriteString(base.Render(text[lastEnd:]))
- }
- // wrap styled text
- styledText := result.String()
- styledText = strings.ReplaceAll(styledText, "-", "\u2011")
- wrappedText := ansi.WordwrapWc(styledText, width-6, " ")
- wrappedText = strings.ReplaceAll(wrappedText, "\u2011", "-")
- content = base.Width(width - 6).Render(wrappedText)
- if isQueued {
- queuedStyle := styles.NewStyle().Background(t.Accent()).Foreground(t.BackgroundPanel()).Bold(true).Padding(0, 1)
- content = queuedStyle.Render("QUEUED") + "\n\n" + content
- }
- }
- timestamp := ts.
- Local().
- Format("02 Jan 2006 03:04 PM")
- if time.Now().Format("02 Jan 2006") == timestamp[:11] {
- timestamp = timestamp[12:]
- }
- timestamp = styles.NewStyle().
- Background(backgroundColor).
- Foreground(t.TextMuted()).
- Render(" (" + timestamp + ")")
- // Check if this is an assistant message with agent information
- var modelAndAgentSuffix string
- if assistantMsg, ok := message.(opencode.AssistantMessage); ok && assistantMsg.Mode != "" {
- // Find the agent index by name to get the correct color
- var agentIndex int
- for i, agent := range app.Agents {
- if agent.Name == assistantMsg.Mode {
- agentIndex = i
- break
- }
- }
- // Get agent color based on the original agent index (same as status bar)
- agentColor := util.GetAgentColor(agentIndex)
- // Style the agent name with the same color as status bar
- agentName := cases.Title(language.Und).String(assistantMsg.Mode)
- styledAgentName := styles.NewStyle().
- Background(backgroundColor).
- Foreground(agentColor).
- Render(agentName + " ")
- styledModelID := styles.NewStyle().
- Background(backgroundColor).
- Foreground(t.TextMuted()).
- Render(assistantMsg.ModelID)
- modelAndAgentSuffix = styledAgentName + styledModelID
- }
- var info string
- if modelAndAgentSuffix != "" {
- info = modelAndAgentSuffix + timestamp
- } else {
- info = author + timestamp
- }
- if !showToolDetails && toolCalls != nil && len(toolCalls) > 0 {
- for _, toolCall := range toolCalls {
- title := renderToolTitle(toolCall, width-2)
- style := styles.NewStyle()
- if toolCall.State.Status == opencode.ToolPartStateStatusError {
- style = style.Foreground(t.Error())
- }
- title = style.Render(title)
- title = "\n∟ " + title
- content = content + title
- }
- }
- sections := []string{content}
- if extra != "" {
- sections = append(sections, "\n"+extra+"\n")
- }
- sections = append(sections, info)
- content = strings.Join(sections, "\n")
- switch message.(type) {
- case opencode.UserMessage:
- borderColor := t.Secondary()
- if isQueued {
- borderColor = t.Accent()
- }
- return renderContentBlock(
- app,
- content,
- width,
- WithTextColor(t.Text()),
- WithBorderColor(borderColor),
- )
- case opencode.AssistantMessage:
- if isThinking {
- return renderContentBlock(
- app,
- content,
- width,
- WithTextColor(t.Text()),
- WithBackgroundColor(t.BackgroundPanel()),
- WithBorderColor(t.BackgroundPanel()),
- )
- }
- return renderContentBlock(
- app,
- content,
- width,
- WithNoBorder(),
- WithBackgroundColor(t.Background()),
- )
- }
- return ""
- }
- func renderToolDetails(
- app *app.App,
- toolCall opencode.ToolPart,
- permission opencode.Permission,
- width int,
- ) string {
- measure := util.Measure("chat.renderToolDetails")
- defer measure("tool", toolCall.Tool)
- ignoredTools := []string{"todoread"}
- if slices.Contains(ignoredTools, toolCall.Tool) {
- return ""
- }
- if toolCall.State.Status == opencode.ToolPartStateStatusPending {
- title := renderToolTitle(toolCall, width)
- return renderContentBlock(app, title, width)
- }
- var result *string
- if toolCall.State.Output != "" {
- result = &toolCall.State.Output
- }
- toolInputMap := make(map[string]any)
- if toolCall.State.Input != nil {
- value := toolCall.State.Input
- if m, ok := value.(map[string]any); ok {
- toolInputMap = m
- keys := make([]string, 0, len(toolInputMap))
- for key := range toolInputMap {
- keys = append(keys, key)
- }
- slices.Sort(keys)
- }
- }
- body := ""
- t := theme.CurrentTheme()
- backgroundColor := t.BackgroundPanel()
- borderColor := t.BackgroundPanel()
- defaultStyle := styles.NewStyle().Background(backgroundColor).Width(width - 6).Render
- baseStyle := styles.NewStyle().Background(backgroundColor).Foreground(t.Text()).Render
- mutedStyle := styles.NewStyle().Background(backgroundColor).Foreground(t.TextMuted()).Render
- permissionContent := ""
- if permission.ID != "" {
- borderColor = t.Warning()
- base := styles.NewStyle().Background(backgroundColor)
- text := base.Foreground(t.Text()).Bold(true).Render
- muted := base.Foreground(t.TextMuted()).Render
- permissionContent = "Permission required to run this tool:\n\n"
- permissionContent += text(
- "enter ",
- ) + muted(
- "accept ",
- ) + text(
- "a",
- ) + muted(
- " accept always ",
- ) + text(
- "esc",
- ) + muted(
- " reject",
- )
- }
- if permission.Metadata != nil {
- metadata, ok := toolCall.State.Metadata.(map[string]any)
- if metadata == nil || !ok {
- metadata = map[string]any{}
- }
- maps.Copy(metadata, permission.Metadata)
- toolCall.State.Metadata = metadata
- }
- if toolCall.State.Metadata != nil {
- metadata := toolCall.State.Metadata.(map[string]any)
- switch toolCall.Tool {
- case "read":
- var preview any
- if metadata != nil {
- preview = metadata["preview"]
- }
- if preview != nil && toolInputMap["filePath"] != nil {
- filename := toolInputMap["filePath"].(string)
- body = preview.(string)
- body = util.RenderFile(filename, body, width, util.WithTruncate(6))
- }
- case "edit":
- if filename, ok := toolInputMap["filePath"].(string); ok {
- var diffField any
- if metadata != nil {
- diffField = metadata["diff"]
- }
- if diffField != nil {
- patch := diffField.(string)
- var formattedDiff string
- if width < 120 {
- formattedDiff, _ = diff.FormatUnifiedDiff(
- filename,
- patch,
- diff.WithWidth(width-2),
- )
- } else {
- formattedDiff, _ = diff.FormatDiff(
- filename,
- patch,
- diff.WithWidth(width-2),
- )
- }
- body = strings.TrimSpace(formattedDiff)
- style := styles.NewStyle().
- Background(backgroundColor).
- Foreground(t.TextMuted()).
- Padding(1, 2).
- Width(width - 4)
- if diagnostics := renderDiagnostics(metadata, filename, backgroundColor, width-6); diagnostics != "" {
- diagnostics = style.Render(diagnostics)
- body += "\n" + diagnostics
- }
- title := renderToolTitle(toolCall, width)
- title = style.Render(title)
- content := title + "\n" + body
- if toolCall.State.Status == opencode.ToolPartStateStatusError {
- errorStyle := styles.NewStyle().
- Background(backgroundColor).
- Foreground(t.Error()).
- Padding(1, 2).
- Width(width - 4)
- errorContent := errorStyle.Render(toolCall.State.Error)
- content += "\n" + errorContent
- }
- if permissionContent != "" {
- permissionContent = styles.NewStyle().
- Background(backgroundColor).
- Padding(1, 2).
- Render(permissionContent)
- content += "\n" + permissionContent
- }
- content = renderContentBlock(
- app,
- content,
- width,
- WithPadding(0),
- WithBorderColor(borderColor),
- WithBorderBoth(permission.ID != ""),
- )
- return content
- }
- }
- case "write":
- if filename, ok := toolInputMap["filePath"].(string); ok {
- if content, ok := toolInputMap["content"].(string); ok {
- body = util.RenderFile(filename, content, width)
- if diagnostics := renderDiagnostics(metadata, filename, backgroundColor, width-4); diagnostics != "" {
- body += "\n\n" + diagnostics
- }
- }
- }
- case "bash":
- if command, ok := toolInputMap["command"].(string); ok {
- body = fmt.Sprintf("```console\n$ %s\n", command)
- output := metadata["output"]
- if output != nil {
- body += ansi.Strip(fmt.Sprintf("%s", output))
- }
- body += "```"
- body = util.ToMarkdown(body, width, backgroundColor)
- }
- case "webfetch":
- if format, ok := toolInputMap["format"].(string); ok && result != nil {
- body = *result
- body = util.TruncateHeight(body, 10)
- if format == "html" || format == "markdown" {
- body = util.ToMarkdown(body, width, backgroundColor)
- }
- }
- case "todowrite":
- todos := metadata["todos"]
- if todos != nil {
- for _, item := range todos.([]any) {
- todo := item.(map[string]any)
- content := todo["content"].(string)
- switch todo["status"] {
- case "completed":
- body += fmt.Sprintf("- [x] %s\n", content)
- case "cancelled":
- // strike through cancelled todo
- body += fmt.Sprintf("- [ ] ~~%s~~\n", content)
- case "in_progress":
- // highlight in progress todo
- body += fmt.Sprintf("- [ ] `%s`\n", content)
- default:
- body += fmt.Sprintf("- [ ] %s\n", content)
- }
- }
- body = util.ToMarkdown(body, width, backgroundColor)
- }
- case "task":
- summary := metadata["summary"]
- if summary != nil {
- toolcalls := summary.([]any)
- steps := []string{}
- for _, item := range toolcalls {
- data, _ := json.Marshal(item)
- var toolCall opencode.ToolPart
- _ = json.Unmarshal(data, &toolCall)
- step := renderToolTitle(toolCall, width-2)
- step = "∟ " + step
- steps = append(steps, step)
- }
- body = strings.Join(steps, "\n")
- body += "\n\n"
- // Build navigation hint with proper spacing
- cycleKeybind := app.Keybind(commands.SessionChildCycleCommand)
- cycleReverseKeybind := app.Keybind(commands.SessionChildCycleReverseCommand)
- var navParts []string
- if cycleKeybind != "" {
- navParts = append(navParts, baseStyle(cycleKeybind))
- }
- if cycleReverseKeybind != "" {
- navParts = append(navParts, baseStyle(cycleReverseKeybind))
- }
- if len(navParts) > 0 {
- body += strings.Join(navParts, mutedStyle(", ")) + mutedStyle(" navigate child sessions")
- }
- }
- body = defaultStyle(body)
- default:
- if result == nil {
- empty := ""
- result = &empty
- }
- body = *result
- body = util.TruncateHeight(body, 10)
- body = defaultStyle(body)
- }
- }
- error := ""
- if toolCall.State.Status == opencode.ToolPartStateStatusError {
- error = toolCall.State.Error
- }
- if error != "" {
- errorContent := styles.NewStyle().
- Width(width - 6).
- Foreground(t.Error()).
- Background(backgroundColor).
- Render(error)
- if body == "" {
- body = errorContent
- } else {
- body += "\n\n" + errorContent
- }
- }
- if body == "" && error == "" && result != nil {
- body = *result
- body = util.TruncateHeight(body, 10)
- body = defaultStyle(body)
- }
- if body == "" {
- body = defaultStyle("")
- }
- title := renderToolTitle(toolCall, width)
- content := title + "\n\n" + body
- if permissionContent != "" {
- content += "\n\n\n" + permissionContent
- }
- return renderContentBlock(
- app,
- content,
- width,
- WithBorderColor(borderColor),
- WithBorderBoth(permission.ID != ""),
- )
- }
- func renderToolName(name string) string {
- switch name {
- case "bash":
- return "Shell"
- case "webfetch":
- return "Fetch"
- case "invalid":
- return "Invalid"
- default:
- normalizedName := name
- if after, ok := strings.CutPrefix(name, "opencode_"); ok {
- normalizedName = after
- }
- return cases.Title(language.Und).String(normalizedName)
- }
- }
- func getTodoPhase(metadata map[string]any) string {
- todos, ok := metadata["todos"].([]any)
- if !ok || len(todos) == 0 {
- return "Plan"
- }
- counts := map[string]int{"pending": 0, "completed": 0}
- for _, item := range todos {
- if todo, ok := item.(map[string]any); ok {
- if status, ok := todo["status"].(string); ok {
- counts[status]++
- }
- }
- }
- total := len(todos)
- switch {
- case counts["pending"] == total:
- return "Creating plan"
- case counts["completed"] == total:
- return "Completing plan"
- default:
- return "Updating plan"
- }
- }
- func getTodoTitle(toolCall opencode.ToolPart) string {
- if toolCall.State.Status == opencode.ToolPartStateStatusCompleted {
- if metadata, ok := toolCall.State.Metadata.(map[string]any); ok {
- return getTodoPhase(metadata)
- }
- }
- return "Plan"
- }
- func renderToolTitle(
- toolCall opencode.ToolPart,
- width int,
- ) string {
- if toolCall.State.Status == opencode.ToolPartStateStatusPending {
- title := renderToolAction(toolCall.Tool)
- t := theme.CurrentTheme()
- shiny := util.Shimmer(title, t.BackgroundPanel(), t.TextMuted(), t.Accent())
- return styles.NewStyle().Background(t.BackgroundPanel()).Width(width - 6).Render(shiny)
- }
- toolArgs := ""
- toolArgsMap := make(map[string]any)
- if toolCall.State.Input != nil {
- value := toolCall.State.Input
- if m, ok := value.(map[string]any); ok {
- toolArgsMap = m
- keys := make([]string, 0, len(toolArgsMap))
- for key := range toolArgsMap {
- keys = append(keys, key)
- }
- slices.Sort(keys)
- firstKey := ""
- if len(keys) > 0 {
- firstKey = keys[0]
- }
- toolArgs = renderArgs(&toolArgsMap, firstKey)
- }
- }
- title := renderToolName(toolCall.Tool)
- switch toolCall.Tool {
- case "read":
- toolArgs = renderArgs(&toolArgsMap, "filePath")
- title = fmt.Sprintf("%s %s", title, toolArgs)
- case "edit", "write":
- if filename, ok := toolArgsMap["filePath"].(string); ok {
- title = fmt.Sprintf("%s %s", title, util.Relative(filename))
- }
- case "bash":
- if description, ok := toolArgsMap["description"].(string); ok {
- title = fmt.Sprintf("%s %s", title, description)
- }
- case "task":
- description := toolArgsMap["description"]
- subagent := toolArgsMap["subagent_type"]
- if description != nil && subagent != nil {
- title = fmt.Sprintf("%s[%s] %s", title, subagent, description)
- } else if description != nil {
- title = fmt.Sprintf("%s %s", title, description)
- }
- case "webfetch":
- toolArgs = renderArgs(&toolArgsMap, "url")
- title = fmt.Sprintf("%s %s", title, toolArgs)
- case "todowrite":
- title = getTodoTitle(toolCall)
- case "todoread":
- return "Plan"
- case "invalid":
- if actualTool, ok := toolArgsMap["tool"].(string); ok {
- title = renderToolName(actualTool)
- }
- default:
- toolName := renderToolName(toolCall.Tool)
- title = fmt.Sprintf("%s %s", toolName, toolArgs)
- }
- title = truncate.StringWithTail(title, uint(width-6), "...")
- if toolCall.State.Error != "" {
- t := theme.CurrentTheme()
- title = styles.NewStyle().Foreground(t.Error()).Render(title)
- }
- return title
- }
- func renderToolAction(name string) string {
- switch name {
- case "task":
- return "Delegating..."
- case "bash":
- return "Writing command..."
- case "edit":
- return "Preparing edit..."
- case "webfetch":
- return "Fetching from the web..."
- case "glob":
- return "Finding files..."
- case "grep":
- return "Searching content..."
- case "list":
- return "Listing directory..."
- case "read":
- return "Reading file..."
- case "write":
- return "Preparing write..."
- case "todowrite", "todoread":
- return "Planning..."
- case "patch":
- return "Preparing patch..."
- }
- return "Working..."
- }
- func renderArgs(args *map[string]any, titleKey string) string {
- if args == nil || len(*args) == 0 {
- return ""
- }
- keys := make([]string, 0, len(*args))
- for key := range *args {
- keys = append(keys, key)
- }
- slices.Sort(keys)
- title := ""
- parts := []string{}
- for _, key := range keys {
- value := (*args)[key]
- if value == nil {
- continue
- }
- if key == "filePath" || key == "path" {
- if strValue, ok := value.(string); ok {
- value = util.Relative(strValue)
- }
- }
- if key == titleKey {
- title = fmt.Sprintf("%s", value)
- continue
- }
- parts = append(parts, fmt.Sprintf("%s=%v", key, value))
- }
- if len(parts) == 0 {
- return title
- }
- return fmt.Sprintf("%s (%s)", title, strings.Join(parts, ", "))
- }
- // Diagnostic represents an LSP diagnostic
- type Diagnostic struct {
- Range struct {
- Start struct {
- Line int `json:"line"`
- Character int `json:"character"`
- } `json:"start"`
- } `json:"range"`
- Severity int `json:"severity"`
- Message string `json:"message"`
- }
- // renderDiagnostics formats LSP diagnostics for display in the TUI
- func renderDiagnostics(
- metadata map[string]any,
- filePath string,
- backgroundColor compat.AdaptiveColor,
- width int,
- ) string {
- if diagnosticsData, ok := metadata["diagnostics"].(map[string]any); ok {
- if fileDiagnostics, ok := diagnosticsData[filePath].([]any); ok {
- var errorDiagnostics []string
- for _, diagInterface := range fileDiagnostics {
- diagMap, ok := diagInterface.(map[string]any)
- if !ok {
- continue
- }
- // Parse the diagnostic
- var diag Diagnostic
- diagBytes, err := json.Marshal(diagMap)
- if err != nil {
- continue
- }
- if err := json.Unmarshal(diagBytes, &diag); err != nil {
- continue
- }
- // Only show error diagnostics (severity === 1)
- if diag.Severity != 1 {
- continue
- }
- line := diag.Range.Start.Line + 1 // 1-based
- column := diag.Range.Start.Character + 1 // 1-based
- errorDiagnostics = append(
- errorDiagnostics,
- fmt.Sprintf("Error [%d:%d] %s", line, column, diag.Message),
- )
- }
- if len(errorDiagnostics) == 0 {
- return ""
- }
- t := theme.CurrentTheme()
- var result strings.Builder
- for _, diagnostic := range errorDiagnostics {
- if result.Len() > 0 {
- result.WriteString("\n\n")
- }
- diagnostic = ansi.WordwrapWc(diagnostic, width, " -")
- result.WriteString(
- styles.NewStyle().
- Background(backgroundColor).
- Foreground(t.Error()).
- Render(diagnostic),
- )
- }
- return result.String()
- }
- }
- return ""
- // diagnosticsData should be a map[string][]Diagnostic
- // strDiagnosticsData := diagnosticsData.Raw()
- // diagnosticsMap := gjson.Parse(strDiagnosticsData).Value().(map[string]any)
- // fileDiagnostics, ok := diagnosticsMap[filePath]
- // if !ok {
- // return ""
- // }
- // diagnosticsList, ok := fileDiagnostics.([]any)
- // if !ok {
- // return ""
- // }
- }
|