Download internal/ui/chat/bash.go from SaylorTwift/crush: direct link, hf CLI and curl.
- Browser
- Download file 8.5 kB
-
https://huggingface.co/SaylorTwift/crush/resolve/main/internal/ui/chat/bash.go
- Command line
-
hf download hf://SaylorTwift/crush/internal/ui/chat/bash.go
-
curl -L -o bash.go https://huggingface.co/SaylorTwift/crush/resolve/main/internal/ui/chat/bash.go
8.5 kB
| package chat | |
| import ( | |
| "cmp" | |
| "encoding/json" | |
| "fmt" | |
| "strings" | |
| "charm.land/lipgloss/v2" | |
| "github.com/charmbracelet/crush/internal/agent/tools" | |
| "github.com/charmbracelet/crush/internal/message" | |
| "github.com/charmbracelet/crush/internal/ui/common" | |
| "github.com/charmbracelet/crush/internal/ui/styles" | |
| "github.com/charmbracelet/x/ansi" | |
| ) | |
| // ----------------------------------------------------------------------------- | |
| // Bash Tool | |
| // ----------------------------------------------------------------------------- | |
| // BashToolMessageItem is a message item that represents a bash tool call. | |
| type BashToolMessageItem struct { | |
| *baseToolMessageItem | |
| } | |
| var _ ToolMessageItem = (*BashToolMessageItem)(nil) | |
| // NewBashToolMessageItem creates a new [BashToolMessageItem]. | |
| func NewBashToolMessageItem( | |
| sty *styles.Styles, | |
| toolCall message.ToolCall, | |
| result *message.ToolResult, | |
| canceled bool, | |
| workingDir string, | |
| ) ToolMessageItem { | |
| base := newBaseToolMessageItem(sty, toolCall, result, &BashToolRenderContext{workingDir: workingDir}, canceled) | |
| return &BashToolMessageItem{baseToolMessageItem: base} | |
| } | |
| // BashToolRenderContext renders bash tool messages. | |
| type BashToolRenderContext struct { | |
| workingDir string | |
| } | |
| // 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, opts.Compact) | |
| } | |
| var params tools.BashParams | |
| if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { | |
| params.Command = "failed to parse command" | |
| } | |
| // Check if this is a background job. | |
| var meta tools.BashResponseMetadata | |
| if opts.HasResult() { | |
| _ = json.Unmarshal([]byte(opts.Result.Metadata), &meta) | |
| } | |
| if meta.Background { | |
| description := cmp.Or(meta.Description, params.Command) | |
| content := "Command: " + params.Command + "\n" + opts.Result.Content | |
| return renderJobTool(sty, opts, cappedWidth, "Start", meta.ShellID, description, content) | |
| } | |
| // Regular bash command. The command is always rendered expanded | |
| // (newlines preserved); expansion only controls the output body. | |
| cmd := params.Command | |
| cmd = strings.ReplaceAll(cmd, "\t", " ") | |
| cmd = common.StripBashDisplayPrefix(cmd, b.workingDir) | |
| if highlighted, err := common.SyntaxHighlightLexerName(sty, cmd, "bash", nil); err == nil { | |
| cmd = highlighted | |
| } | |
| toolParams := []string{cmd} | |
| if params.RunInBackground { | |
| toolParams = append(toolParams, "background", "true") | |
| } | |
| // The command is always rendered expanded (wrapped, never | |
| // truncated); expansion only controls the output body. | |
| headerOpts := *opts | |
| headerOpts.ExpandedContent = true | |
| header := toolHeader(sty, opts.Status, "Bash", cappedWidth, &headerOpts, toolParams...) | |
| if opts.Compact { | |
| return header | |
| } | |
| if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { | |
| return joinToolParts(header, earlyState) | |
| } | |
| if !opts.HasResult() { | |
| return header | |
| } | |
| output := meta.Output | |
| if output == "" && opts.Result.Content != tools.BashNoOutput { | |
| output = opts.Result.Content | |
| } | |
| if output == "" { | |
| return header | |
| } | |
| bodyWidth := cappedWidth - toolBodyLeftPaddingTotal | |
| body := sty.Tool.Body.Render(toolOutputPlainContent(sty, output, bodyWidth, opts.ExpandedContent)) | |
| return joinToolParts(header, body) | |
| } | |
| // ----------------------------------------------------------------------------- | |
| // Job Output Tool | |
| // ----------------------------------------------------------------------------- | |
| // JobOutputToolMessageItem is a message item for job_output tool calls. | |
| type JobOutputToolMessageItem struct { | |
| *baseToolMessageItem | |
| } | |
| var _ ToolMessageItem = (*JobOutputToolMessageItem)(nil) | |
| // NewJobOutputToolMessageItem creates a new [JobOutputToolMessageItem]. | |
| func NewJobOutputToolMessageItem( | |
| sty *styles.Styles, | |
| toolCall message.ToolCall, | |
| result *message.ToolResult, | |
| canceled bool, | |
| ) ToolMessageItem { | |
| return newBaseToolMessageItem(sty, toolCall, result, &JobOutputToolRenderContext{}, canceled) | |
| } | |
| // JobOutputToolRenderContext renders job_output tool messages. | |
| 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, opts.Compact) | |
| } | |
| var params tools.JobOutputParams | |
| if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { | |
| return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) | |
| } | |
| var description string | |
| if opts.HasResult() && opts.Result.Metadata != "" { | |
| var meta tools.JobOutputResponseMetadata | |
| if err := json.Unmarshal([]byte(opts.Result.Metadata), &meta); err == nil { | |
| description = cmp.Or(meta.Description, meta.Command) | |
| } | |
| } | |
| content := "" | |
| if opts.HasResult() { | |
| content = opts.Result.Content | |
| } | |
| return renderJobTool(sty, opts, cappedWidth, "Output", params.ShellID, description, content) | |
| } | |
| // ----------------------------------------------------------------------------- | |
| // Job Kill Tool | |
| // ----------------------------------------------------------------------------- | |
| // JobKillToolMessageItem is a message item for job_kill tool calls. | |
| type JobKillToolMessageItem struct { | |
| *baseToolMessageItem | |
| } | |
| var _ ToolMessageItem = (*JobKillToolMessageItem)(nil) | |
| // NewJobKillToolMessageItem creates a new [JobKillToolMessageItem]. | |
| func NewJobKillToolMessageItem( | |
| sty *styles.Styles, | |
| toolCall message.ToolCall, | |
| result *message.ToolResult, | |
| canceled bool, | |
| ) ToolMessageItem { | |
| return newBaseToolMessageItem(sty, toolCall, result, &JobKillToolRenderContext{}, canceled) | |
| } | |
| // JobKillToolRenderContext renders job_kill tool messages. | |
| 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, opts.Compact) | |
| } | |
| var params tools.JobKillParams | |
| if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { | |
| return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth) | |
| } | |
| var description string | |
| if opts.HasResult() && opts.Result.Metadata != "" { | |
| var meta tools.JobKillResponseMetadata | |
| if err := json.Unmarshal([]byte(opts.Result.Metadata), &meta); err == nil { | |
| description = cmp.Or(meta.Description, meta.Command) | |
| } | |
| } | |
| content := "" | |
| if opts.HasResult() { | |
| content = opts.Result.Content | |
| } | |
| return renderJobTool(sty, opts, cappedWidth, "Kill", params.ShellID, description, content) | |
| } | |
| // renderJobTool renders a job-related tool with the common pattern: | |
| // header → nested check → early state → body. | |
| func renderJobTool(sty *styles.Styles, opts *ToolRenderOpts, width int, action, shellID, description, content string) string { | |
| header := jobHeader(sty, opts.Status, action, shellID, description, width) | |
| if opts.Compact { | |
| return header | |
| } | |
| if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok { | |
| return joinToolParts(header, earlyState) | |
| } | |
| if content == "" { | |
| return header | |
| } | |
| bodyWidth := width - toolBodyLeftPaddingTotal | |
| body := sty.Tool.Body.Render(toolOutputPlainContent(sty, content, bodyWidth, opts.ExpandedContent)) | |
| return joinToolParts(header, body) | |
| } | |
| // jobHeader builds a header for job-related tools. | |
| // Format: "● Job (Action) PID shellID description..." | |
| func jobHeader(sty *styles.Styles, status ToolStatus, action, shellID, description string, width int) string { | |
| icon := toolIcon(sty, status) | |
| jobPart := sty.Tool.JobToolName.Render("Job") | |
| actionPart := sty.Tool.JobAction.Render("(" + action + ")") | |
| pidPart := sty.Tool.JobPID.Render("PID " + shellID) | |
| prefix := fmt.Sprintf("%s %s %s %s", icon, jobPart, actionPart, pidPart) | |
| if description == "" { | |
| return prefix | |
| } | |
| prefixWidth := lipgloss.Width(prefix) | |
| availableWidth := width - prefixWidth - 1 | |
| if availableWidth < 10 { | |
| return prefix | |
| } | |
| truncatedDesc := ansi.Truncate(description, availableWidth, "…") | |
| return prefix + " " + sty.Tool.JobDescription.Render(truncatedDesc) | |
| } | |
| // joinToolParts joins header and body with a blank line separator. | |
| func joinToolParts(header, body string) string { | |
| return strings.Join([]string{header, "", body}, "\n") | |
| } | |