diff --git a/internal/ui/chat/agent.go b/internal/ui/chat/agent.go index 4784b314169f92efe4e80bf875eea5fd3780fe86..c2a439ff23d0bd046b75076ea30de68b60cdcc54 100644 --- a/internal/ui/chat/agent.go +++ b/internal/ui/chat/agent.go @@ -99,6 +99,7 @@ type AgentToolRenderContext struct { // RenderTool implements the [ToolRenderer] interface. func (r *AgentToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if !opts.ToolCall.Finished && !opts.IsCanceled() && len(r.agent.nestedTools) == 0 { return pendingTool(sty, "Agent", opts.Anim) } @@ -109,7 +110,7 @@ func (r *AgentToolRenderContext) RenderTool(sty *styles.Styles, width int, opts prompt := params.Prompt prompt = strings.ReplaceAll(prompt, "\n", " ") - header := toolHeader(sty, opts.Status, "Agent", width, opts.Compact) + header := toolHeader(sty, opts.Status, "Agent", cappedWidth, opts.Compact) if opts.Compact { return header } @@ -119,7 +120,7 @@ func (r *AgentToolRenderContext) RenderTool(sty *styles.Styles, width int, opts taskTagWidth := lipgloss.Width(taskTag) // Calculate remaining width for prompt. - remainingWidth := width - taskTagWidth - 3 // -3 for spacing + remainingWidth := min(cappedWidth-taskTagWidth-3, maxTextWidth-taskTagWidth-3) // -3 for spacing promptText := sty.Tool.AgentPrompt.Width(remainingWidth).Render(prompt) @@ -156,7 +157,7 @@ func (r *AgentToolRenderContext) RenderTool(sty *styles.Styles, width int, opts // Add body content when completed. if opts.HasResult() && opts.Result.Content != "" { - body := toolOutputMarkdownContent(sty, opts.Result.Content, width-toolBodyLeftPaddingTotal, opts.ExpandedContent) + body := toolOutputMarkdownContent(sty, opts.Result.Content, cappedWidth-toolBodyLeftPaddingTotal, opts.ExpandedContent) return joinToolParts(result, body) } @@ -229,6 +230,7 @@ type agenticFetchParams struct { // RenderTool implements the [ToolRenderer] interface. func (r *AgenticFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if !opts.ToolCall.Finished && !opts.IsCanceled() && len(r.fetch.nestedTools) == 0 { return pendingTool(sty, "Agentic Fetch", opts.Anim) } @@ -245,7 +247,7 @@ func (r *AgenticFetchToolRenderContext) RenderTool(sty *styles.Styles, width int toolParams = append(toolParams, params.URL) } - header := toolHeader(sty, opts.Status, "Agentic Fetch", width, opts.Compact, toolParams...) + header := toolHeader(sty, opts.Status, "Agentic Fetch", cappedWidth, opts.Compact, toolParams...) if opts.Compact { return header } @@ -255,7 +257,7 @@ func (r *AgenticFetchToolRenderContext) RenderTool(sty *styles.Styles, width int promptTagWidth := lipgloss.Width(promptTag) // Calculate remaining width for prompt text. - remainingWidth := width - promptTagWidth - 3 // -3 for spacing + remainingWidth := min(cappedWidth-promptTagWidth-3, maxTextWidth-promptTagWidth-3) // -3 for spacing promptText := sty.Tool.AgentPrompt.Width(remainingWidth).Render(prompt) @@ -292,7 +294,7 @@ func (r *AgenticFetchToolRenderContext) RenderTool(sty *styles.Styles, width int // Add body content when completed. if opts.HasResult() && opts.Result.Content != "" { - body := toolOutputMarkdownContent(sty, opts.Result.Content, width-toolBodyLeftPaddingTotal, opts.ExpandedContent) + body := toolOutputMarkdownContent(sty, opts.Result.Content, cappedWidth-toolBodyLeftPaddingTotal, opts.ExpandedContent) return joinToolParts(result, body) } diff --git a/internal/ui/chat/assistant.go b/internal/ui/chat/assistant.go index b9aa19456eb05739484c0b4d1a28813a7b46bb11..4ce71dda2515e5489900c33eb716e1d6d884409a 100644 --- a/internal/ui/chat/assistant.go +++ b/internal/ui/chat/assistant.go @@ -79,20 +79,22 @@ func (a *AssistantMessageItem) ID() string { // RawRender implements [MessageItem]. func (a *AssistantMessageItem) RawRender(width int) string { + cappedWidth := cappedMessageWidth(width) + var spinner string if a.isSpinning() { spinner = a.renderSpinning() } - content, height, ok := a.getCachedRender(width) + content, height, ok := a.getCachedRender(cappedWidth) if !ok { - content = a.renderMessageContent(width) + content = a.renderMessageContent(cappedWidth) height = lipgloss.Height(content) // cache the rendered content - a.setCachedRender(content, width, height) + a.setCachedRender(content, cappedWidth, height) } - highlightedContent := a.renderHighlighted(content, width, height) + highlightedContent := a.renderHighlighted(content, cappedWidth, height) if spinner != "" { if highlightedContent != "" { highlightedContent += "\n\n" diff --git a/internal/ui/chat/bash.go b/internal/ui/chat/bash.go index 445043aef9809b69126d0c409596a299f6a3aa58..18be27ee01b4fcc21749789fc65ec0b71c2b0d4b 100644 --- a/internal/ui/chat/bash.go +++ b/internal/ui/chat/bash.go @@ -39,6 +39,7 @@ type BashToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (b *BashToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "Bash", opts.Anim) } @@ -57,7 +58,7 @@ func (b *BashToolRenderContext) RenderTool(sty *styles.Styles, width int, opts * if meta.Background { description := cmp.Or(meta.Description, params.Command) content := "Command: " + params.Command + "\n" + opts.Result.Content - return renderJobTool(sty, opts, width, "Start", meta.ShellID, description, content) + return renderJobTool(sty, opts, cappedWidth, "Start", meta.ShellID, description, content) } // Regular bash command. @@ -68,12 +69,12 @@ func (b *BashToolRenderContext) RenderTool(sty *styles.Styles, width int, opts * toolParams = append(toolParams, "background", "true") } - header := toolHeader(sty, opts.Status, "Bash", width, opts.Compact, toolParams...) + header := toolHeader(sty, opts.Status, "Bash", cappedWidth, opts.Compact, toolParams...) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } @@ -89,7 +90,7 @@ func (b *BashToolRenderContext) RenderTool(sty *styles.Styles, width int, opts * return header } - bodyWidth := width - toolBodyLeftPaddingTotal + bodyWidth := cappedWidth - toolBodyLeftPaddingTotal body := sty.Tool.Body.Render(toolOutputPlainContent(sty, output, bodyWidth, opts.ExpandedContent)) return joinToolParts(header, body) } @@ -120,13 +121,14 @@ type JobOutputToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (j *JobOutputToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "Job", opts.Anim) } var params tools.JobOutputParams if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { - return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width) + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) } var description string @@ -141,7 +143,7 @@ func (j *JobOutputToolRenderContext) RenderTool(sty *styles.Styles, width int, o if opts.HasResult() { content = opts.Result.Content } - return renderJobTool(sty, opts, width, "Output", params.ShellID, description, content) + return renderJobTool(sty, opts, cappedWidth, "Output", params.ShellID, description, content) } // ----------------------------------------------------------------------------- @@ -170,13 +172,14 @@ type JobKillToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (j *JobKillToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "Job", opts.Anim) } var params tools.JobKillParams if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { - return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width) + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) } var description string @@ -191,7 +194,7 @@ func (j *JobKillToolRenderContext) RenderTool(sty *styles.Styles, width int, opt if opts.HasResult() { content = opts.Result.Content } - return renderJobTool(sty, opts, width, "Kill", params.ShellID, description, content) + return renderJobTool(sty, opts, cappedWidth, "Kill", params.ShellID, description, content) } // renderJobTool renders a job-related tool with the common pattern: diff --git a/internal/ui/chat/diagnostics.go b/internal/ui/chat/diagnostics.go index 16dbda3563b55d881944eea4328d1f2ff99d2d87..68d2ac4a00dc880c27904468008fb8f6b2fcf9c5 100644 --- a/internal/ui/chat/diagnostics.go +++ b/internal/ui/chat/diagnostics.go @@ -35,6 +35,7 @@ type DiagnosticsToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (d *DiagnosticsToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "Diagnostics", opts.Anim) } @@ -48,12 +49,12 @@ func (d *DiagnosticsToolRenderContext) RenderTool(sty *styles.Styles, width int, mainParam = fsext.PrettyPath(params.FilePath) } - header := toolHeader(sty, opts.Status, "Diagnostics", width, opts.Compact, mainParam) + header := toolHeader(sty, opts.Status, "Diagnostics", cappedWidth, opts.Compact, mainParam) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } @@ -61,7 +62,7 @@ func (d *DiagnosticsToolRenderContext) RenderTool(sty *styles.Styles, width int, return header } - bodyWidth := width - toolBodyLeftPaddingTotal + bodyWidth := cappedWidth - toolBodyLeftPaddingTotal body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent)) return joinToolParts(header, body) } diff --git a/internal/ui/chat/fetch.go b/internal/ui/chat/fetch.go index 588b2926258b01b8579330211de83eb266a5adcd..e3f3a809550385dfd0ec557e98151ffc731acc93 100644 --- a/internal/ui/chat/fetch.go +++ b/internal/ui/chat/fetch.go @@ -34,13 +34,14 @@ type FetchToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (f *FetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "Fetch", opts.Anim) } var params tools.FetchParams if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { - return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width) + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) } toolParams := []string{params.URL} @@ -51,12 +52,12 @@ func (f *FetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts toolParams = append(toolParams, "timeout", formatTimeout(params.Timeout)) } - header := toolHeader(sty, opts.Status, "Fetch", width, opts.Compact, toolParams...) + header := toolHeader(sty, opts.Status, "Fetch", cappedWidth, opts.Compact, toolParams...) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } @@ -66,7 +67,7 @@ func (f *FetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts // Determine file extension for syntax highlighting based on format. file := getFileExtensionForFormat(params.Format) - body := toolOutputCodeContent(sty, file, opts.Result.Content, 0, width, opts.ExpandedContent) + body := toolOutputCodeContent(sty, file, opts.Result.Content, 0, cappedWidth, opts.ExpandedContent) return joinToolParts(header, body) } @@ -108,22 +109,23 @@ type WebFetchToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (w *WebFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "Fetch", opts.Anim) } var params tools.WebFetchParams if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { - return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width) + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) } toolParams := []string{params.URL} - header := toolHeader(sty, opts.Status, "Fetch", width, opts.Compact, toolParams...) + header := toolHeader(sty, opts.Status, "Fetch", cappedWidth, opts.Compact, toolParams...) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } @@ -131,7 +133,7 @@ func (w *WebFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, op return header } - body := toolOutputMarkdownContent(sty, opts.Result.Content, width, opts.ExpandedContent) + body := toolOutputMarkdownContent(sty, opts.Result.Content, cappedWidth, opts.ExpandedContent) return joinToolParts(header, body) } @@ -161,22 +163,23 @@ type WebSearchToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (w *WebSearchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "Search", opts.Anim) } var params tools.WebSearchParams if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { - return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width) + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) } toolParams := []string{params.Query} - header := toolHeader(sty, opts.Status, "Search", width, opts.Compact, toolParams...) + header := toolHeader(sty, opts.Status, "Search", cappedWidth, opts.Compact, toolParams...) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } @@ -184,6 +187,6 @@ func (w *WebSearchToolRenderContext) RenderTool(sty *styles.Styles, width int, o return header } - body := toolOutputMarkdownContent(sty, opts.Result.Content, width, opts.ExpandedContent) + body := toolOutputMarkdownContent(sty, opts.Result.Content, cappedWidth, opts.ExpandedContent) return joinToolParts(header, body) } diff --git a/internal/ui/chat/file.go b/internal/ui/chat/file.go index 13cb5104233af51756806cebb9b545b3bb5076f0..d558f79d597871bf6074d33c76b44549ee6725d5 100644 --- a/internal/ui/chat/file.go +++ b/internal/ui/chat/file.go @@ -37,13 +37,14 @@ type ViewToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (v *ViewToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "View", opts.Anim) } var params tools.ViewParams if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { - return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width) + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) } file := fsext.PrettyPath(params.FilePath) @@ -55,12 +56,12 @@ func (v *ViewToolRenderContext) RenderTool(sty *styles.Styles, width int, opts * toolParams = append(toolParams, "offset", fmt.Sprintf("%d", params.Offset)) } - header := toolHeader(sty, opts.Status, "View", width, opts.Compact, toolParams...) + header := toolHeader(sty, opts.Status, "View", cappedWidth, opts.Compact, toolParams...) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } @@ -86,7 +87,7 @@ func (v *ViewToolRenderContext) RenderTool(sty *styles.Styles, width int, opts * } // Render code content with syntax highlighting. - body := toolOutputCodeContent(sty, params.FilePath, content, params.Offset, width, opts.ExpandedContent) + body := toolOutputCodeContent(sty, params.FilePath, content, params.Offset, cappedWidth, opts.ExpandedContent) return joinToolParts(header, body) } @@ -116,22 +117,23 @@ type WriteToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (w *WriteToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "Write", opts.Anim) } var params tools.WriteParams if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { - return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width) + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) } file := fsext.PrettyPath(params.FilePath) - header := toolHeader(sty, opts.Status, "Write", width, opts.Compact, file) + header := toolHeader(sty, opts.Status, "Write", cappedWidth, opts.Compact, file) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } @@ -140,7 +142,7 @@ func (w *WriteToolRenderContext) RenderTool(sty *styles.Styles, width int, opts } // Render code content with syntax highlighting. - body := toolOutputCodeContent(sty, params.FilePath, params.Content, 0, width, opts.ExpandedContent) + body := toolOutputCodeContent(sty, params.FilePath, params.Content, 0, cappedWidth, opts.ExpandedContent) return joinToolParts(header, body) } @@ -301,13 +303,14 @@ type DownloadToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (d *DownloadToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "Download", opts.Anim) } var params tools.DownloadParams if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { - return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width) + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) } toolParams := []string{params.URL} @@ -318,12 +321,12 @@ func (d *DownloadToolRenderContext) RenderTool(sty *styles.Styles, width int, op toolParams = append(toolParams, "timeout", formatTimeout(params.Timeout)) } - header := toolHeader(sty, opts.Status, "Download", width, opts.Compact, toolParams...) + header := toolHeader(sty, opts.Status, "Download", cappedWidth, opts.Compact, toolParams...) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } @@ -331,7 +334,7 @@ func (d *DownloadToolRenderContext) RenderTool(sty *styles.Styles, width int, op return header } - bodyWidth := width - toolBodyLeftPaddingTotal + bodyWidth := cappedWidth - toolBodyLeftPaddingTotal body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent)) return joinToolParts(header, body) } diff --git a/internal/ui/chat/generic.go b/internal/ui/chat/generic.go index 269bf651f7ec402d5e41aecabfb7aee0d9272cb5..6b0ac433028daf7a06c57f85c7799250e9652f6f 100644 --- a/internal/ui/chat/generic.go +++ b/internal/ui/chat/generic.go @@ -31,6 +31,7 @@ type GenericToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (g *GenericToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) name := genericPrettyName(opts.ToolCall.Name) if opts.IsPending() { @@ -39,7 +40,7 @@ func (g *GenericToolRenderContext) RenderTool(sty *styles.Styles, width int, opt var params map[string]any if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { - return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width) + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) } var toolParams []string @@ -48,12 +49,12 @@ func (g *GenericToolRenderContext) RenderTool(sty *styles.Styles, width int, opt toolParams = append(toolParams, string(parsed)) } - header := toolHeader(sty, opts.Status, name, width, opts.Compact, toolParams...) + header := toolHeader(sty, opts.Status, name, cappedWidth, opts.Compact, toolParams...) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } @@ -61,7 +62,7 @@ func (g *GenericToolRenderContext) RenderTool(sty *styles.Styles, width int, opt return header } - bodyWidth := width - toolBodyLeftPaddingTotal + bodyWidth := cappedWidth - toolBodyLeftPaddingTotal // Handle image data. if opts.Result.Data != "" && strings.HasPrefix(opts.Result.MIMEType, "image/") { diff --git a/internal/ui/chat/lsp_restart.go b/internal/ui/chat/lsp_restart.go index 4ee188a42428167314cd34aa60828cb87d121b79..66c316fcaf7c949711babeb9ebe864e558ae5bc0 100644 --- a/internal/ui/chat/lsp_restart.go +++ b/internal/ui/chat/lsp_restart.go @@ -30,6 +30,7 @@ type LSPRestartToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (r *LSPRestartToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "Restart LSP", opts.Anim) } @@ -42,12 +43,12 @@ func (r *LSPRestartToolRenderContext) RenderTool(sty *styles.Styles, width int, toolParams = append(toolParams, params.Name) } - header := toolHeader(sty, opts.Status, "Restart LSP", width, opts.Compact, toolParams...) + header := toolHeader(sty, opts.Status, "Restart LSP", cappedWidth, opts.Compact, toolParams...) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } @@ -55,7 +56,7 @@ func (r *LSPRestartToolRenderContext) RenderTool(sty *styles.Styles, width int, return header } - bodyWidth := width - toolBodyLeftPaddingTotal + bodyWidth := cappedWidth - toolBodyLeftPaddingTotal body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent)) return joinToolParts(header, body) } diff --git a/internal/ui/chat/mcp.go b/internal/ui/chat/mcp.go index 5cf750bacf7227744f06cc2d5253d98ad1713cbd..c4d124e7381a9ddaa39f56750367d3f2cf4d207f 100644 --- a/internal/ui/chat/mcp.go +++ b/internal/ui/chat/mcp.go @@ -32,9 +32,10 @@ type MCPToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (b *MCPToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) toolNameParts := strings.SplitN(opts.ToolCall.Name, "_", 3) if len(toolNameParts) != 3 { - return toolErrorContent(sty, &message.ToolResult{Content: "Invalid tool name"}, width) + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid tool name"}, cappedWidth) } mcpName := prettyName(toolNameParts[1]) toolName := prettyName(toolNameParts[2]) @@ -50,7 +51,7 @@ func (b *MCPToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *T var params map[string]any if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { - return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width) + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) } var toolParams []string @@ -59,12 +60,12 @@ func (b *MCPToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *T toolParams = append(toolParams, string(parsed)) } - header := toolHeader(sty, opts.Status, name, width, opts.Compact, toolParams...) + header := toolHeader(sty, opts.Status, name, cappedWidth, opts.Compact, toolParams...) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } @@ -72,7 +73,7 @@ func (b *MCPToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *T return header } - bodyWidth := width - toolBodyLeftPaddingTotal + bodyWidth := cappedWidth - toolBodyLeftPaddingTotal // see if the result is json var result json.RawMessage var body string diff --git a/internal/ui/chat/messages.go b/internal/ui/chat/messages.go index 0c5a3ed6a8a9d65d26cf67adff5f308e3d82a929..0c5668a20d52c5975dc63cb37da8090e9aa0ca7f 100644 --- a/internal/ui/chat/messages.go +++ b/internal/ui/chat/messages.go @@ -245,6 +245,11 @@ func (a *AssistantInfoItem) renderContent(width int) string { return common.Section(a.sty, assistant, width) } +// cappedMessageWidth returns the maximum width for message content for readability. +func cappedMessageWidth(availableWidth int) int { + return min(availableWidth-MessageLeftPaddingTotal, maxTextWidth) +} + // ExtractMessageItems extracts [MessageItem]s from a [message.Message]. It // returns all parts of the message as [MessageItem]s. // diff --git a/internal/ui/chat/references.go b/internal/ui/chat/references.go index 25fee7a15710c5ce1f470e470ff3b491da5000c3..2d7efe8df3ed38bf3768d7ae13c433fc05c17418 100644 --- a/internal/ui/chat/references.go +++ b/internal/ui/chat/references.go @@ -31,6 +31,7 @@ type ReferencesToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (r *ReferencesToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "Find References", opts.Anim) } @@ -43,12 +44,12 @@ func (r *ReferencesToolRenderContext) RenderTool(sty *styles.Styles, width int, toolParams = append(toolParams, "path", fsext.PrettyPath(params.Path)) } - header := toolHeader(sty, opts.Status, "Find References", width, opts.Compact, toolParams...) + header := toolHeader(sty, opts.Status, "Find References", cappedWidth, opts.Compact, toolParams...) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } @@ -56,7 +57,7 @@ func (r *ReferencesToolRenderContext) RenderTool(sty *styles.Styles, width int, return header } - bodyWidth := width - toolBodyLeftPaddingTotal + bodyWidth := cappedWidth - toolBodyLeftPaddingTotal body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent)) return joinToolParts(header, body) } diff --git a/internal/ui/chat/search.go b/internal/ui/chat/search.go index 2a252936f63c41dd18afde4ef725ed43a3c23a95..2342f671fdaed3bfdcf56619864bd3b60987d8a6 100644 --- a/internal/ui/chat/search.go +++ b/internal/ui/chat/search.go @@ -35,13 +35,14 @@ type GlobToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (g *GlobToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "Glob", opts.Anim) } var params tools.GlobParams if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { - return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width) + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) } toolParams := []string{params.Pattern} @@ -49,12 +50,12 @@ func (g *GlobToolRenderContext) RenderTool(sty *styles.Styles, width int, opts * toolParams = append(toolParams, "path", params.Path) } - header := toolHeader(sty, opts.Status, "Glob", width, opts.Compact, toolParams...) + header := toolHeader(sty, opts.Status, "Glob", cappedWidth, opts.Compact, toolParams...) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } @@ -62,7 +63,7 @@ func (g *GlobToolRenderContext) RenderTool(sty *styles.Styles, width int, opts * return header } - bodyWidth := width - toolBodyLeftPaddingTotal + bodyWidth := cappedWidth - toolBodyLeftPaddingTotal body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent)) return joinToolParts(header, body) } @@ -93,13 +94,14 @@ type GrepToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (g *GrepToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "Grep", opts.Anim) } var params tools.GrepParams if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { - return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width) + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) } toolParams := []string{params.Pattern} @@ -113,12 +115,12 @@ func (g *GrepToolRenderContext) RenderTool(sty *styles.Styles, width int, opts * toolParams = append(toolParams, "literal", "true") } - header := toolHeader(sty, opts.Status, "Grep", width, opts.Compact, toolParams...) + header := toolHeader(sty, opts.Status, "Grep", cappedWidth, opts.Compact, toolParams...) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } @@ -126,7 +128,7 @@ func (g *GrepToolRenderContext) RenderTool(sty *styles.Styles, width int, opts * return header } - bodyWidth := width - toolBodyLeftPaddingTotal + bodyWidth := cappedWidth - toolBodyLeftPaddingTotal body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent)) return joinToolParts(header, body) } @@ -157,13 +159,14 @@ type LSToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (l *LSToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "List", opts.Anim) } var params tools.LSParams if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { - return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width) + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) } path := params.Path @@ -172,12 +175,12 @@ func (l *LSToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *To } path = fsext.PrettyPath(path) - header := toolHeader(sty, opts.Status, "List", width, opts.Compact, path) + header := toolHeader(sty, opts.Status, "List", cappedWidth, opts.Compact, path) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } @@ -185,7 +188,7 @@ func (l *LSToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *To return header } - bodyWidth := width - toolBodyLeftPaddingTotal + bodyWidth := cappedWidth - toolBodyLeftPaddingTotal body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent)) return joinToolParts(header, body) } @@ -216,13 +219,14 @@ type SourcegraphToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (s *SourcegraphToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "Sourcegraph", opts.Anim) } var params tools.SourcegraphParams if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { - return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width) + return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) } toolParams := []string{params.Query} @@ -233,12 +237,12 @@ func (s *SourcegraphToolRenderContext) RenderTool(sty *styles.Styles, width int, toolParams = append(toolParams, "context", formatNonZero(params.ContextWindow)) } - header := toolHeader(sty, opts.Status, "Sourcegraph", width, opts.Compact, toolParams...) + header := toolHeader(sty, opts.Status, "Sourcegraph", cappedWidth, opts.Compact, toolParams...) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } @@ -246,7 +250,7 @@ func (s *SourcegraphToolRenderContext) RenderTool(sty *styles.Styles, width int, return header } - bodyWidth := width - toolBodyLeftPaddingTotal + bodyWidth := cappedWidth - toolBodyLeftPaddingTotal body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent)) return joinToolParts(header, body) } diff --git a/internal/ui/chat/todos.go b/internal/ui/chat/todos.go index 42e9762b8bf1685495b65626bc36b1b3f45031a8..5678d0e47f4c3a808c13c1dc6209f9194e9f9482 100644 --- a/internal/ui/chat/todos.go +++ b/internal/ui/chat/todos.go @@ -39,6 +39,7 @@ type TodosToolRenderContext struct{} // RenderTool implements the [ToolRenderer] interface. func (t *TodosToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) if opts.IsPending() { return pendingTool(sty, "To-Do", opts.Anim) } @@ -81,7 +82,7 @@ func (t *TodosToolRenderContext) RenderTool(sty *styles.Styles, width int, opts } else { headerText = fmt.Sprintf("created %d todos", meta.Total) } - body = FormatTodosList(sty, meta.Todos, styles.ArrowRightIcon, width) + body = FormatTodosList(sty, meta.Todos, styles.ArrowRightIcon, cappedWidth) } else { // Build header based on what changed. hasCompleted := len(meta.JustCompleted) > 0 @@ -107,7 +108,7 @@ func (t *TodosToolRenderContext) RenderTool(sty *styles.Styles, width int, opts // Build body with details. if allCompleted { // Show all todos when all are completed, like when created. - body = FormatTodosList(sty, meta.Todos, styles.ArrowRightIcon, width) + body = FormatTodosList(sty, meta.Todos, styles.ArrowRightIcon, cappedWidth) } else if meta.JustStarted != "" { body = sty.Tool.TodoInProgressIcon.Render(styles.ArrowRightIcon+" ") + sty.Base.Render(meta.JustStarted) @@ -118,12 +119,12 @@ func (t *TodosToolRenderContext) RenderTool(sty *styles.Styles, width int, opts } toolParams := []string{headerText} - header := toolHeader(sty, opts.Status, "To-Do", width, opts.Compact, toolParams...) + header := toolHeader(sty, opts.Status, "To-Do", cappedWidth, opts.Compact, toolParams...) if opts.Compact { return header } - if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { return joinToolParts(header, earlyState) } diff --git a/internal/ui/chat/tools.go b/internal/ui/chat/tools.go index 07bf40f96b08c24907f0bd65d80cebfb74eae58b..f7702cc1fe516bb3dee7d57ce15fed050299019f 100644 --- a/internal/ui/chat/tools.go +++ b/internal/ui/chat/tools.go @@ -295,6 +295,9 @@ func (t *baseToolMessageItem) Animate(msg anim.StepMsg) tea.Cmd { // RawRender implements [MessageItem]. func (t *baseToolMessageItem) RawRender(width int) string { toolItemWidth := width - MessageLeftPaddingTotal + if t.hasCappedWidth { + toolItemWidth = cappedMessageWidth(width) + } content, height, ok := t.getCachedRender(toolItemWidth) // if we are spinning or there is no cache rerender @@ -770,6 +773,11 @@ func roundedEnumerator(lPadding, width int) tree.Enumerator { func toolOutputMarkdownContent(sty *styles.Styles, content string, width int, expanded bool) string { content = stringext.NormalizeSpace(content) + // Cap width for readability. + if width > maxTextWidth { + width = maxTextWidth + } + renderer := common.PlainMarkdownRenderer(sty, width) rendered, err := renderer.Render(content) if err != nil { diff --git a/internal/ui/chat/user.go b/internal/ui/chat/user.go index 814a0270aad00bbf85c78629ffdfaf01a17c2e7f..91211590ce66dd0dd7edbde03becdf469e26b521 100644 --- a/internal/ui/chat/user.go +++ b/internal/ui/chat/user.go @@ -36,13 +36,15 @@ func NewUserMessageItem(sty *styles.Styles, message *message.Message, attachment // RawRender implements [MessageItem]. func (m *UserMessageItem) RawRender(width int) string { - content, height, ok := m.getCachedRender(width) + cappedWidth := cappedMessageWidth(width) + + content, height, ok := m.getCachedRender(cappedWidth) // cache hit if ok { - return m.renderHighlighted(content, width, height) + return m.renderHighlighted(content, cappedWidth, height) } - renderer := common.MarkdownRenderer(m.sty, width) + renderer := common.MarkdownRenderer(m.sty, cappedWidth) msgContent := strings.TrimSpace(m.message.Content().Text) result, err := renderer.Render(msgContent) @@ -53,7 +55,7 @@ func (m *UserMessageItem) RawRender(width int) string { } if len(m.message.BinaryContent()) > 0 { - attachmentsStr := m.renderAttachments(width) + attachmentsStr := m.renderAttachments(cappedWidth) if content == "" { content = attachmentsStr } else { @@ -62,8 +64,8 @@ func (m *UserMessageItem) RawRender(width int) string { } height = lipgloss.Height(content) - m.setCachedRender(content, width, height) - return m.renderHighlighted(content, width, height) + m.setCachedRender(content, cappedWidth, height) + return m.renderHighlighted(content, cappedWidth, height) } // Render implements MessageItem.