@@ -0,0 +1,555 @@
9+ "github.com/charmbracelet/lipgloss/v2"
10+ "github.com/charmbracelet/x/ansi"
11+ "github.com/opencode-ai/opencode/internal/config"
12+ "github.com/opencode-ai/opencode/internal/diff"
13+ "github.com/opencode-ai/opencode/internal/highlight"
14+ "github.com/opencode-ai/opencode/internal/llm/agent"
15+ "github.com/opencode-ai/opencode/internal/llm/tools"
16+ "github.com/opencode-ai/opencode/internal/tui/styles"
17+ "github.com/opencode-ai/opencode/internal/tui/theme"
20+const responseContextHeight = 10
22+type renderer interface {
23+ // Render returns the complete (already styled) tool‑call view, not
24+ // including the outer border.
25+ Render(v *toolCallCmp) string
28+type rendererFactory func() renderer
30+type renderRegistry map[string]rendererFactory
32+func (rr renderRegistry) register(name string, f rendererFactory) { rr[name] = f }
33+func (rr renderRegistry) lookup(name string) renderer {
34+ if f, ok := rr[name]; ok {
37+ return genericRenderer{} // sensible fallback
40+var registry = renderRegistry{}
42+// Registger tool renderers
44+ registry.register(tools.BashToolName, func() renderer { return bashRenderer{} })
45+ registry.register(tools.ViewToolName, func() renderer { return viewRenderer{} })
46+ registry.register(tools.EditToolName, func() renderer { return editRenderer{} })
47+ registry.register(tools.WriteToolName, func() renderer { return writeRenderer{} })
48+ registry.register(tools.FetchToolName, func() renderer { return fetchRenderer{} })
49+ registry.register(tools.GlobToolName, func() renderer { return globRenderer{} })
50+ registry.register(tools.GrepToolName, func() renderer { return grepRenderer{} })
51+ registry.register(tools.LSToolName, func() renderer { return lsRenderer{} })
52+ registry.register(tools.SourcegraphToolName, func() renderer { return sourcegraphRenderer{} })
53+ registry.register(tools.PatchToolName, func() renderer { return patchRenderer{} })
54+ registry.register(tools.DiagnosticsToolName, func() renderer { return diagnosticsRenderer{} })
57+// -----------------------------------------------------------------------------
59+// -----------------------------------------------------------------------------
61+type genericRenderer struct{}
63+func (genericRenderer) Render(v *toolCallCmp) string {
64+ header := makeHeader(prettifyToolName(v.call.Name), v.textWidth(), v.call.Input)
65+ if res, done := earlyState(header, v); done {
68+ body := renderPlainContent(v, v.result.Content)
69+ return joinHeaderBody(header, body)
72+// -----------------------------------------------------------------------------
74+// -----------------------------------------------------------------------------
76+type bashRenderer struct{}
78+func (bashRenderer) Render(v *toolCallCmp) string {
79+ var p tools.BashParams
80+ _ = json.Unmarshal([]byte(v.call.Input), &p)
82+ cmd := strings.ReplaceAll(p.Command, "\n", " ")
83+ header := makeHeader("Bash", v.textWidth(), cmd)
84+ if res, done := earlyState(header, v); done {
87+ body := renderPlainContent(v, v.result.Content)
88+ return joinHeaderBody(header, body)
91+// -----------------------------------------------------------------------------
93+// -----------------------------------------------------------------------------
95+type viewRenderer struct{}
97+func (viewRenderer) Render(v *toolCallCmp) string {
98+ var params tools.ViewParams
99+ _ = json.Unmarshal([]byte(v.call.Input), ¶ms)
101+ file := removeWorkingDirPrefix(params.FilePath)
102+ args := []string{file}
103+ if params.Limit != 0 {
104+ args = append(args, "limit", fmt.Sprintf("%d", params.Limit))
106+ if params.Offset != 0 {
107+ args = append(args, "offset", fmt.Sprintf("%d", params.Offset))
110+ header := makeHeader("View", v.textWidth(), args...)
111+ if res, done := earlyState(header, v); done {
115+ var meta tools.ViewResponseMetadata
116+ _ = json.Unmarshal([]byte(v.result.Metadata), &meta)
118+ body := renderCodeContent(v, meta.FilePath, meta.Content, params.Offset)
119+ return joinHeaderBody(header, body)
122+// -----------------------------------------------------------------------------
124+// -----------------------------------------------------------------------------
126+type editRenderer struct{}
128+func (editRenderer) Render(v *toolCallCmp) string {
129+ var params tools.EditParams
130+ _ = json.Unmarshal([]byte(v.call.Input), ¶ms)
132+ file := removeWorkingDirPrefix(params.FilePath)
133+ header := makeHeader("Edit", v.textWidth(), file)
134+ if res, done := earlyState(header, v); done {
138+ var meta tools.EditResponseMetadata
139+ _ = json.Unmarshal([]byte(v.result.Metadata), &meta)
141+ trunc := truncateHeight(meta.Diff, responseContextHeight)
142+ diffView, _ := diff.FormatDiff(trunc, diff.WithTotalWidth(v.textWidth()))
143+ return joinHeaderBody(header, diffView)
146+// -----------------------------------------------------------------------------
148+// -----------------------------------------------------------------------------
150+type writeRenderer struct{}
152+func (writeRenderer) Render(v *toolCallCmp) string {
153+ var params tools.WriteParams
154+ _ = json.Unmarshal([]byte(v.call.Input), ¶ms)
156+ file := removeWorkingDirPrefix(params.FilePath)
157+ header := makeHeader("Write", v.textWidth(), file)
158+ if res, done := earlyState(header, v); done {
162+ body := renderCodeContent(v, file, params.Content, 0)
163+ return joinHeaderBody(header, body)
166+// -----------------------------------------------------------------------------
168+// -----------------------------------------------------------------------------
170+type fetchRenderer struct{}
172+func (fetchRenderer) Render(v *toolCallCmp) string {
173+ var params tools.FetchParams
174+ _ = json.Unmarshal([]byte(v.call.Input), ¶ms)
176+ args := []string{params.URL}
177+ if params.Format != "" {
178+ args = append(args, "format", params.Format)
180+ if params.Timeout != 0 {
181+ args = append(args, "timeout", (time.Duration(params.Timeout) * time.Second).String())
184+ header := makeHeader("Fetch", v.textWidth(), args...)
185+ if res, done := earlyState(header, v); done {
190+ switch params.Format {
197+ body := renderCodeContent(v, file, v.result.Content, 0)
198+ return joinHeaderBody(header, body)
201+// -----------------------------------------------------------------------------
203+// -----------------------------------------------------------------------------
205+type globRenderer struct{}
207+func (globRenderer) Render(v *toolCallCmp) string {
208+ var params tools.GlobParams
209+ _ = json.Unmarshal([]byte(v.call.Input), ¶ms)
211+ args := []string{params.Pattern}
212+ if params.Path != "" {
213+ args = append(args, "path", params.Path)
216+ header := makeHeader("Glob", v.textWidth(), args...)
217+ if res, done := earlyState(header, v); done {
221+ body := renderPlainContent(v, v.result.Content)
222+ return joinHeaderBody(header, body)
225+// -----------------------------------------------------------------------------
227+// -----------------------------------------------------------------------------
229+type grepRenderer struct{}
231+func (grepRenderer) Render(v *toolCallCmp) string {
232+ var params tools.GrepParams
233+ _ = json.Unmarshal([]byte(v.call.Input), ¶ms)
235+ args := []string{params.Pattern}
236+ if params.Path != "" {
237+ args = append(args, "path", params.Path)
239+ if params.Include != "" {
240+ args = append(args, "include", params.Include)
242+ if params.LiteralText {
243+ args = append(args, "literal", "true")
246+ header := makeHeader("Grep", v.textWidth(), args...)
247+ if res, done := earlyState(header, v); done {
251+ body := renderPlainContent(v, v.result.Content)
252+ return joinHeaderBody(header, body)
255+// -----------------------------------------------------------------------------
257+// -----------------------------------------------------------------------------
259+type lsRenderer struct{}
261+func (lsRenderer) Render(v *toolCallCmp) string {
262+ var params tools.LSParams
263+ _ = json.Unmarshal([]byte(v.call.Input), ¶ms)
270+ header := makeHeader("List", v.textWidth(), path)
271+ if res, done := earlyState(header, v); done {
275+ body := renderPlainContent(v, v.result.Content)
276+ return joinHeaderBody(header, body)
279+// -----------------------------------------------------------------------------
280+// Sourcegraph renderer
281+// -----------------------------------------------------------------------------
283+type sourcegraphRenderer struct{}
285+func (sourcegraphRenderer) Render(v *toolCallCmp) string {
286+ var params tools.SourcegraphParams
287+ _ = json.Unmarshal([]byte(v.call.Input), ¶ms)
289+ args := []string{params.Query}
290+ if params.Count != 0 {
291+ args = append(args, "count", fmt.Sprintf("%d", params.Count))
293+ if params.ContextWindow != 0 {
294+ args = append(args, "context", fmt.Sprintf("%d", params.ContextWindow))
297+ header := makeHeader("Sourcegraph", v.textWidth(), args...)
298+ if res, done := earlyState(header, v); done {
302+ body := renderPlainContent(v, v.result.Content)
303+ return joinHeaderBody(header, body)
306+// -----------------------------------------------------------------------------
308+// -----------------------------------------------------------------------------
310+type patchRenderer struct{}
312+func (patchRenderer) Render(v *toolCallCmp) string {
313+ var params tools.PatchParams
314+ _ = json.Unmarshal([]byte(v.call.Input), ¶ms)
316+ header := makeHeader("Patch", v.textWidth(), "multiple files")
317+ if res, done := earlyState(header, v); done {
321+ var meta tools.PatchResponseMetadata
322+ _ = json.Unmarshal([]byte(v.result.Metadata), &meta)
324+ // Format the result as a summary of changes
325+ summary := fmt.Sprintf("Changed %d files (%d+ %d-)",
326+ len(meta.FilesChanged), meta.Additions, meta.Removals)
328+ // List the changed files
329+ filesList := strings.Join(meta.FilesChanged, "\n")
331+ body := renderPlainContent(v, summary+"\n\n"+filesList)
332+ return joinHeaderBody(header, body)
335+// -----------------------------------------------------------------------------
336+// Diagnostics renderer
337+// -----------------------------------------------------------------------------
339+type diagnosticsRenderer struct{}
341+func (diagnosticsRenderer) Render(v *toolCallCmp) string {
342+ header := makeHeader("Diagnostics", v.textWidth(), "project")
343+ if res, done := earlyState(header, v); done {
347+ body := renderPlainContent(v, v.result.Content)
348+ return joinHeaderBody(header, body)
351+// makeHeader builds "<Tool>: param (key=value)" and truncates as needed.
352+func makeHeader(tool string, width int, params ...string) string {
353+ prefix := tool + ": "
354+ return prefix + renderParams(width-lipgloss.Width(prefix), params...)
357+// renders params, params[0] (params[1]=params[2] ....)
358+func renderParams(paramsWidth int, params ...string) string {
359+ if len(params) == 0 {
362+ mainParam := params[0]
363+ if len(mainParam) > paramsWidth {
364+ mainParam = mainParam[:paramsWidth-3] + "..."
367+ if len(params) == 1 {
370+ otherParams := params[1:]
371+ // create pairs of key/value
372+ // if odd number of params, the last one is a key without value
373+ if len(otherParams)%2 != 0 {
374+ otherParams = append(otherParams, "")
376+ parts := make([]string, 0, len(otherParams)/2)
377+ for i := 0; i < len(otherParams); i += 2 {
378+ key := otherParams[i]
379+ value := otherParams[i+1]
383+ parts = append(parts, fmt.Sprintf("%s=%s", key, value))
386+ partsRendered := strings.Join(parts, ", ")
387+ remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3 // count for " ()"
388+ if remainingWidth < 30 {
389+ // No space for the params, just show the main
394+ mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", "))
397+ return ansi.Truncate(mainParam, paramsWidth, "...")
400+// earlyState returns immediately‑rendered error/cancelled/ongoing states.
401+func earlyState(header string, v *toolCallCmp) (string, bool) {
403+ case v.result.IsError:
404+ return lipgloss.JoinVertical(lipgloss.Left, header, v.renderToolError()), true
406+ return lipgloss.JoinVertical(lipgloss.Left, header, "Cancelled"), true
407+ case v.result.ToolCallID == "":
408+ return lipgloss.JoinVertical(lipgloss.Left, header, "Waiting for tool to finish..."), true
414+func joinHeaderBody(header, body string) string {
415+ return lipgloss.JoinVertical(lipgloss.Left, header, "", body, "")
418+func renderPlainContent(v *toolCallCmp, content string) string {
419+ t := theme.CurrentTheme()
420+ content = strings.TrimSpace(content)
421+ lines := strings.Split(content, "\n")
424+ for i, ln := range lines {
425+ if i >= responseContextHeight {
428+ ln = " " + ln // left padding
429+ if len(ln) > v.textWidth() {
430+ ln = v.fit(ln, v.textWidth())
432+ out = append(out, lipgloss.NewStyle().
433+ Width(v.textWidth()).
434+ Background(t.BackgroundSecondary()).
435+ Foreground(t.TextMuted()).
439+ if len(lines) > responseContextHeight {
440+ out = append(out, lipgloss.NewStyle().
441+ Background(t.BackgroundSecondary()).
442+ Foreground(t.TextMuted()).
443+ Render(fmt.Sprintf("... (%d lines)", len(lines)-responseContextHeight)))
445+ return strings.Join(out, "\n")
448+func renderCodeContent(v *toolCallCmp, path, content string, offset int) string {
449+ t := theme.CurrentTheme()
450+ truncated := truncateHeight(content, responseContextHeight)
452+ highlighted, _ := highlight.SyntaxHighlight(truncated, path, t.BackgroundSecondary())
453+ lines := strings.Split(highlighted, "\n")
455+ if len(strings.Split(content, "\n")) > responseContextHeight {
456+ lines = append(lines, lipgloss.NewStyle().
457+ Background(t.BackgroundSecondary()).
458+ Foreground(t.TextMuted()).
459+ Render(fmt.Sprintf("... (%d lines)", len(strings.Split(content, "\n"))-responseContextHeight)))
462+ for i, ln := range lines {
463+ num := lipgloss.NewStyle().
464+ PaddingLeft(4).PaddingRight(2).
465+ Background(t.BackgroundSecondary()).
466+ Foreground(t.TextMuted()).
467+ Render(fmt.Sprintf("%d", i+1+offset))
468+ w := v.textWidth() - lipgloss.Width(num)
469+ lines[i] = lipgloss.JoinHorizontal(lipgloss.Left,
471+ lipgloss.NewStyle().
473+ Background(t.BackgroundSecondary()).
474+ Render(v.fit(ln, w)))
476+ return lipgloss.JoinVertical(lipgloss.Left, lines...)
479+func (v *toolCallCmp) renderToolError() string {
480+ t := theme.CurrentTheme()
481+ err := strings.ReplaceAll(v.result.Content, "\n", " ")
482+ err = fmt.Sprintf("Error: %s", err)
483+ return styles.BaseStyle().Foreground(t.Error()).Render(v.fit(err, v.textWidth()))
486+func removeWorkingDirPrefix(path string) string {
487+ wd := config.WorkingDirectory()
488+ return strings.TrimPrefix(path, wd)
491+func truncateHeight(s string, h int) string {
492+ lines := strings.Split(s, "\n")
494+ return strings.Join(lines[:h], "\n")
499+func prettifyToolName(name string) string {
501+ case agent.AgentToolName:
503+ case tools.BashToolName:
505+ case tools.EditToolName:
507+ case tools.FetchToolName:
509+ case tools.GlobToolName:
511+ case tools.GrepToolName:
513+ case tools.LSToolName:
515+ case tools.SourcegraphToolName:
516+ return "Sourcegraph"
517+ case tools.ViewToolName:
519+ case tools.WriteToolName:
521+ case tools.PatchToolName:
528+func toolAction(name string) string {
530+ case agent.AgentToolName:
531+ return "Preparing prompt..."
532+ case tools.BashToolName:
533+ return "Building command..."
534+ case tools.EditToolName:
535+ return "Preparing edit..."
536+ case tools.FetchToolName:
537+ return "Writing fetch..."
538+ case tools.GlobToolName:
539+ return "Finding files..."
540+ case tools.GrepToolName:
541+ return "Searching content..."
542+ case tools.LSToolName:
543+ return "Listing directory..."
544+ case tools.SourcegraphToolName:
545+ return "Searching code..."
546+ case tools.ViewToolName:
547+ return "Reading file..."
548+ case tools.WriteToolName:
549+ return "Preparing write..."
550+ case tools.PatchToolName:
551+ return "Preparing patch..."