--- phase: 04-notifications-et-i3bar plan: 01 type: execute wave: 1 depends_on: [] files_modified: [notify.go, notify_test.go, focus.go, focus_test.go, daemon.go, hook.go, protocol.go, main.go] autonomous: true requirements: [NOTIF-01, NOTIF-02] must_haves: truths: - "Une notification dunst apparait quand une session passe de Working a Needs Input" - "Pas de notification pour Idle -> Needs Input ou Working -> Idle" - "vmux focus 30 supprime les notifications pendant 30 minutes" - "Le focus expire automatiquement apres la duree" - "Le widget i3bar reste visible meme en mode focus" artifacts: - path: "notify.go" provides: "Notifier interface + ExecNotifier (notify-send)" exports: ["Notifier", "ExecNotifier", "NullNotifier"] - path: "focus.go" provides: "FocusTimer struct thread-safe" exports: ["FocusTimer"] - path: "notify_test.go" provides: "Tests notification transitions" - path: "focus_test.go" provides: "Tests FocusTimer Set/IsActive/Remaining/expiry" key_links: - from: "hook.go" to: "notify.go" via: "d.notifier.Notify() apres transition Working -> Needs Input" pattern: "notifier\\.Notify" - from: "hook.go" to: "focus.go" via: "d.focus.IsActive() pour bloquer les notifications" pattern: "focus\\.IsActive" - from: "main.go" to: "daemon.go" via: "vmux focus envoie action focus au daemon" pattern: "case \"focus\"" --- Notifications desktop et mode focus pour vmux. Purpose: Alerter l'utilisateur via dunst quand une session passe de Working a Needs Input, avec possibilite de supprimer temporairement les notifications via `vmux focus `. Output: notify.go, focus.go, integration dans daemon/hook, sous-commande `vmux focus`. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/04-notifications-et-i3bar/04-RESEARCH.md From daemon.go: ```go type Daemon struct { registry *SessionRegistry labels *LabelStore sockPath string procDir string claudeDir string workspaceResolver func(claudePID int) string i3commander I3Commander pollInterval time.Duration stopCh chan struct{} listener net.Listener hookPort int httpServer *http.Server lastHookTime time.Time mu sync.Mutex } func NewDaemon(sockPath, procDir, claudeDir string, labels *LabelStore) *Daemon func (d *Daemon) handleConnection(conn net.Conn) // switch req.Action ``` From hook.go: ```go func (d *Daemon) processHookEvent(event HookEvent) // maps hook -> registry update func (r *SessionRegistry) UpdateFromHook(sessionID, state, waitType, cwd string) // tracks PrevState ``` From protocol.go: ```go type Request struct { Action string `json:"action"` Args json.RawMessage `json:"args,omitempty"` } type Response struct { OK bool `json:"ok"` Error string `json:"error,omitempty"` Sessions []SessionInfo `json:"sessions,omitempty"` } type SessionInfo struct { PID, SessionID, Cwd, GitBranch, State, Preview, Workspace, Label, WaitType string WaitingSince *time.Time } ``` From daemon.go (TrackedSession): ```go type TrackedSession struct { Info SessionInfo PrevState string } ``` Task 1: Notifier interface, FocusTimer, et tests notify.go, notify_test.go, focus.go, focus_test.go - daemon.go (Daemon struct, pour comprendre ou injecter notifier et focus) - hook.go (processHookEvent, pour comprendre la transition d'etat) - protocol.go (SessionInfo, pour le shortName) - TestExecNotifier_CallsNotifySend: ExecNotifier.Notify() appelle notify-send avec --urgency=critical --app-name=vmux titre body - TestNullNotifier_DropsAll: NullNotifier.Notify() retourne nil sans side-effect - TestFocusTimer_Set: apres Set(30*time.Minute), IsActive() retourne true - TestFocusTimer_Expired: apres Set(0), IsActive() retourne false - TestFocusTimer_Remaining: apres Set(30min), Remaining() > 0 - TestFocusTimer_ZeroValue: FocusTimer{} non initialisee, IsActive() retourne false - TestShortName_Label: SessionInfo{Label:"auth"} -> "auth" - TestShortName_Cwd: SessionInfo{Cwd:"/home/pierre/Code/vibe/vmux"} -> "vmux" Creer 4 fichiers. TDD: ecrire les tests d'abord, puis l'implementation. **notify.go:** - Interface `Notifier` avec methode `Notify(title, body string) error` - `ExecNotifier` struct vide. Notify() utilise `exec.CommandContext` avec timeout 5s, appelle `notify-send --urgency=critical --app-name=vmux title body`. Per D-03: notify-send via os/exec. - `NullNotifier` struct vide. Notify() retourne nil. - Fonction `shortName(s SessionInfo) string` : retourne Label si non-vide, sinon `filepath.Base(s.Cwd)`. **focus.go:** - `FocusTimer` struct avec `mu sync.Mutex` et `expires time.Time`. - `Set(d time.Duration)` : `expires = time.Now().Add(d)` sous verrou. - `IsActive() bool` : `time.Now().Before(expires)` sous verrou. Per D-04: timer uniquement. - `Remaining() time.Duration` : `time.Until(expires)` sous verrou, clamp a 0 si negatif. Per D-05: le focus ne bloque que les notifications, pas l'i3bar. cd /home/pierre/Code/vibe/vmux && nix-shell -p go --run "go test -run 'TestExecNotifier|TestNullNotifier|TestFocusTimer|TestShortName' -count=1 -v" - grep -q "type Notifier interface" notify.go - grep -q "type ExecNotifier struct" notify.go - grep -q "type NullNotifier struct" notify.go - grep -q "notify-send" notify.go - grep -q "func shortName" notify.go - grep -q "type FocusTimer struct" focus.go - grep -q "func.*FocusTimer.*Set" focus.go - grep -q "func.*FocusTimer.*IsActive" focus.go - grep -q "func.*FocusTimer.*Remaining" focus.go - grep -q "TestFocusTimer" focus_test.go - grep -q "TestShortName" notify_test.go Notifier interface et FocusTimer testees et fonctionnelles. ExecNotifier appelle notify-send avec timeout. FocusTimer thread-safe avec Set/IsActive/Remaining. Task 2: Integration notifications dans daemon + CLI focus daemon.go, hook.go, protocol.go, main.go, daemon_test.go - notify.go (Notifier interface cree en Task 1) - focus.go (FocusTimer cree en Task 1) - daemon.go (Daemon struct, NewDaemon, handleConnection) - hook.go (processHookEvent, UpdateFromHook, TrackedSession.PrevState) - main.go (switch CLI, runDaemon, printUsage) - protocol.go (Request, Response, FocusArgs a ajouter) - TestNotification_WorkingToNeedsInput: processHookEvent avec prevState=Working, nouvel etat Needs Input -> notifier.Notify appele - TestNotification_IdleToNeedsInput: processHookEvent avec prevState=Idle, nouvel etat Needs Input -> notifier.Notify PAS appele (per D-01) - TestNotification_FocusActive: focus.Set(30min), transition Working -> Needs Input -> notifier.Notify PAS appele (per D-05) - TestNotification_FocusExpired: focus avec duree 0, transition Working -> Needs Input -> notifier.Notify appele - TestFocusHandler: envoyer action "focus" avec minutes=30 -> response OK, focus.IsActive() true **protocol.go:** - Ajouter `FocusArgs struct { Minutes int \`json:"minutes"\` }` - Ajouter `FocusRemaining float64 \`json:"focus_remaining,omitempty"\`` dans Response (pour feedback CLI) **daemon.go:** - Ajouter champs `notifier Notifier` et `focus *FocusTimer` dans Daemon struct - Dans NewDaemon: initialiser `focus: &FocusTimer{}` et `notifier: &ExecNotifier{}` - Dans handleConnection, ajouter case "focus": - Parser FocusArgs, appeler `d.focus.Set(time.Duration(args.Minutes) * time.Minute)` - Repondre `Response{OK: true, FocusRemaining: d.focus.Remaining().Minutes()}` **hook.go — processHookEvent:** - AVANT l'appel a `d.registry.UpdateFromHook`, lire le PrevState: ```go d.registry.mu.RLock() prevState := "" if ts, ok := d.registry.sessions[event.SessionID]; ok { prevState = ts.PrevState } d.registry.mu.RUnlock() ``` - APRES `d.registry.UpdateFromHook`, si `state == "Needs Input" && prevState == "Working"` (per D-01): - Si `!d.focus.IsActive()` (per D-05): appeler `d.notifier.Notify("vmux: "+shortName(info), "Session needs input ("+waitType+")")` - Pour le shortName, utiliser le SessionInfo du registre (qui a Label et Cwd) **main.go:** - Ajouter case "focus" dans le switch CLI: - Parser argument `filteredArgs[1]` comme entier (minutes). Erreur si absent ou invalide. Per D-04: timer uniquement, pas de toggle. - Envoyer action "focus" avec FocusArgs au daemon via client - Afficher "Focus mode: notifications suppressed for N minutes" - Ajouter "focus" dans printUsage: ` focus Suppress notifications for N minutes` - Dans runDaemon: le notifier est deja initialise par NewDaemon, rien a changer **Pitfall PrevState (de RESEARCH):** Lire PrevState AVANT UpdateFromHook sous verrou separe. La lecture RLock puis l'ecriture Lock dans UpdateFromHook sont safe car RLock se relache avant. **Pitfall notify-send timeout (de RESEARCH):** Deja gere dans ExecNotifier avec CommandContext 5s. cd /home/pierre/Code/vibe/vmux && nix-shell -p go --run "go test -run 'TestNotification|TestFocusHandler' -count=1 -v" - grep -q "notifier Notifier" daemon.go - grep -q "focus.*FocusTimer" daemon.go - grep -q "type FocusArgs struct" protocol.go - grep -q 'case "focus"' daemon.go - grep -q 'case "focus"' main.go - grep -q "prevState.*Working" hook.go - grep -q "notifier.Notify" hook.go - grep -q "focus.IsActive" hook.go - grep -q "focus " main.go - grep -q "TestNotification" daemon_test.go Notifications dunst actives sur transition Working -> Needs Input uniquement. Mode focus bloque les notifications. CLI `vmux focus 30` fonctionnel. Tests passent. - `nix-shell -p go --run "go test ./... -count=1 -race"` passe sans erreur - `nix-shell -p go --run "go build -o /dev/null ."` compile sans erreur - grep confirme: Notifier interface, FocusTimer, integration hook, CLI focus 1. Transition Working -> Needs Input declenche une notification dunst (via notify-send) 2. Transitions Idle -> Needs Input et Working -> Idle ne declenchent PAS de notification 3. `vmux focus 30` supprime les notifications pendant 30 minutes 4. Le focus expire automatiquement 5. Tous les tests passent avec -race After completion, create `.planning/phases/04-notifications-et-i3bar/04-01-SUMMARY.md`