diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index a50bbf0..9dc6293 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -70,13 +70,13 @@ Plans: **Requirements**: NOTIF-01, NOTIF-02, I3-03 **Success Criteria** (what must be TRUE): 1. Une notification dunst apparait quand une session passe de "Working" a "Needs Input" - 2. Le mode focus supprime temporairement les notifications (`vmux focus on/off`) + 2. Le mode focus supprime temporairement les notifications (`vmux focus `) 3. Un widget i3bar affiche en temps reel le nombre de sessions et combien attendent de l'input -**Plans**: TBD +**Plans**: 2 plans Plans: -- [ ] 04-01: TBD -- [ ] 04-02: TBD +- [ ] 04-01-PLAN.md — Notifications dunst (Working->Needs Input), mode focus avec timer, CLI vmux focus +- [ ] 04-02-PLAN.md — Widget i3bar avec format compact, wrapping i3status, CLI vmux i3bar ## Progress diff --git a/.planning/phases/04-notifications-et-i3bar/04-01-PLAN.md b/.planning/phases/04-notifications-et-i3bar/04-01-PLAN.md new file mode 100644 index 0000000..22372b9 --- /dev/null +++ b/.planning/phases/04-notifications-et-i3bar/04-01-PLAN.md @@ -0,0 +1,268 @@ +--- +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` + diff --git a/.planning/phases/04-notifications-et-i3bar/04-02-PLAN.md b/.planning/phases/04-notifications-et-i3bar/04-02-PLAN.md new file mode 100644 index 0000000..9925195 --- /dev/null +++ b/.planning/phases/04-notifications-et-i3bar/04-02-PLAN.md @@ -0,0 +1,248 @@ +--- +phase: 04-notifications-et-i3bar +plan: 02 +type: execute +wave: 1 +depends_on: [] +files_modified: [i3bar.go, i3bar_test.go, main.go] +autonomous: false +requirements: [I3-03] + +must_haves: + truths: + - "vmux i3bar affiche le statut des sessions en format i3bar JSON sur stdout" + - "Le format est vmux: auth[!] portal[W] neia[I] avec noms courts" + - "Quand aucune session n'attend: vmux: all working (3)" + - "Couleur rouge si >= 1 session attend, vert sinon" + - "Le widget wrap i3status pour garder les infos systeme" + artifacts: + - path: "i3bar.go" + provides: "I3BarBlock, formatI3BarBlocks, shortName, runI3Bar" + exports: ["I3BarBlock", "formatI3BarBlocks", "runI3Bar"] + - path: "i3bar_test.go" + provides: "Tests formatage i3bar" + key_links: + - from: "i3bar.go" + to: "protocol.go" + via: "formatI3BarBlocks consomme []SessionInfo" + pattern: "formatI3BarBlocks.*SessionInfo" + - from: "main.go" + to: "i3bar.go" + via: "case i3bar appelle runI3Bar" + pattern: "case \"i3bar\"" +--- + + +Widget i3bar affichant le statut des sessions vmux en temps reel. + +Purpose: L'utilisateur voit dans sa barre i3 quelles sessions Claude Code ont besoin de lui, sans ouvrir vmux. +Output: i3bar.go avec formatage et boucle i3bar protocol, sous-commande `vmux i3bar`, wrapping i3status. + + + +@$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 protocol.go: +```go +type SessionInfo struct { + PID int `json:"pid"` + SessionID string `json:"session_id"` + Cwd string `json:"cwd"` + GitBranch string `json:"git_branch"` + State string `json:"state"` + Preview string `json:"preview"` + Workspace string `json:"workspace"` + Label string `json:"label,omitempty"` + WaitType string `json:"wait_type,omitempty"` + WaitingSince *time.Time `json:"waiting_since,omitempty"` +} + +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"` +} +``` + +From client.go (inferred from main.go usage): +```go +func NewClient(sockPath string) *Client +func (c *Client) Send(req Request) (*Response, error) +``` + +From main.go: +```go +// CLI dispatch: switch filteredArgs[0] { case "list", "switch", "label", "stop", "daemon" } +// runDaemon(sockPath) starts the daemon +// printUsage() shows help +``` + + + + + + + Task 1: Formatage i3bar et tests + i3bar.go, i3bar_test.go + + - protocol.go (SessionInfo struct) + - display.go (pattern formatage existant, shortName si deja defini) + - notify.go (shortName si cree en plan 01, sinon le creer ici) + + + - TestFormatI3BarBlocks_MixedStates: 3 sessions (Working, Needs Input, Idle) -> full_text="vmux: auth[!] portal[W] neia[I]", color="#ff0000" + - TestFormatI3BarBlocks_AllWorking: 3 sessions Working -> full_text="vmux: all working (3)", color="#00ff00" (per D-07) + - TestFormatI3BarBlocks_NoSessions: [] -> full_text="vmux: no sessions", color="#00ff00" + - TestFormatI3BarBlocks_ColorRed: >= 1 Needs Input -> color="#ff0000" (per D-08) + - TestFormatI3BarBlocks_ColorGreen: 0 Needs Input -> color="#00ff00" (per D-08) + - TestFormatI3BarBlocks_UsesLabel: session avec Label="auth" -> "auth[W]" pas le cwd (per D-06) + - TestFormatI3BarBlocks_UsesCwdBase: session sans label, cwd="/home/pierre/Code/vibe/vmux" -> "vmux[W]" (per D-06) + - TestFormatI3BarBlocks_NeedsInputMarker: session Needs Input -> "[!]" (per D-06) + - TestFormatI3BarBlocks_IdleMarker: session Idle -> "[I]" (per D-06) + + + Creer 2 fichiers. TDD: tests d'abord. + + **i3bar.go:** + - `I3BarBlock` struct avec champs JSON: `full_text`, `short_text` (omitempty), `color`, `name`, `markup` (="none"). + - `formatI3BarBlocks(sessions []SessionInfo) []I3BarBlock`: + - Iterer les sessions. Pour chaque, `shortName(s)` (fonction de notify.go ou locale si plan 01 pas encore execute) + suffixe selon State: + - "Needs Input" -> `[!]`, set `hasWaiting = true` + - "Working" -> `[W]` + - "Idle" -> `[I]` + - Si `!hasWaiting && len(sessions) > 0`: text = `vmux: all working (N)` per D-07 + - Si `len(sessions) == 0`: text = `vmux: no sessions` + - Sinon: text = `vmux: ` + parts jointes par espace per D-06 + - Color: `#ff0000` si hasWaiting, `#00ff00` sinon per D-08 + - Retourner un seul bloc `[]I3BarBlock{{FullText: text, Color: color, Name: "vmux"}}` + + **Note fichiers partages avec plan 01:** Si plan 01 est execute en parallele et definit `shortName` dans notify.go, cette fonction sera deja disponible. Sinon, definir `shortName` dans i3bar.go (et deplacer plus tard si doublon). Le plan 01 a la priorite sur shortName. + + **Pitfall stdout buffering (de RESEARCH):** Pas concerne dans cette tache (formatage pur). Gere en Task 2. + + + cd /home/pierre/Code/vibe/vmux && nix-shell -p go --run "go test -run 'TestFormatI3Bar' -count=1 -v" + + + - grep -q "type I3BarBlock struct" i3bar.go + - grep -q "func formatI3BarBlocks" i3bar.go + - grep -q "all working" i3bar.go + - grep -q "#ff0000" i3bar.go + - grep -q "#00ff00" i3bar.go + - grep -q "TestFormatI3BarBlocks" i3bar_test.go + + formatI3BarBlocks produit le format compact D-06/D-07/D-08. Tous les tests passent. + + + + Task 2: Boucle i3bar protocol et CLI + i3bar.go, main.go + + - i3bar.go (I3BarBlock et formatI3BarBlocks crees en Task 1) + - main.go (switch CLI, sockPath, EnsureDaemon pattern) + - protocol.go (Request/Response) + - 04-RESEARCH.md (pattern i3bar protocol, i3status wrapping, pitfall stdout buffering) + + + **i3bar.go — runI3Bar(sockPath, i3statusCmd string):** + Boucle infinie qui parle le protocole i3bar v1 sur stdout. Deux modes selon i3statusCmd: + + **Mode standalone (i3statusCmd vide):** + 1. Ecrire header: `{"version":1}` + newline + 2. Ecrire `[` + newline + 3. Boucle: query daemon via socket (`action: "list"`), formater avec `formatI3BarBlocks`, json.Marshal le tableau de blocs, ecrire `,` (sauf premier) + ligne JSON + newline. `os.Stdout.Write()` + flush explicite (per pitfall stdout buffering de RESEARCH). + 4. Sleep 2 secondes entre chaque iteration. + 5. Si le daemon est injoignable, afficher bloc `vmux: daemon offline` en gris. + + **Mode wrap i3status (i3statusCmd non-vide):** + 1. Lancer i3statusCmd en subprocess (`exec.Command(i3statusCmd)`) + 2. Lire et forwarder le header JSON (`{"version":1}`) + 3. Lire et forwarder le `[` + 4. Pour chaque ligne lue de i3status: trim le `,` initial, json.Unmarshal en `[]I3BarBlock`, prepend le bloc vmux (query daemon), re-Marshal, ecrire avec `,` prefix + 5. Si i3status se termine, continuer en mode standalone + + **main.go:** + - Ajouter case "i3bar" dans le switch CLI: + - `EnsureDaemon(sockPath)` pour s'assurer que le daemon tourne + - Detecter i3statusCmd: chercher `i3status` dans PATH. Si trouve, utiliser comme wrapping. Sinon, mode standalone. + - Appeler `runI3Bar(sockPath, i3statusCmd)` + - Ajouter dans printUsage: ` i3bar Output i3bar JSON (use as status_command in i3 config)` + + **Pitfall stdout buffering:** Utiliser `os.Stdout.Write(data)` suivi de `os.Stdout.Sync()` ou un `bufio.Writer` avec Flush() apres chaque ligne complete. + + **Pitfall i3status wrapping (de RESEARCH):** i3status peut prefixer les lignes de blocs avec `,`. Le parser doit les trimmer avant json.Unmarshal. + + + cd /home/pierre/Code/vibe/vmux && nix-shell -p go --run "go build -o /dev/null . && echo 'build OK'" + + + - grep -q "func runI3Bar" i3bar.go + - grep -q "version.*1" i3bar.go + - grep -q 'case "i3bar"' main.go + - grep -q "i3bar" main.go + - grep -q "i3status" i3bar.go + - grep -q "Stdout" i3bar.go + + vmux i3bar fonctionne en mode standalone et wrap i3status. Le protocole i3bar v1 est respecte (header + array JSON). La sous-commande est accessible. + + + + Task 3: Verification visuelle du widget i3bar + i3bar.go, main.go + + Verification humaine du widget i3bar. Pas de code a ecrire. + + Ce qui a ete construit: Widget i3bar vmux avec format compact, couleurs, wrapping i3status. + + Etapes de verification: + 1. Compiler: `nix-shell -p go --run "go build -o vmux ."` + 2. S'assurer que le daemon tourne: `./vmux list` + 3. Tester le mode standalone: `./vmux i3bar` (Ctrl+C apres quelques lignes) + - Verifier: header `{"version":1}`, puis `[`, puis lignes JSON avec bloc vmux + - Format attendu: `vmux: auth[!] portal[W]` ou `vmux: all working (N)` + 4. Configurer i3bar (optionnel): + - Dans `~/.config/i3/config`, remplacer `status_command i3status` par `status_command /chemin/vers/vmux i3bar` + - Recharger i3: `i3-msg reload` + - Verifier que le bloc vmux apparait dans la barre avec les infos systeme + 5. Verifier les couleurs: rouge si session attend, vert sinon + + User confirms: "approved" or describes issues + L'utilisateur a valide visuellement que le widget i3bar fonctionne correctement dans sa barre i3. + + + + + +- `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 +- `vmux i3bar` produit du JSON i3bar valide sur stdout + + + +1. `vmux i3bar` produit un flux JSON i3bar protocol v1 valide +2. Le format compact respecte D-06 (noms courts + suffixes [!] [W] [I]) +3. "all working (N)" quand aucune session n'attend (D-07) +4. Couleurs rouge/vert selon urgence (D-08) +5. Le widget wrap i3status pour conserver les infos systeme +6. Tous les tests passent avec -race + + + +After completion, create `.planning/phases/04-notifications-et-i3bar/04-02-SUMMARY.md` +