blob: 512fa6537562217d401f934f09e8d7290841c76b [file] [log] [blame]
Earl Lee2e463fb2025-04-17 11:22:22 -07001package termui
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "os"
11 "os/exec"
12 "os/signal"
13 "strings"
14 "sync"
15 "syscall"
16 "text/template"
17 "time"
18
Josh Bleecher Snydera0801ad2025-04-25 19:34:53 +000019 "github.com/dustin/go-humanize"
Earl Lee2e463fb2025-04-17 11:22:22 -070020 "github.com/fatih/color"
21 "golang.org/x/term"
22 "sketch.dev/loop"
23)
24
25var (
26 // toolUseTemplTxt defines how tool invocations appear in the terminal UI.
27 // Keep this template in sync with the tools defined in claudetool package
28 // and registered in loop/agent.go.
29 // Add formatting for new tools as they are created.
30 // TODO: should this be part of tool definition to make it harder to forget to set up?
Josh Bleecher Snyderc3c20232025-05-07 05:46:04 -070031 toolUseTemplTxt = `{{if .msg.ToolError}}ใ€ฐ๏ธ {{end -}}
Earl Lee2e463fb2025-04-17 11:22:22 -070032{{if eq .msg.ToolName "think" -}}
33 ๐Ÿง  {{.input.thoughts -}}
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070034{{else if eq .msg.ToolName "todo_read" -}}
35 ๐Ÿ“‹ Reading todo list
36{{else if eq .msg.ToolName "todo_write" }}
37{{range .input.tasks}}{{if eq .status "queued"}}โšช{{else if eq .status "in-progress"}}๐Ÿฆ‰{{else if eq .status "completed"}}โœ…{{end}} {{.task}}
38{{end}}
Earl Lee2e463fb2025-04-17 11:22:22 -070039{{else if eq .msg.ToolName "keyword_search" -}}
Josh Bleecher Snyder453a62f2025-05-01 10:14:33 -070040 ๐Ÿ” {{ .input.query}}: {{.input.search_terms -}}
Earl Lee2e463fb2025-04-17 11:22:22 -070041{{else if eq .msg.ToolName "bash" -}}
Philip Zeyligerb60f0f22025-04-23 18:19:32 +000042 ๐Ÿ–ฅ๏ธ{{if .input.background}}๐Ÿ”„{{end}} {{ .input.command -}}
Earl Lee2e463fb2025-04-17 11:22:22 -070043{{else if eq .msg.ToolName "patch" -}}
44 โŒจ๏ธ {{.input.path -}}
45{{else if eq .msg.ToolName "done" -}}
46{{/* nothing to show here, the agent will write more in its next message */}}
Josh Bleecher Snyder19969a92025-06-05 14:34:02 -070047{{else if eq .msg.ToolName "set-slug" -}}
48๐ŸŒ {{.input.slug}}
49{{else if eq .msg.ToolName "commit-message-style" -}}
50๐ŸŒฑ learn git commit message style
Josh Bleecher Snyder74d690e2025-05-14 18:16:03 -070051{{else if eq .msg.ToolName "about_sketch" -}}
52๐Ÿ“š About Sketch
Earl Lee2e463fb2025-04-17 11:22:22 -070053{{else if eq .msg.ToolName "codereview" -}}
54 ๐Ÿ› Running automated code review, may be slow
Sean McCullough485afc62025-04-28 14:28:39 -070055{{else if eq .msg.ToolName "multiplechoice" -}}
56 ๐Ÿ“ {{.input.question}}
57{{ range .input.responseOptions -}}
58 - {{ .caption}}: {{.responseText}}
59{{end -}}
Josh Bleecher Snyder2d081192025-05-29 13:46:04 +000060{{else if eq .msg.ToolName "browser_navigate" -}}
61 ๐ŸŒ {{.input.url -}}
62{{else if eq .msg.ToolName "browser_click" -}}
63 ๐Ÿ–ฑ๏ธ {{.input.selector -}}
64{{else if eq .msg.ToolName "browser_type" -}}
65 โŒจ๏ธ {{.input.selector}}: "{{.input.text}}"
66{{else if eq .msg.ToolName "browser_wait_for" -}}
67 โณ {{.input.selector -}}
68{{else if eq .msg.ToolName "browser_get_text" -}}
69 ๐Ÿ“– {{.input.selector -}}
70{{else if eq .msg.ToolName "browser_eval" -}}
71 ๐Ÿ“ฑ {{.input.expression -}}
72{{else if eq .msg.ToolName "browser_take_screenshot" -}}
73 ๐Ÿ“ธ Screenshot
74{{else if eq .msg.ToolName "browser_scroll_into_view" -}}
75 ๐Ÿ”„ {{.input.selector -}}
76{{else if eq .msg.ToolName "browser_resize" -}}
77 ๐Ÿ–ผ๏ธ {{.input.width}}x{{.input.height -}}
78{{else if eq .msg.ToolName "browser_read_image" -}}
79 ๐Ÿ–ผ๏ธ {{.input.path -}}
80{{else if eq .msg.ToolName "browser_recent_console_logs" -}}
81 ๐Ÿ“œ Console logs
82{{else if eq .msg.ToolName "browser_clear_console_logs" -}}
83 ๐Ÿงน Clear console logs
Philip Zeyligerc17ffe32025-06-05 19:49:13 -070084{{else if eq .msg.ToolName "list_recent_sketch_sessions" -}}
85 ๐Ÿ“š List recent sketch sessions
86{{else if eq .msg.ToolName "read_sketch_session" -}}
87 ๐Ÿ“– Read session {{.input.session_id}}
Earl Lee2e463fb2025-04-17 11:22:22 -070088{{else -}}
Josh Bleecher Snyder47b19362025-04-30 01:34:14 +000089 ๐Ÿ› ๏ธ {{ .msg.ToolName}}: {{.msg.ToolInput -}}
Earl Lee2e463fb2025-04-17 11:22:22 -070090{{end -}}
91`
92 toolUseTmpl = template.Must(template.New("tool_use").Parse(toolUseTemplTxt))
93)
94
David Crawshaw93fec602025-05-05 08:40:06 -070095type TermUI struct {
Earl Lee2e463fb2025-04-17 11:22:22 -070096 stdin *os.File
97 stdout *os.File
98 stderr *os.File
99
100 agent loop.CodingAgent
101 httpURL string
102
103 trm *term.Terminal
104
105 // the chatMsgCh channel is for "conversation" messages, like responses to user input
106 // from the LLM, or output from executing slash-commands issued by the user.
107 chatMsgCh chan chatMessage
108
109 // the log channel is for secondary messages, like logging, errors, and debug information
110 // from local and remove subproceses.
111 termLogCh chan string
112
113 // protects following
114 mu sync.Mutex
115 oldState *term.State
116 // Tracks branches that were pushed during the session
117 pushedBranches map[string]struct{}
Josh Bleecher Snyderb1e81572025-05-01 00:53:27 +0000118
119 // Pending message count, for graceful shutdown
120 messageWaitGroup sync.WaitGroup
Earl Lee2e463fb2025-04-17 11:22:22 -0700121}
122
123type chatMessage struct {
124 idx int
125 sender string
126 content string
127 thinking bool
128}
129
David Crawshaw93fec602025-05-05 08:40:06 -0700130func New(agent loop.CodingAgent, httpURL string) *TermUI {
131 return &TermUI{
Earl Lee2e463fb2025-04-17 11:22:22 -0700132 agent: agent,
133 stdin: os.Stdin,
134 stdout: os.Stdout,
135 stderr: os.Stderr,
136 httpURL: httpURL,
137 chatMsgCh: make(chan chatMessage, 1),
138 termLogCh: make(chan string, 1),
139 pushedBranches: make(map[string]struct{}),
140 }
141}
142
David Crawshaw93fec602025-05-05 08:40:06 -0700143func (ui *TermUI) Run(ctx context.Context) error {
Earl Lee2e463fb2025-04-17 11:22:22 -0700144 fmt.Println(`๐ŸŒ ` + ui.httpURL + `/`)
Earl Lee2e463fb2025-04-17 11:22:22 -0700145 fmt.Println(`๐Ÿ’ฌ type 'help' for help`)
146 fmt.Println()
147
148 // Start up the main terminal UI:
149 if err := ui.initializeTerminalUI(ctx); err != nil {
150 return err
151 }
152 go ui.receiveMessagesLoop(ctx)
153 if err := ui.inputLoop(ctx); err != nil {
154 return err
155 }
156 return nil
157}
158
David Crawshaw93fec602025-05-05 08:40:06 -0700159func (ui *TermUI) LogToolUse(resp *loop.AgentMessage) {
Earl Lee2e463fb2025-04-17 11:22:22 -0700160 inputData := map[string]any{}
161 if err := json.Unmarshal([]byte(resp.ToolInput), &inputData); err != nil {
162 ui.AppendSystemMessage("error: %v", err)
163 return
164 }
165 buf := bytes.Buffer{}
Philip Zeyligerbe7802a2025-06-04 20:15:25 +0000166 if err := toolUseTmpl.Execute(&buf, map[string]any{"msg": resp, "input": inputData, "output": resp.ToolResult, "branch_prefix": ui.agent.BranchPrefix()}); err != nil {
Earl Lee2e463fb2025-04-17 11:22:22 -0700167 ui.AppendSystemMessage("error: %v", err)
168 return
169 }
170 ui.AppendSystemMessage("%s\n", buf.String())
171}
172
David Crawshaw93fec602025-05-05 08:40:06 -0700173func (ui *TermUI) receiveMessagesLoop(ctx context.Context) {
Philip Zeyligerb7c58752025-05-01 10:10:17 -0700174 it := ui.agent.NewIterator(ctx, 0)
Earl Lee2e463fb2025-04-17 11:22:22 -0700175 bold := color.New(color.Bold).SprintFunc()
176 for {
177 select {
178 case <-ctx.Done():
179 return
180 default:
181 }
Philip Zeyligerb7c58752025-05-01 10:10:17 -0700182 resp := it.Next()
183 if resp == nil {
184 return
185 }
Josh Bleecher Snyder4d544932025-05-07 13:33:53 +0000186 if resp.HideOutput {
187 continue
188 }
Earl Lee2e463fb2025-04-17 11:22:22 -0700189 // Typically a user message will start the thinking and a (top-level
190 // conversation) end of turn will stop it.
191 thinking := !(resp.EndOfTurn && resp.ParentConversationID == nil)
192
193 switch resp.Type {
194 case loop.AgentMessageType:
Josh Bleecher Snyder2978ab22025-04-30 10:29:32 -0700195 ui.AppendChatMessage(chatMessage{thinking: thinking, idx: resp.Idx, sender: "๐Ÿ•ด๏ธ ", content: resp.Content})
Earl Lee2e463fb2025-04-17 11:22:22 -0700196 case loop.ToolUseMessageType:
197 ui.LogToolUse(resp)
198 case loop.ErrorMessageType:
199 ui.AppendSystemMessage("โŒ %s", resp.Content)
200 case loop.BudgetMessageType:
201 ui.AppendSystemMessage("๐Ÿ’ฐ %s", resp.Content)
202 case loop.AutoMessageType:
203 ui.AppendSystemMessage("๐Ÿง %s", resp.Content)
204 case loop.UserMessageType:
Josh Bleecher Snyderc2d26102025-04-30 06:19:43 -0700205 ui.AppendChatMessage(chatMessage{thinking: thinking, idx: resp.Idx, sender: "๐Ÿฆธ", content: resp.Content})
Earl Lee2e463fb2025-04-17 11:22:22 -0700206 case loop.CommitMessageType:
207 // Display each commit in the terminal
208 for _, commit := range resp.Commits {
209 if commit.PushedBranch != "" {
Sean McCullough43664f62025-04-20 16:13:03 -0700210 ui.AppendSystemMessage("๐Ÿ”„ new commit: [%s] %s\npushed to: %s", commit.Hash[:8], commit.Subject, bold(commit.PushedBranch))
Earl Lee2e463fb2025-04-17 11:22:22 -0700211
212 // Track the pushed branch in our map
213 ui.mu.Lock()
214 ui.pushedBranches[commit.PushedBranch] = struct{}{}
215 ui.mu.Unlock()
216 } else {
217 ui.AppendSystemMessage("๐Ÿ”„ new commit: [%s] %s", commit.Hash[:8], commit.Subject)
218 }
219 }
220 default:
221 ui.AppendSystemMessage("โŒ Unexpected Message Type %s %v", resp.Type, resp)
222 }
223 }
224}
225
David Crawshaw93fec602025-05-05 08:40:06 -0700226func (ui *TermUI) inputLoop(ctx context.Context) error {
Earl Lee2e463fb2025-04-17 11:22:22 -0700227 for {
228 line, err := ui.trm.ReadLine()
229 if errors.Is(err, io.EOF) {
230 ui.AppendSystemMessage("\n")
231 line = "exit"
232 } else if err != nil {
233 return err
234 }
235
236 line = strings.TrimSpace(line)
237
238 switch line {
239 case "?", "help":
Josh Bleecher Snyder85068942025-04-30 10:51:27 -0700240 ui.AppendSystemMessage(`General use:
241Use chat to ask sketch to tackle a task or answer a question about this repo.
242
243Special commands:
244- help, ? : Show this help message
245- budget : Show original budget
246- usage, cost : Show current token usage and cost
Josh Bleecher Snyder3e2111b2025-04-30 17:53:28 +0000247- browser, open, b : Open current conversation in browser
Earl Lee2e463fb2025-04-17 11:22:22 -0700248- stop, cancel, abort : Cancel the current operation
Josh Bleecher Snyder85068942025-04-30 10:51:27 -0700249- exit, quit, q : Exit sketch
250- ! <command> : Execute a shell command (e.g. !ls -la)`)
Earl Lee2e463fb2025-04-17 11:22:22 -0700251 case "budget":
252 originalBudget := ui.agent.OriginalBudget()
253 ui.AppendSystemMessage("๐Ÿ’ฐ Budget summary:")
Philip Zeyligere6c294d2025-06-04 16:55:21 +0000254
Earl Lee2e463fb2025-04-17 11:22:22 -0700255 ui.AppendSystemMessage("- Max total cost: %0.2f", originalBudget.MaxDollars)
Josh Bleecher Snyder3e2111b2025-04-30 17:53:28 +0000256 case "browser", "open", "b":
257 if ui.httpURL != "" {
258 ui.AppendSystemMessage("๐ŸŒ Opening %s in browser", ui.httpURL)
259 go ui.agent.OpenBrowser(ui.httpURL)
260 } else {
261 ui.AppendSystemMessage("โŒ No web URL available for this session")
262 }
Earl Lee2e463fb2025-04-17 11:22:22 -0700263 case "usage", "cost":
264 totalUsage := ui.agent.TotalUsage()
265 ui.AppendSystemMessage("๐Ÿ’ฐ Current usage summary:")
Josh Bleecher Snydera0801ad2025-04-25 19:34:53 +0000266 ui.AppendSystemMessage("- Input tokens: %s", humanize.Comma(int64(totalUsage.TotalInputTokens())))
267 ui.AppendSystemMessage("- Output tokens: %s", humanize.Comma(int64(totalUsage.OutputTokens)))
Earl Lee2e463fb2025-04-17 11:22:22 -0700268 ui.AppendSystemMessage("- Responses: %d", totalUsage.Responses)
269 ui.AppendSystemMessage("- Wall time: %s", totalUsage.WallTime().Round(time.Second))
270 ui.AppendSystemMessage("- Total cost: $%0.2f", totalUsage.TotalCostUSD)
271 case "bye", "exit", "q", "quit":
272 ui.trm.SetPrompt("")
273 // Display final usage stats
274 totalUsage := ui.agent.TotalUsage()
275 ui.AppendSystemMessage("๐Ÿ’ฐ Final usage summary:")
Josh Bleecher Snydera0801ad2025-04-25 19:34:53 +0000276 ui.AppendSystemMessage("- Input tokens: %s", humanize.Comma(int64(totalUsage.TotalInputTokens())))
277 ui.AppendSystemMessage("- Output tokens: %s", humanize.Comma(int64(totalUsage.OutputTokens)))
Earl Lee2e463fb2025-04-17 11:22:22 -0700278 ui.AppendSystemMessage("- Responses: %d", totalUsage.Responses)
279 ui.AppendSystemMessage("- Wall time: %s", totalUsage.WallTime().Round(time.Second))
280 ui.AppendSystemMessage("- Total cost: $%0.2f", totalUsage.TotalCostUSD)
281
282 // Display pushed branches
283 ui.mu.Lock()
284 if len(ui.pushedBranches) > 0 {
285 // Convert map keys to a slice for display
286 branches := make([]string, 0, len(ui.pushedBranches))
287 for branch := range ui.pushedBranches {
288 branches = append(branches, branch)
289 }
290
Philip Zeyliger49edc922025-05-14 09:45:45 -0700291 initialCommitRef := getShortSHA(ui.agent.SketchGitBase())
Earl Lee2e463fb2025-04-17 11:22:22 -0700292 if len(branches) == 1 {
293 ui.AppendSystemMessage("\n๐Ÿ”„ Branch pushed during session: %s", branches[0])
Josh Bleecher Snyder956626d2025-05-15 21:24:07 +0000294 ui.AppendSystemMessage("๐Ÿ’ Cherry-pick those changes: git cherry-pick %s..%s", initialCommitRef, branches[0])
295 ui.AppendSystemMessage("๐Ÿ”€ Merge those changes: git merge %s", branches[0])
296 ui.AppendSystemMessage("๐Ÿ—‘๏ธ Delete the branch: git branch -D %s", branches[0])
Earl Lee2e463fb2025-04-17 11:22:22 -0700297 } else {
298 ui.AppendSystemMessage("\n๐Ÿ”„ Branches pushed during session:")
299 for _, branch := range branches {
300 ui.AppendSystemMessage("- %s", branch)
301 }
302 ui.AppendSystemMessage("\n๐Ÿ’ To add all those changes to your branch:")
303 for _, branch := range branches {
Josh Bleecher Snyder0137a7f2025-04-30 01:16:35 +0000304 ui.AppendSystemMessage("git cherry-pick %s..%s", initialCommitRef, branch)
Earl Lee2e463fb2025-04-17 11:22:22 -0700305 }
Philip Zeyliger49edc922025-05-14 09:45:45 -0700306 ui.AppendSystemMessage("\n๐Ÿ”€ or:")
307 for _, branch := range branches {
308 ui.AppendSystemMessage("git merge %s", branch)
309 }
Josh Bleecher Snyder956626d2025-05-15 21:24:07 +0000310
311 ui.AppendSystemMessage("\n๐Ÿ—‘๏ธ To delete branches:")
312 for _, branch := range branches {
313 ui.AppendSystemMessage("git branch -D %s", branch)
314 }
Earl Lee2e463fb2025-04-17 11:22:22 -0700315 }
316 }
317 ui.mu.Unlock()
318
319 ui.AppendSystemMessage("\n๐Ÿ‘‹ Goodbye!")
Josh Bleecher Snyderb1e81572025-05-01 00:53:27 +0000320 // Wait for all pending messages to be processed before exiting
321 ui.messageWaitGroup.Wait()
Earl Lee2e463fb2025-04-17 11:22:22 -0700322 return nil
323 case "stop", "cancel", "abort":
Sean McCulloughedc88dc2025-04-30 02:55:01 +0000324 ui.agent.CancelTurn(fmt.Errorf("user canceled the operation"))
Earl Lee2e463fb2025-04-17 11:22:22 -0700325 case "panic":
326 panic("user forced a panic")
327 default:
328 if line == "" {
329 continue
330 }
331 if strings.HasPrefix(line, "!") {
332 // Execute as shell command
333 line = line[1:] // remove the '!' prefix
334 sendToLLM := strings.HasPrefix(line, "!")
335 if sendToLLM {
336 line = line[1:] // remove the second '!'
337 }
338
339 // Create a cmd and run it
340 // TODO: ui.trm contains a mutex inside its write call.
341 // It is potentially safe to attach ui.trm directly to this
342 // cmd object's Stdout/Stderr and stream the output.
343 // That would make a big difference for, e.g. wget.
344 cmd := exec.Command("bash", "-c", line)
345 out, err := cmd.CombinedOutput()
346 ui.AppendSystemMessage("%s", out)
347 if err != nil {
348 ui.AppendSystemMessage("โŒ Command error: %v", err)
349 }
350 if sendToLLM {
351 // Send the command and its output to the agent
352 message := fmt.Sprintf("I ran the command: `%s`\nOutput:\n```\n%s```", line, out)
353 if err != nil {
354 message += fmt.Sprintf("\n\nError: %v", err)
355 }
356 ui.agent.UserMessage(ctx, message)
357 }
358 continue
359 }
360
361 // Send it to the LLM
362 // chatMsg := chatMessage{sender: "you", content: line}
363 // ui.sendChatMessage(chatMsg)
364 ui.agent.UserMessage(ctx, line)
365 }
366 }
367}
368
David Crawshaw93fec602025-05-05 08:40:06 -0700369func (ui *TermUI) updatePrompt(thinking bool) {
Earl Lee2e463fb2025-04-17 11:22:22 -0700370 var t string
Earl Lee2e463fb2025-04-17 11:22:22 -0700371 if thinking {
372 // Emoji don't seem to work here? Messes up my terminal.
373 t = "*"
374 }
Josh Bleecher Snyder03376232025-06-05 14:29:48 -0700375 var money string
376 if totalCost := ui.agent.TotalUsage().TotalCostUSD; totalCost > 0 {
377 money = fmt.Sprintf("($%0.2f/%0.2f)", totalCost, ui.agent.OriginalBudget().MaxDollars)
378 }
379 p := fmt.Sprintf("%s %s%s> ", ui.httpURL, money, t)
Earl Lee2e463fb2025-04-17 11:22:22 -0700380 ui.trm.SetPrompt(p)
381}
382
David Crawshaw93fec602025-05-05 08:40:06 -0700383func (ui *TermUI) initializeTerminalUI(ctx context.Context) error {
Earl Lee2e463fb2025-04-17 11:22:22 -0700384 ui.mu.Lock()
385 defer ui.mu.Unlock()
386
387 if !term.IsTerminal(int(ui.stdin.Fd())) {
Philip Zeyligerc5b8ed42025-05-05 20:28:34 +0000388 return fmt.Errorf("this command requires terminal I/O when termui=true")
Earl Lee2e463fb2025-04-17 11:22:22 -0700389 }
390
391 oldState, err := term.MakeRaw(int(ui.stdin.Fd()))
392 if err != nil {
393 return err
394 }
395 ui.oldState = oldState
396 ui.trm = term.NewTerminal(ui.stdin, "")
397 width, height, err := term.GetSize(int(ui.stdin.Fd()))
398 if err != nil {
399 return fmt.Errorf("Error getting terminal size: %v\n", err)
400 }
401 ui.trm.SetSize(width, height)
402 // Handle terminal resizes...
403 sig := make(chan os.Signal, 1)
404 signal.Notify(sig, syscall.SIGWINCH)
405 go func() {
406 for {
407 <-sig
408 newWidth, newHeight, err := term.GetSize(int(ui.stdin.Fd()))
409 if err != nil {
410 continue
411 }
412 if newWidth != width || newHeight != height {
413 width, height = newWidth, newHeight
414 ui.trm.SetSize(width, height)
415 }
416 }
417 }()
418
419 ui.updatePrompt(false)
420
421 // This is the only place where we should call fe.trm.Write:
422 go func() {
Sean McCullougha4b19f82025-05-05 10:22:59 -0700423 var lastMsg *chatMessage
Earl Lee2e463fb2025-04-17 11:22:22 -0700424 for {
425 select {
426 case <-ctx.Done():
427 return
428 case msg := <-ui.chatMsgCh:
Josh Bleecher Snyderb1e81572025-05-01 00:53:27 +0000429 func() {
430 defer ui.messageWaitGroup.Done()
Sean McCullougha4b19f82025-05-05 10:22:59 -0700431 // Update prompt before writing, because otherwise it doesn't redraw the prompt.
432 ui.updatePrompt(msg.thinking)
433 lastMsg = &msg
Josh Bleecher Snyderb1e81572025-05-01 00:53:27 +0000434 // Sometimes claude doesn't say anything when it runs tools.
435 // No need to output anything in that case.
436 if strings.TrimSpace(msg.content) == "" {
437 return
438 }
439 s := fmt.Sprintf("%s %s\n", msg.sender, msg.content)
Josh Bleecher Snyderb1e81572025-05-01 00:53:27 +0000440 ui.trm.Write([]byte(s))
441 }()
Earl Lee2e463fb2025-04-17 11:22:22 -0700442 case logLine := <-ui.termLogCh:
Josh Bleecher Snyderb1e81572025-05-01 00:53:27 +0000443 func() {
444 defer ui.messageWaitGroup.Done()
Sean McCullougha4b19f82025-05-05 10:22:59 -0700445 if lastMsg != nil {
446 ui.updatePrompt(lastMsg.thinking)
447 } else {
448 ui.updatePrompt(false)
449 }
Josh Bleecher Snyderb1e81572025-05-01 00:53:27 +0000450 b := []byte(logLine + "\n")
451 ui.trm.Write(b)
452 }()
Earl Lee2e463fb2025-04-17 11:22:22 -0700453 }
454 }
455 }()
456
457 return nil
458}
459
David Crawshaw93fec602025-05-05 08:40:06 -0700460func (ui *TermUI) RestoreOldState() error {
Earl Lee2e463fb2025-04-17 11:22:22 -0700461 ui.mu.Lock()
462 defer ui.mu.Unlock()
463 return term.Restore(int(ui.stdin.Fd()), ui.oldState)
464}
465
466// AppendChatMessage is for showing responses the user's request, conversational dialog etc
David Crawshaw93fec602025-05-05 08:40:06 -0700467func (ui *TermUI) AppendChatMessage(msg chatMessage) {
Josh Bleecher Snyderb1e81572025-05-01 00:53:27 +0000468 ui.messageWaitGroup.Add(1)
Earl Lee2e463fb2025-04-17 11:22:22 -0700469 ui.chatMsgCh <- msg
470}
471
472// AppendSystemMessage is for debug information, errors and such that are not part of the "conversation" per se,
473// but still need to be shown to the user.
David Crawshaw93fec602025-05-05 08:40:06 -0700474func (ui *TermUI) AppendSystemMessage(fmtString string, args ...any) {
Josh Bleecher Snyderb1e81572025-05-01 00:53:27 +0000475 ui.messageWaitGroup.Add(1)
Earl Lee2e463fb2025-04-17 11:22:22 -0700476 ui.termLogCh <- fmt.Sprintf(fmtString, args...)
477}
Josh Bleecher Snyder0137a7f2025-04-30 01:16:35 +0000478
Josh Bleecher Snyder8fdf7532025-05-06 00:56:12 +0000479// getShortSHA returns the short SHA for the given git reference, falling back to the original SHA on error.
480func getShortSHA(sha string) string {
481 cmd := exec.Command("git", "rev-parse", "--short", sha)
Josh Bleecher Snyder0137a7f2025-04-30 01:16:35 +0000482 shortSha, err := cmd.Output()
483 if err == nil {
484 shortStr := strings.TrimSpace(string(shortSha))
485 if shortStr != "" {
486 return shortStr
487 }
488 }
Josh Bleecher Snyder0137a7f2025-04-30 01:16:35 +0000489 return sha
490}