返回 DeepSeek-Reasonix
toolcard.go
根目录 / internal / cli / toolcard.go
1 // Formats a tool call as a Claude-style card line: a "● Verb(primary arg)"
2 // header instead of the raw "-> name {json}", plus the "⎿" continuation gutter.
3 package cli
4
5 import (
6 "encoding/json"
7 "fmt"
8 "strconv"
9 "strings"
10
11 "reasonix/internal/event"
12 "reasonix/internal/shellrun"
13 "reasonix/internal/tool"
14 )
15
16 // connector is the Claude-style "⎿" gutter that ties a continuation block (tool
17 // output, streamed thinking) to the header line above it.
18 const connector = " ⎿ "
19
20 // connectorBlock renders lines under the connector: the first carries the "⎿"
21 // gutter, the rest align beneath it. Returns "" for no lines.
22 func connectorBlock(lines []string) string {
23 if len(lines) == 0 {
24 return ""
25 }
26 indent := strings.Repeat(" ", len([]rune(connector)))
27 out := dim(connector) + lines[0]
28 for _, ln := range lines[1:] {
29 out += "\n" + indent + ln
30 }
31 return out
32 }
33
34 // toolVerb maps a tool's snake_case id to the verb shown in its card.
35 var toolVerb = map[string]string{
36 "bash": "Bash",
37 "bash_output": "Output",
38 "kill_shell": "Kill",
39 "wait": "Wait",
40 "read_file": "Read",
41 "write_file": "Write",
42 "edit_file": "Update",
43 "multi_edit": "Update",
44 "move_file": "Move",
45 "delete_range": "Update",
46 "delete_symbol": "Update",
47 "notebook_edit": "Update",
48 "glob": "Glob",
49 "grep": "Search",
50 "ls": "List",
51 "web_fetch": "Fetch",
52 "web_search": "Search",
53 "complete_step": "Step",
54 "task": "Task",
55 "use_capability": "MCP",
56 }
57
58 // toolArgKey is the JSON field shown in parentheses for each tool (wait is
59 // special-cased — it carries a job_ids array, not a scalar).
60 var toolArgKey = map[string]string{
61 "bash": "command",
62 "bash_output": "job_id",
63 "kill_shell": "job_id",
64 "read_file": "path",
65 "write_file": "path",
66 "edit_file": "path",
67 "multi_edit": "path",
68 "move_file": "source_path",
69 "delete_range": "path",
70 "delete_symbol": "name",
71 "notebook_edit": "path",
72 "glob": "pattern",
73 "grep": "pattern",
74 "ls": "path",
75 "web_fetch": "url",
76 "web_search": "query",
77 "complete_step": "summary",
78 "task": "description",
79 }
80
81 // toolDot returns the "●" status glyph coloured by the tool's category so the eye
82 // can tell reads (cyan) from writes (green), shell (yellow), process control
83 // (magenta), and everything else (copper) at a glance.
84 func toolDot(name string) string {
85 var c cliColor
86 switch toolCategory[name] {
87 case "read":
88 c = activeCLITheme.toolRead
89 case "write":
90 c = activeCLITheme.success
91 case "exec":
92 c = activeCLITheme.warn
93 case "proc":
94 c = activeCLITheme.toolProc
95 default:
96 c = activeCLITheme.accent
97 }
98 return themeFg(c, "●")
99 }
100
101 var toolCategory = map[string]string{
102 "read_file": "read", "ls": "read", "glob": "read", "grep": "read",
103 "web_fetch": "read", "web_search": "read", "bash_output": "read",
104 "write_file": "write", "edit_file": "write", "multi_edit": "write",
105 "move_file": "write", "delete_range": "write", "delete_symbol": "write", "notebook_edit": "write",
106 "bash": "exec",
107 "wait": "proc", "kill_shell": "proc",
108 }
109
110 // toolDisplayName returns the card verb for a tool: a mapped builtin verb, the
111 // short name for an MCP tool (mcp__server__tool), or the raw id as a fallback.
112 func toolDisplayName(name string) string {
113 if _, short, ok := tool.SplitMCPName(name); ok {
114 return short
115 }
116 if v, ok := toolVerb[name]; ok {
117 return v
118 }
119 return name
120 }
121
122 // shellToolDisplayName prefers the actual interpreter label when structured
123 // execution metadata is present (Git Bash / Windows PowerShell / PowerShell 7+).
124 func shellToolDisplayName(name string, ex *event.ShellExecution) string {
125 if name == "bash" && ex != nil && ex.Shell != "" {
126 return shellrun.DisplayName(&tool.ShellExecution{Shell: ex.Shell, ShellVersion: ex.ShellVersion})
127 }
128 return toolDisplayName(name)
129 }
130
131 // shellFailureDetail appends exit code, failure phase, and mutation-risk hints
132 // for failed shell results. Empty when there is no structured metadata.
133 func shellFailureDetail(ex *event.ShellExecution) string {
134 if ex == nil {
135 return ""
136 }
137 var parts []string
138 if ex.ExitCode != nil {
139 parts = append(parts, fmt.Sprintf("exit %d", *ex.ExitCode))
140 }
141 if ex.FailurePhase != "" {
142 parts = append(parts, ex.FailurePhase)
143 }
144 switch ex.FailurePhase {
145 case tool.ShellPhasePreflight, tool.ShellPhaseAuthorization, tool.ShellPhaseDependency, tool.ShellPhaseLaunch:
146 parts = append(parts, "not executed")
147 default:
148 if ex.MutationRisk == tool.ShellMutationMayBePartial {
149 parts = append(parts, "may be partial")
150 }
151 }
152 return strings.Join(parts, " · ")
153 }
154
155 // toolArg pulls the primary argument shown in the card's parentheses.
156 func toolArg(name, args string) string {
157 var m map[string]any
158 if json.Unmarshal([]byte(args), &m) != nil {
159 return ""
160 }
161 if name == "wait" {
162 return argList(m["job_ids"])
163 }
164 if name == "use_capability" {
165 if id, ok := m["capability_id"].(string); ok && strings.TrimSpace(id) != "" {
166 return strings.TrimSpace(id)
167 }
168 if action, ok := m["action"].(string); ok {
169 return strings.TrimSpace(action)
170 }
171 return ""
172 }
173 v, ok := m[toolArgKey[name]]
174 if !ok {
175 return ""
176 }
177 switch x := v.(type) {
178 case string:
179 return strings.TrimSpace(x)
180 case []any:
181 return argList(x)
182 case float64:
183 return strconv.Itoa(int(x))
184 default:
185 return ""
186 }
187 }
188
189 func argList(v any) string {
190 arr, ok := v.([]any)
191 if !ok {
192 return ""
193 }
194 parts := make([]string, 0, len(arr))
195 for _, e := range arr {
196 if s, ok := e.(string); ok {
197 parts = append(parts, s)
198 }
199 }
200 return strings.Join(parts, ", ")
201 }
202
203 // toolCard renders the dispatch line: " ⏺ Verb(arg)", arg clamped to width.
204 func toolCard(name, args string, width int) string {
205 return " " + toolDot(name) + " " + toolHead(name, toolArg(name, args), width)
206 }
207
208 // toolHead builds "Verb(arg)" with the verb bold and the arg clamped to fit the
209 // remaining width; shared by toolCard and the diff block header.
210 func toolHead(name, arg string, width int) string {
211 label := toolDisplayName(name)
212 head := bold(label)
213 if arg != "" {
214 avail := width - 4 - len([]rune(label)) - 2
215 head += dim("(") + clampPlain(arg, avail) + dim(")")
216 }
217 return head
218 }
219
219 lines GO