docs(04): create phase plan for notifications et i3bar
This commit is contained in:
268
.planning/phases/04-notifications-et-i3bar/04-01-PLAN.md
Normal file
268
.planning/phases/04-notifications-et-i3bar/04-01-PLAN.md
Normal file
@@ -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 <minutes> envoie action focus au daemon"
|
||||
pattern: "case \"focus\""
|
||||
---
|
||||
|
||||
<objective>
|
||||
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`.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/04-notifications-et-i3bar/04-RESEARCH.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Existing code contracts needed by executor -->
|
||||
|
||||
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
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Notifier interface, FocusTimer, et tests</name>
|
||||
<files>notify.go, notify_test.go, focus.go, focus_test.go</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- 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"
|
||||
</behavior>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/pierre/Code/vibe/vmux && nix-shell -p go --run "go test -run 'TestExecNotifier|TestNullNotifier|TestFocusTimer|TestShortName' -count=1 -v"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>Notifier interface et FocusTimer testees et fonctionnelles. ExecNotifier appelle notify-send avec timeout. FocusTimer thread-safe avec Set/IsActive/Remaining.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Integration notifications dans daemon + CLI focus</name>
|
||||
<files>daemon.go, hook.go, protocol.go, main.go, daemon_test.go</files>
|
||||
<read_first>
|
||||
- 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)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- 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
|
||||
</behavior>
|
||||
<action>
|
||||
**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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/pierre/Code/vibe/vmux && nix-shell -p go --run "go test -run 'TestNotification|TestFocusHandler' -count=1 -v"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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 <minutes>" main.go
|
||||
- grep -q "TestNotification" daemon_test.go
|
||||
</acceptance_criteria>
|
||||
<done>Notifications dunst actives sur transition Working -> Needs Input uniquement. Mode focus bloque les notifications. CLI `vmux focus 30` fonctionnel. Tests passent.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `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
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/04-notifications-et-i3bar/04-01-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user