feat: add terminal spinner for CLI agent mode
Fixes #576: shows a rotating Braille dot animation with 'Thinking...' on stderr while waiting for LLM responses. Spinner runs in all three agent modes (single message, interactive, simple interactive).
This commit is contained in:
parent
08bb45aeb6
commit
67bbf6f7f4
2 changed files with 73 additions and 0 deletions
|
|
@ -65,6 +65,7 @@ func agentCmd() {
|
|||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading config: %v\n", err)
|
||||
fmt.Println("Run 'picoclaw doctor' to check for common problems.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
|
|
@ -75,6 +76,7 @@ func agentCmd() {
|
|||
provider, modelID, err := providers.CreateProvider(cfg)
|
||||
if err != nil {
|
||||
fmt.Printf("Error creating provider: %v\n", err)
|
||||
fmt.Println("Run 'picoclaw doctor' to check for common problems.")
|
||||
os.Exit(1)
|
||||
}
|
||||
// Use the resolved model ID from provider creation
|
||||
|
|
@ -105,7 +107,10 @@ func agentCmd() {
|
|||
|
||||
if message != "" {
|
||||
ctx := context.Background()
|
||||
spin := newSpinner("Thinking...")
|
||||
spin.Start()
|
||||
response, err := agentLoop.ProcessDirect(ctx, message, sessionKey)
|
||||
spin.Stop()
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
|
|
@ -157,7 +162,10 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
|||
}
|
||||
|
||||
ctx := context.Background()
|
||||
spin := newSpinner("Thinking...")
|
||||
spin.Start()
|
||||
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
||||
spin.Stop()
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
continue
|
||||
|
|
@ -192,7 +200,10 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
|||
}
|
||||
|
||||
ctx := context.Background()
|
||||
spin := newSpinner("Thinking...")
|
||||
spin.Start()
|
||||
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
||||
spin.Stop()
|
||||
if err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
continue
|
||||
|
|
|
|||
62
cmd/picoclaw/spinner.go
Normal file
62
cmd/picoclaw/spinner.go
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// PicoClaw - Ultra-lightweight personal AI agent
|
||||
// License: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// spinner displays a rotating progress indicator on stderr while the agent
|
||||
// processes a request. Output goes to stderr so that piped stdout is not
|
||||
// affected.
|
||||
type spinner struct {
|
||||
frames []rune
|
||||
text string
|
||||
stop chan struct{}
|
||||
stopped chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func newSpinner(text string) *spinner {
|
||||
return &spinner{
|
||||
frames: []rune{'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'},
|
||||
text: text,
|
||||
stop: make(chan struct{}),
|
||||
stopped: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start launches the spinner animation in a background goroutine.
|
||||
func (s *spinner) Start() {
|
||||
go func() {
|
||||
defer close(s.stopped)
|
||||
i := 0
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.stop:
|
||||
// Clear the spinner line
|
||||
fmt.Fprintf(os.Stderr, "\r\033[K")
|
||||
return
|
||||
case <-ticker.C:
|
||||
frame := s.frames[i%len(s.frames)]
|
||||
fmt.Fprintf(os.Stderr, "\r%c %s", frame, s.text)
|
||||
i++
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Stop signals the spinner to halt and waits for it to finish.
|
||||
func (s *spinner) Stop() {
|
||||
s.once.Do(func() {
|
||||
close(s.stop)
|
||||
<-s.stopped
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue