Files
vmux/.planning/phases/04-notifications-et-i3bar/04-01-PLAN.md
2026-03-23 21:07:35 +01:00

11 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
04-notifications-et-i3bar 01 execute 1
notify.go
notify_test.go
focus.go
focus_test.go
daemon.go
hook.go
protocol.go
main.go
true
NOTIF-01
NOTIF-02
truths artifacts key_links
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
path provides exports
notify.go Notifier interface + ExecNotifier (notify-send)
Notifier
ExecNotifier
NullNotifier
path provides exports
focus.go FocusTimer struct thread-safe
FocusTimer
path provides
notify_test.go Tests notification transitions
path provides
focus_test.go Tests FocusTimer Set/IsActive/Remaining/expiry
from to via pattern
hook.go notify.go d.notifier.Notify() apres transition Working -> Needs Input notifier.Notify
from to via pattern
hook.go focus.go d.focus.IsActive() pour bloquer les notifications focus.IsActive
from to via pattern
main.go daemon.go vmux focus <minutes> envoie action focus au daemon 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 <minutes>. Output: notify.go, focus.go, integration dans daemon/hook, sous-commande vmux focus.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/04-notifications-et-i3bar/04-RESEARCH.md

From daemon.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:

func (d *Daemon) processHookEvent(event HookEvent) // maps hook -> registry update
func (r *SessionRegistry) UpdateFromHook(sessionID, state, waitType, cwd string) // tracks PrevState

From protocol.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):

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 <minutes>   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

<success_criteria>

  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 </success_criteria>
After completion, create `.planning/phases/04-notifications-et-i3bar/04-01-SUMMARY.md`