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()
|
cfg, err := loadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error loading config: %v\n", err)
|
fmt.Printf("Error loading config: %v\n", err)
|
||||||
|
fmt.Println("Run 'picoclaw doctor' to check for common problems.")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,6 +76,7 @@ func agentCmd() {
|
||||||
provider, modelID, err := providers.CreateProvider(cfg)
|
provider, modelID, err := providers.CreateProvider(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error creating provider: %v\n", err)
|
fmt.Printf("Error creating provider: %v\n", err)
|
||||||
|
fmt.Println("Run 'picoclaw doctor' to check for common problems.")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
// Use the resolved model ID from provider creation
|
// Use the resolved model ID from provider creation
|
||||||
|
|
@ -105,7 +107,10 @@ func agentCmd() {
|
||||||
|
|
||||||
if message != "" {
|
if message != "" {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
spin := newSpinner("Thinking...")
|
||||||
|
spin.Start()
|
||||||
response, err := agentLoop.ProcessDirect(ctx, message, sessionKey)
|
response, err := agentLoop.ProcessDirect(ctx, message, sessionKey)
|
||||||
|
spin.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error: %v\n", err)
|
fmt.Printf("Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
@ -157,7 +162,10 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
spin := newSpinner("Thinking...")
|
||||||
|
spin.Start()
|
||||||
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
||||||
|
spin.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error: %v\n", err)
|
fmt.Printf("Error: %v\n", err)
|
||||||
continue
|
continue
|
||||||
|
|
@ -192,7 +200,10 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
spin := newSpinner("Thinking...")
|
||||||
|
spin.Start()
|
||||||
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
response, err := agentLoop.ProcessDirect(ctx, input, sessionKey)
|
||||||
|
spin.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error: %v\n", err)
|
fmt.Printf("Error: %v\n", err)
|
||||||
continue
|
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