diff --git a/internal/llm/tools/bash.go b/internal/llm/tools/bash.go index 5228e432d25f6f4dad56fac415e1d1023fce7173..da47526a5b252af7166562cb61165ed308a2b348 100644 --- a/internal/llm/tools/bash.go +++ b/internal/llm/tools/bash.go @@ -36,6 +36,7 @@ const ( DefaultTimeout = 1 * 60 * 1000 // 1 minutes in milliseconds MaxTimeout = 10 * 60 * 1000 // 10 minutes in milliseconds MaxOutputLength = 30000 + BashNoOutput = "no output" ) var bannedCommands = []string{ @@ -321,7 +322,7 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) EndTime: time.Now().UnixMilli(), } if stdout == "" { - return WithResponseMetadata(NewTextResponse("no output"), metadata), nil + return WithResponseMetadata(NewTextResponse(BashNoOutput), metadata), nil } return WithResponseMetadata(NewTextResponse(stdout), metadata), nil } diff --git a/internal/tui/components/chat/messages/renderer.go b/internal/tui/components/chat/messages/renderer.go index c32a7a124f160b2efe6a832ac8fea6ae8357692c..1bc586de56eaa9aa13d029c9a87381524c43e1fb 100644 --- a/internal/tui/components/chat/messages/renderer.go +++ b/internal/tui/components/chat/messages/renderer.go @@ -143,8 +143,9 @@ func (br baseRenderer) makeHeader(v *toolCallCmp, tool string, width int, params func (br baseRenderer) renderError(v *toolCallCmp, message string) string { t := styles.CurrentTheme() header := br.makeHeader(v, prettifyToolName(v.call.Name), v.textWidth(), "") - message = t.S().Error.Render(v.fit(message, v.textWidth()-2)) // -2 for padding - return joinHeaderBody(header, message) + errorTag := t.S().Base.Padding(0, 1).Background(t.Red).Foreground(t.White).Render("ERROR") + message = t.S().Base.Foreground(t.FgHalfMuted).Render(v.fit(message, v.textWidth()-3-lipgloss.Width(errorTag))) // -2 for padding and space + return joinHeaderBody(header, errorTag+" "+message) } // Register tool renderers @@ -198,6 +199,9 @@ func (br bashRenderer) Render(v *toolCallCmp) string { args := newParamBuilder().addMain(cmd).build() return br.renderWithParams(v, "Bash", args, func() string { + if v.result.Content == tools.BashNoOutput { + return "" + } return renderPlainContent(v, v.result.Content) }) } @@ -254,13 +258,12 @@ type editRenderer struct { // Render displays the edited file with a formatted diff of changes func (er editRenderer) Render(v *toolCallCmp) string { var params tools.EditParams - if err := er.unmarshalParams(v.call.Input, ¶ms); err != nil { - return er.renderError(v, "Invalid edit parameters") + var args []string + if err := er.unmarshalParams(v.call.Input, ¶ms); err == nil { + file := fsext.PrettyPath(params.FilePath) + args = newParamBuilder().addMain(file).build() } - file := fsext.PrettyPath(params.FilePath) - args := newParamBuilder().addMain(file).build() - return er.renderWithParams(v, "Edit", args, func() string { var meta tools.EditResponseMetadata if err := er.unmarshalParams(v.result.Metadata, &meta); err != nil { @@ -290,13 +293,13 @@ type writeRenderer struct { // Render displays the file being written with syntax highlighting func (wr writeRenderer) Render(v *toolCallCmp) string { var params tools.WriteParams - if err := wr.unmarshalParams(v.call.Input, ¶ms); err != nil { - return wr.renderError(v, "Invalid write parameters") + var args []string + var file string + if err := wr.unmarshalParams(v.call.Input, ¶ms); err == nil { + file = fsext.PrettyPath(params.FilePath) + args = newParamBuilder().addMain(file).build() } - file := fsext.PrettyPath(params.FilePath) - args := newParamBuilder().addMain(file).build() - return wr.renderWithParams(v, "Write", args, func() string { return renderCodeContent(v, file, params.Content, 0) }) @@ -314,16 +317,15 @@ type fetchRenderer struct { // Render displays the fetched URL with format and timeout parameters func (fr fetchRenderer) Render(v *toolCallCmp) string { var params tools.FetchParams - if err := fr.unmarshalParams(v.call.Input, ¶ms); err != nil { - return fr.renderError(v, "Invalid fetch parameters") + var args []string + if err := fr.unmarshalParams(v.call.Input, ¶ms); err == nil { + args = newParamBuilder(). + addMain(params.URL). + addKeyValue("format", params.Format). + addKeyValue("timeout", formatTimeout(params.Timeout)). + build() } - args := newParamBuilder(). - addMain(params.URL). - addKeyValue("format", params.Format). - addKeyValue("timeout", formatTimeout(params.Timeout)). - build() - return fr.renderWithParams(v, "Fetch", args, func() string { file := fr.getFileExtension(params.Format) return renderCodeContent(v, file, v.result.Content, 0) @@ -362,15 +364,14 @@ type globRenderer struct { // Render displays the glob pattern with optional path parameter func (gr globRenderer) Render(v *toolCallCmp) string { var params tools.GlobParams - if err := gr.unmarshalParams(v.call.Input, ¶ms); err != nil { - return gr.renderError(v, "Invalid glob parameters") + var args []string + if err := gr.unmarshalParams(v.call.Input, ¶ms); err == nil { + args = newParamBuilder(). + addMain(params.Pattern). + addKeyValue("path", params.Path). + build() } - args := newParamBuilder(). - addMain(params.Pattern). - addKeyValue("path", params.Path). - build() - return gr.renderWithParams(v, "Glob", args, func() string { return renderPlainContent(v, v.result.Content) }) @@ -388,17 +389,16 @@ type grepRenderer struct { // Render displays the search pattern with path, include, and literal text options func (gr grepRenderer) Render(v *toolCallCmp) string { var params tools.GrepParams - if err := gr.unmarshalParams(v.call.Input, ¶ms); err != nil { - return gr.renderError(v, "Invalid grep parameters") + var args []string + if err := gr.unmarshalParams(v.call.Input, ¶ms); err == nil { + args = newParamBuilder(). + addMain(params.Pattern). + addKeyValue("path", params.Path). + addKeyValue("include", params.Include). + addFlag("literal", params.LiteralText). + build() } - args := newParamBuilder(). - addMain(params.Pattern). - addKeyValue("path", params.Path). - addKeyValue("include", params.Include). - addFlag("literal", params.LiteralText). - build() - return gr.renderWithParams(v, "Grep", args, func() string { return renderPlainContent(v, v.result.Content) }) @@ -416,17 +416,16 @@ type lsRenderer struct { // Render displays the directory path, defaulting to current directory func (lr lsRenderer) Render(v *toolCallCmp) string { var params tools.LSParams - if err := lr.unmarshalParams(v.call.Input, ¶ms); err != nil { - return lr.renderError(v, "Invalid ls parameters") - } + var args []string + if err := lr.unmarshalParams(v.call.Input, ¶ms); err == nil { + path := params.Path + if path == "" { + path = "." + } + path = fsext.PrettyPath(path) - path := params.Path - if path == "" { - path = "." + args = newParamBuilder().addMain(path).build() } - path = fsext.PrettyPath(path) - - args := newParamBuilder().addMain(path).build() return lr.renderWithParams(v, "List", args, func() string { return renderPlainContent(v, v.result.Content) @@ -445,16 +444,15 @@ type sourcegraphRenderer struct { // Render displays the search query with optional count and context window parameters func (sr sourcegraphRenderer) Render(v *toolCallCmp) string { var params tools.SourcegraphParams - if err := sr.unmarshalParams(v.call.Input, ¶ms); err != nil { - return sr.renderError(v, "Invalid sourcegraph parameters") + var args []string + if err := sr.unmarshalParams(v.call.Input, ¶ms); err == nil { + args = newParamBuilder(). + addMain(params.Query). + addKeyValue("count", formatNonZero(params.Count)). + addKeyValue("context", formatNonZero(params.ContextWindow)). + build() } - args := newParamBuilder(). - addMain(params.Query). - addKeyValue("count", formatNonZero(params.Count)). - addKeyValue("context", formatNonZero(params.ContextWindow)). - build() - return sr.renderWithParams(v, "Sourcegraph", args, func() string { return renderPlainContent(v, v.result.Content) }) @@ -498,13 +496,15 @@ func RoundedEnumerator(children tree.Children, index int) string { func (tr agentRenderer) Render(v *toolCallCmp) string { t := styles.CurrentTheme() var params agent.AgentParams - if err := tr.unmarshalParams(v.call.Input, ¶ms); err != nil { - return tr.renderError(v, "Invalid task parameters") - } + tr.unmarshalParams(v.call.Input, ¶ms) + prompt := params.Prompt prompt = strings.ReplaceAll(prompt, "\n", " ") header := tr.makeHeader(v, "Agent", v.textWidth()) + if res, done := earlyState(header, v); done { + return res + } taskTag := t.S().Base.Padding(0, 1).MarginLeft(1).Background(t.BlueLight).Foreground(t.White).Render("Task") remainingWidth := v.textWidth() - lipgloss.Width(header) - lipgloss.Width(taskTag) - 2 // -2 for padding prompt = t.S().Muted.Width(remainingWidth).Render(prompt) @@ -608,15 +608,15 @@ func earlyState(header string, v *toolCallCmp) (string, bool) { case v.result.IsError: message = v.renderToolError() case v.cancelled: - message = "Cancelled" + message = t.S().Base.Padding(0, 1).Background(t.Border).Render("Cancelled") case v.result.ToolCallID == "": - message = "Waiting for tool to start..." + message = t.S().Base.Padding(0, 1).Background(t.Accent).Foreground(t.FgSubtle).Render("Waiting for tool to start...") default: return "", false } message = t.S().Base.PaddingLeft(2).Render(message) - return lipgloss.JoinVertical(lipgloss.Left, header, message), true + return lipgloss.JoinVertical(lipgloss.Left, header, "", message), true } func joinHeaderBody(header, body string) string { @@ -699,8 +699,9 @@ func renderCodeContent(v *toolCallCmp, path, content string, offset int) string func (v *toolCallCmp) renderToolError() string { t := styles.CurrentTheme() err := strings.ReplaceAll(v.result.Content, "\n", " ") - err = fmt.Sprintf("Error: %s", err) - return t.S().Base.Foreground(t.Error).Render(v.fit(err, v.textWidth()-2)) + errTag := t.S().Base.Padding(0, 1).Background(t.Red).Foreground(t.White).Render("ERROR") + err = fmt.Sprintf("%s %s", errTag, t.S().Base.Foreground(t.FgHalfMuted).Render(v.fit(err, v.textWidth()-2-lipgloss.Width(errTag)))) + return err } func truncateHeight(s string, h int) string {