diff --git a/VERSION.md b/VERSION.md index 409cdff..1008d74 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1,4 +1,35 @@ -# Version actuelle : 0.3.1 +# Version actuelle : 0.3.2 + +## [0.3.2] - 2026-04-15 +**Type:** Patch — Double-Enter pour soumettre les prompts multi-lignes + +### Corrigé +- **Les tâches dispatchées restaient coincées dans le buffer d'entrée Claude**. + Le message s'affichait comme `[Pasted text #N +M lines]` et Claude attendait + indéfiniment un Enter explicite. Constaté sur `ccl-auto-11` (tâche secumon) + et `ccl-auto-12` (tâche secuops) : le prompt était bien envoyé mais jamais + soumis, l'agent restait au prompt vide. +- Cause : `SendKeys` envoie `tmux send-keys -t Enter` mais quand + `text` contient des `\n`, Claude Code détecte un paste et **absorbe le Enter + final** comme nouvelle ligne du paste. Aucun submit. + +### Ajouté +- `tmux.Client.SendEnter(session)` : envoie un Enter isolé. + Implémentation `ExecClient.SendEnter` = `tmux send-keys -t Enter`. +- Dispatcher : après `SendKeys(msg)`, `time.Sleep(500ms)` puis `SendEnter()` + pour soumettre le paste. +- Mocks mis à jour dans 5 fichiers de test (quota, dispatcher, switcher, + lifecycle, watcher). + +### Tests effectués +- ✅ `go test ./...` full suite (incluant dispatcher) +- ✅ Sessions ccl-auto-11 et ccl-auto-12 débloquées manuellement après Enter, + travail en cours depuis + +### Fichiers modifiés +- `internal/tmux/client.go` — interface + ExecClient.SendEnter +- `internal/dispatcher/dispatcher.go` — submit après paste +- 5 fichiers `*_test.go` — mocks étendus ## [0.3.1] - 2026-04-15 **Type:** Patch — `start_index` pour faire coexister pool manuel et pool auto diff --git a/internal/dispatcher/dispatcher.go b/internal/dispatcher/dispatcher.go index f2f9f03..e7ac58d 100644 --- a/internal/dispatcher/dispatcher.go +++ b/internal/dispatcher/dispatcher.go @@ -234,11 +234,19 @@ func (d *Dispatcher) launchAgent(session, projectDir, taskFile string) error { return fmt.Errorf("claude not ready in %q after %v", session, promptTimeout) } - // Send the task message. + // Send the task message. Multi-line task bodies render in Claude's TUI + // as "[Pasted text #N +M lines]" — the trailing Enter that SendKeys + // appends is consumed as part of the paste, so the message would stay + // unsubmitted in the input buffer forever. Wait for the paste to + // register, then send a lone Enter to actually submit. msg := buildTaskMessage(body, taskFile) if err := d.tmux.SendKeys(session, msg); err != nil { return err } + time.Sleep(500 * time.Millisecond) + if err := d.tmux.SendEnter(session); err != nil { + return err + } d.state.SetWorking(session, filepath.Base(taskFile)) d.logger.Printf("[dispatcher] DISPATCHED session=%q task=%s model=%s", diff --git a/internal/dispatcher/dispatcher_test.go b/internal/dispatcher/dispatcher_test.go index d790feb..f27d47e 100644 --- a/internal/dispatcher/dispatcher_test.go +++ b/internal/dispatcher/dispatcher_test.go @@ -31,6 +31,10 @@ func (m *mockTmux) SendKeys(_, keys string) error { m.sentKeys = append(m.sentKeys, keys) return nil } +func (m *mockTmux) SendEnter(_ string) error { + m.sentKeys = append(m.sentKeys, "") + return nil +} func (m *mockTmux) CapturePaneTail(session string, _ int) (string, error) { return m.paneOutput[session], nil } diff --git a/internal/lifecycle/manager_test.go b/internal/lifecycle/manager_test.go index e773310..6c51e87 100644 --- a/internal/lifecycle/manager_test.go +++ b/internal/lifecycle/manager_test.go @@ -40,6 +40,11 @@ func (m *mockTmux) SendKeys(session, keys string) error { return nil } +func (m *mockTmux) SendEnter(session string) error { + m.sendKeysCalls = append(m.sendKeysCalls, session) + return nil +} + func (m *mockTmux) CapturePaneTail(session string, lines int) (string, error) { return "", nil } diff --git a/internal/quota/monitor_test.go b/internal/quota/monitor_test.go index 800b8ee..524b3e7 100644 --- a/internal/quota/monitor_test.go +++ b/internal/quota/monitor_test.go @@ -24,6 +24,7 @@ func (m *mockTmux) HasSession(name string) bool { return m.sessions[name] func (m *mockTmux) CreateSession(name, _ string) error { m.sessions[name] = true; return nil } func (m *mockTmux) KillSession(_ string) error { return nil } func (m *mockTmux) SendKeys(_, _ string) error { return nil } +func (m *mockTmux) SendEnter(_ string) error { return nil } func (m *mockTmux) CapturePaneTail(session string, _ int) (string, error) { return m.paneOutput[session], nil } diff --git a/internal/switcher/account_switcher_test.go b/internal/switcher/account_switcher_test.go index 24aeab5..8c3b292 100644 --- a/internal/switcher/account_switcher_test.go +++ b/internal/switcher/account_switcher_test.go @@ -41,6 +41,10 @@ func (m *mockTmux) SendKeys(session, keys string) error { m.sendKeyCalls = append(m.sendKeyCalls, session+":"+keys) return nil } +func (m *mockTmux) SendEnter(session string) error { + m.sendKeyCalls = append(m.sendKeyCalls, session+":") + return nil +} func (m *mockTmux) CapturePaneTail(session string, _ int) (string, error) { return m.paneOutput[session], nil } diff --git a/internal/tmux/client.go b/internal/tmux/client.go index fc61b42..87c5cf7 100644 --- a/internal/tmux/client.go +++ b/internal/tmux/client.go @@ -16,8 +16,14 @@ type Client interface { CreateSession(name, workdir string) error // KillSession destroys the named session. KillSession(name string) error - // SendKeys sends key strokes to the first window of a session. + // SendKeys sends key strokes to the first window of a session, terminated + // by a single Enter. Note: when keys contains newlines, Claude Code's + // TUI treats the whole block as a paste and the trailing Enter is + // consumed as part of it — call SendEnter afterwards to submit. SendKeys(session, keys string) error + // SendEnter sends a lone Enter keypress. Used to submit a multi-line + // paste in Claude Code after SendKeys. + SendEnter(session string) error // CapturePaneTail returns the last n lines from the session's active pane. CapturePaneTail(session string, lines int) (string, error) } @@ -78,6 +84,15 @@ func (c *ExecClient) SendKeys(session, keys string) error { return nil } +// SendEnter sends a lone Enter keypress to the session's first pane. +func (c *ExecClient) SendEnter(session string) error { + out, err := run("send-keys", "-t", session, "Enter") + if err != nil { + return fmt.Errorf("tmux send-keys Enter %q: %w (%s)", session, err, out) + } + return nil +} + // CapturePaneTail captures the last n lines from the session's active pane. func (c *ExecClient) CapturePaneTail(session string, lines int) (string, error) { out, err := run("capture-pane", "-p", "-t", session, diff --git a/internal/watcher/session_watcher_test.go b/internal/watcher/session_watcher_test.go index bbba16a..937f2be 100644 --- a/internal/watcher/session_watcher_test.go +++ b/internal/watcher/session_watcher_test.go @@ -28,6 +28,7 @@ func (m *mockTmux) HasSession(name string) bool { return m.sessions func (m *mockTmux) CreateSession(name, _ string) error { m.sessions[name] = true; return nil } func (m *mockTmux) KillSession(_ string) error { return nil } func (m *mockTmux) SendKeys(_, keys string) error { m.sentKeys = append(m.sentKeys, keys); return nil } +func (m *mockTmux) SendEnter(_ string) error { m.sentKeys = append(m.sentKeys, ""); return nil } func (m *mockTmux) CapturePaneTail(session string, _ int) (string, error) { return m.paneOutput[session], nil }