diff --git a/cmd/godex/main.go b/cmd/godex/main.go index 142ca59..7bc3f29 100644 --- a/cmd/godex/main.go +++ b/cmd/godex/main.go @@ -325,6 +325,9 @@ func main() { if err != nil { log.Printf("[Scheduler] Failed to initialize: %v", err) } else { + sched.SetOnTaskFinished(func(*scheduler.ScheduledTask) { + playSound() + }) sched.SetProviderGetter(&schedulerProviderGetter{provider: provider}) var serverSlice []interface{} = make([]interface{}, len(servers)) for i, s := range servers { @@ -1007,9 +1010,10 @@ promptLoop: if totalToolCalls > 0 && isToolCallResponse { prevNoTool = false } + if !isToolCallResponse || len(toolCalls) == 0 { - if !looksLikeToolCall(resp) { + if !looksLikeToolCall(resp) && round > 0 { go playSound() fmt.Printf("\n\n%s\n", renderMarkdown(resp)) break @@ -2245,7 +2249,7 @@ func runToolLoop(ctx context.Context, provider *config.Provider, servers []MCPSe if !isToolCallResponse || len(toolCalls) == 0 { if !hasFinal { - if !looksLikeToolCall(resp) { + if !looksLikeToolCall(resp) && round > 0 { if nocont { fmt.Printf("\n\n%s\n", renderMarkdown(resp)) } diff --git a/cmd/godex/scheduler.go b/cmd/godex/scheduler.go index deb6c10..916b799 100644 --- a/cmd/godex/scheduler.go +++ b/cmd/godex/scheduler.go @@ -45,7 +45,10 @@ func handleSchedule(input string) { fmt.Println(" /schedule - List all scheduled tasks") fmt.Println(" /schedule - Show last output of a task") fmt.Println(" /schedule stop - Stop a running task by 4-char ID") + fmt.Println(" /schedule stop --all - Stop all running tasks") fmt.Println(" /schedule remove - Remove a task from the schedule by 4-char ID") + fmt.Println(" /schedule remove --all - Remove all tasks from the schedule") + fmt.Println(" /schedule clear - Stop and remove all tasks") fmt.Println(" /schedule help - Show this help message") fmt.Println("") fmt.Println("LLM Tool (scheduler):") @@ -70,7 +73,33 @@ func handleSchedule(input string) { return } + if len(parts) == 2 && (parts[1] == "clear" || parts[1] == "clear-all") { + tasks := sched.ListTasks() + if len(tasks) == 0 { + fmt.Println("No scheduled tasks") + return + } + sched.StopAllTasks() + removed := 0 + for _, t := range tasks { + task, ok := t.(*scheduler.ScheduledTask) + if !ok || task == nil { + continue + } + if sched.RemoveTask(task.ID) { + removed++ + } + } + fmt.Printf("Cleared %d task(s)\n", removed) + return + } + if len(parts) >= 3 && parts[1] == "stop" { + if parts[2] == "all" || parts[2] == "--all" || parts[2] == "*" { + sched.StopAllTasks() + fmt.Println("Stopped all running tasks") + return + } id := strings.ToUpper(parts[2]) if len(id) != 4 { fmt.Println("Invalid task ID. Use 4-character identifier (e.g., ABC1)") @@ -85,6 +114,25 @@ func handleSchedule(input string) { } if len(parts) >= 3 && parts[1] == "remove" { + if parts[2] == "all" || parts[2] == "--all" || parts[2] == "*" { + tasks := sched.ListTasks() + if len(tasks) == 0 { + fmt.Println("No scheduled tasks") + return + } + removed := 0 + for _, t := range tasks { + task, ok := t.(*scheduler.ScheduledTask) + if !ok || task == nil { + continue + } + if sched.RemoveTask(task.ID) { + removed++ + } + } + fmt.Printf("Removed %d task(s)\n", removed) + return + } id := strings.ToUpper(parts[2]) if len(id) != 4 { fmt.Println("Invalid task ID. Use 4-character identifier (e.g., ABC1)") @@ -123,6 +171,7 @@ func handleSchedule(input string) { fmt.Println(" /schedule - Show last output of a task") fmt.Println(" /schedule stop - Stop a task by 4-char ID") fmt.Println(" /schedule remove - Remove a task by 4-char ID") + fmt.Println(" /schedule clear - Stop and remove all tasks") } func listScheduledTasks() { diff --git a/internal/scheduler/scheduler.go b/internal/scheduler/scheduler.go index 402cee0..9526cb3 100644 --- a/internal/scheduler/scheduler.go +++ b/internal/scheduler/scheduler.go @@ -46,6 +46,7 @@ type Scheduler struct { wg sync.WaitGroup provider ProviderGetter servers []ToolServer + onTaskFinished func(*ScheduledTask) maxRounds int toolTimeout int wd string @@ -251,6 +252,10 @@ func (s *Scheduler) SetServers(servers []interface{}) { } } +func (s *Scheduler) SetOnTaskFinished(cb func(*ScheduledTask)) { + s.onTaskFinished = cb +} + func (s *Scheduler) SetConfig(maxRounds, toolTimeout int, wd, os string) { if maxRounds > 0 { s.maxRounds = maxRounds @@ -474,6 +479,13 @@ func (s *Scheduler) executeTask(task *ScheduledTask) { task.LastRun = time.Now() task.RunCount++ s.updateTask(task) + + if s.onTaskFinished != nil { + func() { + defer func() { _ = recover() }() + s.onTaskFinished(task) + }() + } } func (s *Scheduler) executeWithTools(ctx context.Context, task *ScheduledTask) (string, error) { diff --git a/internal/scheduler/scheduler_callback_test.go b/internal/scheduler/scheduler_callback_test.go new file mode 100644 index 0000000..f7bb55a --- /dev/null +++ b/internal/scheduler/scheduler_callback_test.go @@ -0,0 +1,41 @@ +package scheduler + +import ( + "sync/atomic" + "testing" + "time" +) + +func TestScheduler_OnTaskFinishedCalled(t *testing.T) { + dbPath := t.TempDir() + "/scheduler.db" + s, err := New(dbPath) + if err != nil { + t.Fatalf("New: %v", err) + } + defer s.Close() + + var called int32 + s.SetOnTaskFinished(func(*ScheduledTask) { + atomic.AddInt32(&called, 1) + }) + + task := &ScheduledTask{ + ID: "TEST", + Prompt: "noop", + IntervalSec: 0, + RunAt: "", + IsRepeating: false, + WorkingDir: s.wd, + CreatedAt: time.Now(), + ProviderType: "current", + ProviderName: "current", + ProviderModel: "current", + } + + s.executeTask(task) + + if atomic.LoadInt32(&called) != 1 { + t.Fatalf("expected callback called once, got %d", atomic.LoadInt32(&called)) + } +} +