--- phase: 02-daemon-et-i3-bridge plan: 03 type: execute wave: 2 depends_on: ["02-01", "02-02"] files_modified: - client.go - client_test.go - main.go - display.go - display_test.go - daemon.go autonomous: false requirements: [DISC-04, I3-01, I3-02, STATE-04] must_haves: truths: - "vmux list affiche workspace, label et temps d'attente pour chaque session" - "vmux switch bascule vers le workspace i3 de la session matchee" - "vmux label attribue un label humain" - "vmux stop arrete le daemon proprement" - "Le daemon demarre automatiquement si absent quand on lance une commande" artifacts: - path: "client.go" provides: "Client Unix socket CLI -> daemon" exports: ["Client", "EnsureDaemon"] - path: "main.go" provides: "Dispatch sous-commandes list/switch/label/stop/daemon" contains: "case \"switch\"" - path: "display.go" provides: "Affichage enrichi avec workspace, label, temps d'attente" contains: "WaitingSince" key_links: - from: "client.go" to: "daemon.go" via: "Unix socket ~/.vmux/vmux.sock" pattern: "net.Dial.*unix" - from: "main.go" to: "client.go" via: "Dispatch sous-commandes vers Client" pattern: "client\\." - from: "daemon.go" to: "i3bridge.go" via: "Switch handler appelle FuzzyMatch + SwitchToWorkspace" pattern: "FuzzyMatch|SwitchToWorkspace" - from: "daemon.go" to: "workspace.go" via: "workspaceResolver dans la boucle de poll" pattern: "ResolveWorkspace|BuildTerminalWorkspaceMap" --- CLI complet : client socket, sous-commandes (list/switch/label/stop), autostart daemon, affichage enrichi. Purpose: L'utilisateur peut interagir avec vmux via les sous-commandes. Tout est cable de bout en bout. Output: client.go (communication socket), main.go refactore, display.go enrichi, daemon.go complete avec switch + workspace. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/02-daemon-et-i3-bridge/02-CONTEXT.md @.planning/phases/02-daemon-et-i3-bridge/02-RESEARCH.md @.planning/phases/02-daemon-et-i3-bridge/02-01-SUMMARY.md @.planning/phases/02-daemon-et-i3-bridge/02-02-SUMMARY.md From protocol.go (plan 02-01): ```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 int; SessionID string; Cwd string; GitBranch string; State string; Preview string; Workspace string; Label string; WaitingSince *time.Time } type SwitchArgs struct { Query string `json:"query"` } type LabelArgs struct { SessionID string `json:"session_id"`; Label string `json:"label"` } ``` From daemon.go (plan 02-01): ```go type Daemon struct { registry *SessionRegistry; labels *LabelStore; sockPath string; ... } func NewDaemon(sockPath, procDir, claudeDir string, labels *LabelStore) *Daemon func (d *Daemon) Start() error ``` From i3bridge.go (plan 02-02): ```go func FuzzyMatch(query string, sessions []SessionInfo) *SessionInfo func SwitchToWorkspace(commander I3Commander, wsName string) error type RealI3Commander struct{} ``` From workspace.go (plan 02-02): ```go func ResolveWorkspace(procDir string, claudePID int, terminalWorkspaces map[int]string) string func BuildTerminalWorkspaceMap(tree I3TreeProvider, x11 X11PIDResolver) (map[int]string, error) ``` Task 1: Client socket + autostart + daemon switch handler + workspace wiring client.go, client_test.go, daemon.go daemon.go, protocol.go, i3bridge.go, workspace.go 1. Creer client.go (per D-01, D-03): ```go type Client struct { sockPath string } func NewClient(sockPath string) *Client func (c *Client) Send(req Request) (*Response, error) // net.Dial("unix", sockPath), encoder req JSON, decoder response JSON ``` ```go func EnsureDaemon(sockPath string) error // Tenter net.DialTimeout. Si echec: lancer os.Executable() + "daemon" en background. // Retry 50ms x 20 (per RESEARCH Pattern 2). Detacher avec Setsid. ``` 2. Completer daemon.go : ajouter le handler "switch" (per D-07, I3-02): ```go case "switch": var args SwitchArgs json.Unmarshal(req.Args, &args) sessions := r.registry.List() match := FuzzyMatch(args.Query, sessions) if match == nil { return Response{Error: "no session matching..."} } if match.Workspace == "" { return Response{Error: "no workspace for session..."} } err := SwitchToWorkspace(d.i3commander, match.Workspace) ``` 3. Cabler le workspace resolution dans la boucle de poll du daemon: - Au demarrage du daemon, initialiser BuildTerminalWorkspaceMap (i3 GetTree + X11). - Si i3/X11 indisponible: log warning, continuer sans workspace (per Pitfall 4, 5). - Dans scanOnce: appeler ResolveWorkspace(procDir, pid, terminalMap) pour chaque session. - Rafraichir la terminalMap a chaque scan (les fenetres peuvent bouger de workspace). 4. Tests: - TestClientSendReceive: Demarrer un daemon dans goroutine, creer Client, envoyer "list", verifier Response - TestEnsureDaemonAlreadyRunning: Socket actif -> EnsureDaemon retourne nil immediatement nix-shell --run "go test -run 'TestClient|TestEnsureDaemon' -v -race ./..." - grep -q 'type Client struct' client.go - grep -q 'func EnsureDaemon' client.go - grep -q 'case "switch"' daemon.go - grep -q 'FuzzyMatch' daemon.go - grep -q 'ResolveWorkspace\|BuildTerminalWorkspaceMap' daemon.go - grep -q 'TestClientSendReceive' client_test.go Client envoie des requetes au daemon via socket, EnsureDaemon autostart le daemon, le handler switch utilise FuzzyMatch + SwitchToWorkspace, la boucle de poll resout les workspaces. Task 2: main.go sous-commandes + display enrichi main.go, display.go, display_test.go main.go, display.go, display_test.go, client.go, protocol.go 1. Refactorer main.go pour supporter les sous-commandes (per D-03, D-07, D-08): ```go func main() { // Parse --no-color global // Dispatch sur os.Args: // "list" -> EnsureDaemon + Client.Send(list) + DisplaySessionInfos // "switch" -> EnsureDaemon + Client.Send(switch, query) // "label" -> EnsureDaemon + Client.Send(label, session_id, texte) // "stop" -> Client.Send(stop) // "daemon" -> NewDaemon + Start (mode foreground, lance par autostart) // default -> usage } ``` sockPath = `~/.vmux/vmux.sock` (per D-01). Pour "label": args[2] = session_id, args[3:] = label text (join avec espace). Pour "switch": args[2] = query string. 2. Etendre display.go pour afficher les champs enrichis: ```go // DisplaySessionInfos affiche les sessions recues du daemon (SessionInfo, pas Session). func DisplaySessionInfos(w io.Writer, sessions []SessionInfo, noColor bool, now time.Time) ``` Format par session: ``` [Needs Input] /home/pierre/Code/vibe/vmux (feat/auth) [ws:3] "review MR !456" (depuis 3 min) preview line 1 preview line 2 ``` - `[ws:N]` seulement si Workspace non vide (per D-05) - `"label"` entre guillemets seulement si Label non vide (per D-08) - `(depuis X min)` seulement si WaitingSince non nil (per D-09, STATE-04) - Calculer la duree relative : now.Sub(*WaitingSince). Afficher "< 1 min", "3 min", "1 h 5 min". 3. Mise a jour display_test.go: - TestDisplayWithWorkspace: session avec Workspace "3" -> contient "[ws:3]" - TestDisplayWithLabel: session avec Label "review MR" -> contient "\"review MR\"" - TestDisplayWithWaitingSince: session avec WaitingSince 3 min ago -> contient "depuis 3 min" - TestDisplayWithoutOptionalFields: session sans workspace/label/waiting -> pas de [ws:], pas de guillemets, pas de "depuis" nix-shell --run "go test -run 'TestDisplay' -v ./..." && nix-shell --run "go build -o /dev/null ./..." - grep -q 'case "switch"' main.go - grep -q 'case "label"' main.go - grep -q 'case "stop"' main.go - grep -q 'case "daemon"' main.go - grep -q 'EnsureDaemon' main.go - grep -q 'func DisplaySessionInfos' display.go - grep -q 'WaitingSince' display.go - grep -q 'TestDisplayWithWorkspace' display_test.go - grep -q 'TestDisplayWithLabel' display_test.go - grep -q 'TestDisplayWithWaitingSince' display_test.go - nix-shell --run "go build -o /dev/null ./..." (compile sans erreur) main.go dispatch list/switch/label/stop/daemon. DisplaySessionInfos affiche workspace, label et temps d'attente. Le binaire compile. Task 3: Verification manuelle du workflow complet Daemon vmuxd complet avec CLI multi-commandes : list (avec workspace, label, temps d'attente), switch (fuzzy match), label, stop, autostart. 1. Builder : `nix-shell --run "go build -o vmux ./..."` 2. Lancer `./vmux list` (doit autostart le daemon et afficher les sessions avec workspaces) 3. Verifier que chaque session affiche son workspace i3 ([ws:N]) 4. Attribuer un label : `./vmux label "test label"` 5. Relancer `./vmux list` et verifier que le label apparait 6. Tester le switch : `./vmux switch ` (doit changer de workspace) 7. Verifier que les sessions en attente affichent "depuis X min" 8. Arreter : `./vmux stop` 9. Verifier que le socket est supprime : `ls ~/.vmux/vmux.sock` (doit echouer) Type "approved" ou decris les problemes constates nix-shell --run "go test -v -race ./..." nix-shell --run "go build -o vmux ./..." ./vmux list - vmux list affiche workspace, label et temps d'attente (DISC-04, I3-01, STATE-04) - vmux switch bascule vers le bon workspace i3 (I3-02) - vmux label persiste le label (DISC-04) - vmux stop arrete le daemon (D-03) - Autostart fonctionne (D-03) - Le binaire compile et tous les tests passent After completion, create `.planning/phases/02-daemon-et-i3-bridge/02-03-SUMMARY.md`