diff --git a/.planning/phases/02-daemon-et-i3-bridge/02-01-PLAN.md b/.planning/phases/02-daemon-et-i3-bridge/02-01-PLAN.md new file mode 100644 index 0000000..e99a56b --- /dev/null +++ b/.planning/phases/02-daemon-et-i3-bridge/02-01-PLAN.md @@ -0,0 +1,269 @@ +--- +phase: 02-daemon-et-i3-bridge +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - types.go + - protocol.go + - protocol_test.go + - daemon.go + - daemon_test.go +autonomous: true +requirements: [DISC-04, STATE-04] + +must_haves: + truths: + - "Le daemon maintient un registre des sessions a jour (poll toutes les 5s)" + - "Le daemon ecoute sur un Unix socket et repond aux requetes JSON" + - "Les labels sont persistes dans ~/.vmux/labels.json" + - "Le temps d'attente est tracke quand une session passe a NeedsInput" + artifacts: + - path: "protocol.go" + provides: "Types Request, Response, SessionInfo pour le protocole socket" + exports: ["Request", "Response", "SessionInfo"] + - path: "daemon.go" + provides: "SessionRegistry, LabelStore, boucle de poll, Unix socket server" + exports: ["SessionRegistry", "LabelStore", "StartDaemon"] + - path: "types.go" + provides: "Session enrichi avec Workspace, Label, WaitingSince" + contains: "Workspace string" + key_links: + - from: "daemon.go" + to: "proc.go" + via: "FindClaudeProcesses dans la boucle de poll" + pattern: "FindClaudeProcesses" + - from: "daemon.go" + to: "protocol.go" + via: "Request/Response JSON sur le socket" + pattern: "Request|Response" +--- + + +Daemon vmuxd : registre des sessions, Unix socket server, labels persistants et tracking du temps d'attente. + +Purpose: Transformer vmux d'un CLI one-shot en un daemon persistant qui maintient l'etat des sessions. +Output: protocol.go (types IPC), daemon.go (registre + socket + poll + labels), types.go enrichi. + + + +@$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/02-daemon-et-i3-bridge/02-CONTEXT.md +@.planning/phases/02-daemon-et-i3-bridge/02-RESEARCH.md +@.planning/phases/01-session-discovery/01-02-SUMMARY.md + + + + +From types.go: +```go +type SessionState int +const (Working SessionState = iota; NeedsInput; Idle; Unknown) +type Process struct { PID int; Cmd []string; Cwd string } +type Session struct { Process Process; SessionID string; GitBranch string; State SessionState; Preview string; CwdPath string; Worktree string } +``` + +From proc.go: +```go +func FindClaudeProcesses(procDir string) ([]Process, error) +func EncodePath(path string) string +``` + +From session.go: +```go +func FindSessionForProcess(claudeDir string, proc Process) (string, []JSONLMessage, error) +func TailReadJSONL(path string, n int) ([]JSONLMessage, error) +``` + +From state.go: +```go +func DetectState(messages []JSONLMessage, now time.Time) SessionState +func ExtractPreview(messages []JSONLMessage) string +``` + + + + + + + Task 1: Protocol types + SessionRegistry + LabelStore + protocol.go, protocol_test.go, daemon.go, daemon_test.go, types.go + types.go, proc.go, state.go, session.go + + - TestRequestMarshal: Request{Action:"list"} se serialise/deserialise correctement en JSON + - TestResponseWithSessions: Response avec SessionInfo contenant Workspace, Label, WaitingSince + - TestRegistryUpdate: Ajouter une session, la retrouver dans List() + - TestRegistryWaitingSince: Session passant de Working a NeedsInput enregistre WaitingSince; re-passer a Working la reset + - TestRegistryRemoveStale: Sessions absentes du scan sont supprimees du registre + - TestLabelStoreSetGet: Set("session-id", "review MR") puis Get("session-id") retourne "review MR" + - TestLabelStorePersistence: Set() ecrit sur disque, nouveau LabelStore charge le fichier et retrouve le label + - TestLabelStoreLoadMissing: Charger un fichier inexistant retourne un store vide sans erreur + + + 1. Enrichir Session dans types.go (per D-08, D-09): + - Ajouter `Workspace string` + - Ajouter `Label string` + - Ajouter `WaitingSince *time.Time` + + 2. Creer protocol.go avec les types IPC (per D-01): + ```go + type Request struct { + Action string `json:"action"` // "list", "switch", "label", "stop" + 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 `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"` + WaitingSince *time.Time `json:"waiting_since,omitempty"` + } + type SwitchArgs struct { Query string `json:"query"` } + type LabelArgs struct { SessionID string `json:"session_id"`; Label string `json:"label"` } + ``` + + 3. Creer daemon.go avec SessionRegistry (per D-02, D-09): + ```go + type TrackedSession struct { + Info SessionInfo + PrevState string + WaitingSince time.Time + } + type SessionRegistry struct { + mu sync.RWMutex + sessions map[string]*TrackedSession + } + func NewRegistry() *SessionRegistry + func (r *SessionRegistry) Update(info SessionInfo) // track WaitingSince transitions + func (r *SessionRegistry) List() []SessionInfo + func (r *SessionRegistry) RemoveStale(activeIDs map[string]bool) + ``` + + 4. Creer LabelStore dans daemon.go (per D-08): + ```go + type LabelStore struct { + mu sync.RWMutex + labels map[string]string + path string + } + func NewLabelStore(path string) (*LabelStore, error) + func (ls *LabelStore) Set(sessionID, label string) error + func (ls *LabelStore) Get(sessionID string) string + ``` + Persistence dans le fichier JSON. Load au demarrage, save apres chaque Set. + + + nix-shell --run "go test -run 'TestRequest|TestResponse|TestRegistry|TestLabelStore' -v ./..." + + + - grep -q 'Workspace string' types.go + - grep -q 'WaitingSince' types.go + - grep -q 'type Request struct' protocol.go + - grep -q 'type Response struct' protocol.go + - grep -q 'type SessionRegistry struct' daemon.go + - grep -q 'type LabelStore struct' daemon.go + - grep -q 'func.*LabelStore.*Set' daemon.go + - grep -q 'TestRegistryWaitingSince' daemon_test.go + - grep -q 'TestLabelStorePersistence' daemon_test.go + + Protocol types compiles, Registry tracke les transitions WaitingSince, LabelStore persiste sur disque, tous les tests passent. + + + + Task 2: Unix socket server + poll loop + stop handler + daemon.go, daemon_test.go + daemon.go, protocol.go, proc.go, session.go, state.go + + 1. Ajouter la boucle de poll dans daemon.go (per D-02): + ```go + func (d *Daemon) scanOnce(procDir, claudeDir string, now time.Time) { + // FindClaudeProcesses -> FindSessionForProcess -> DetectState -> registry.Update + // resolveWorkspace via d.workspaceResolver (interface, nil-safe pour tests) + } + ``` + Le daemon fait un scan synchrone AVANT d'ecouter sur le socket (per Pitfall 3). + Ticker de 5 secondes pour les scans suivants. + + 2. Ajouter le socket server (per D-01): + ```go + type Daemon struct { + registry *SessionRegistry + labels *LabelStore + sockPath string + procDir string + claudeDir string + workspaceResolver func(claudePID int) string // nil = pas de workspace + stopCh chan struct{} + } + func NewDaemon(sockPath, procDir, claudeDir string, labels *LabelStore) *Daemon + func (d *Daemon) Start() error // scan initial, listen, poll loop + func (d *Daemon) handleConnection(conn net.Conn) // dispatch Request.Action + ``` + Gestion du stale socket (per Pitfall 2): tenter net.Dial avant net.Listen, supprimer si stale. + PID file dans ~/.vmux/vmuxd.pid. + + 3. Handler pour chaque action: + - "list": registry.List() avec labels enrichis + - "label": labels.Set(args.SessionID, args.Label) + - "stop": fermer le listener, signal stopCh + + 4. Tests: + - TestDaemonStartStop: demarrer daemon dans goroutine avec tmpdir socket, envoyer "stop", verifier arret propre + - TestDaemonListOverSocket: demarrer daemon, envoyer "list", verifier la reponse JSON + - TestDaemonLabelOverSocket: envoyer "label", puis "list", verifier que le label apparait + + Note: Le handler "switch" sera ajoute dans le plan 02-03 (depend de i3bridge du plan 02-02). + Le workspaceResolver est nil dans les tests de ce plan. Le plan 02-02 fournira l'implementation reelle. + + + nix-shell --run "go test -run 'TestDaemon' -v -race ./..." + + + - grep -q 'type Daemon struct' daemon.go + - grep -q 'func.*Daemon.*Start' daemon.go + - grep -q 'func.*Daemon.*handleConnection' daemon.go + - grep -q 'net.Listen.*unix' daemon.go + - grep -q 'TestDaemonStartStop' daemon_test.go + - grep -q 'TestDaemonListOverSocket' daemon_test.go + + Le daemon demarre, ecoute sur un Unix socket, repond a list/label/stop, fait un scan initial synchrone et poll toutes les 5s. Tests avec -race passent. + + + + + +nix-shell --run "go test -v -race ./..." +grep -q 'type Daemon struct' daemon.go +grep -q 'type LabelStore struct' daemon.go +grep -q 'type Request struct' protocol.go + + + +- Protocol types (Request, Response, SessionInfo) definis et testes +- SessionRegistry tracke les transitions d'etat avec WaitingSince (STATE-04) +- LabelStore persiste les labels dans ~/.vmux/labels.json (DISC-04) +- Daemon ecoute sur Unix socket, repond aux requetes list/label/stop (D-01) +- Boucle de poll toutes les 5s avec scan initial synchrone (D-02) +- Tous les tests passent avec -race + + + +After completion, create `.planning/phases/02-daemon-et-i3-bridge/02-01-SUMMARY.md` + diff --git a/.planning/phases/02-daemon-et-i3-bridge/02-02-PLAN.md b/.planning/phases/02-daemon-et-i3-bridge/02-02-PLAN.md new file mode 100644 index 0000000..61ea589 --- /dev/null +++ b/.planning/phases/02-daemon-et-i3-bridge/02-02-PLAN.md @@ -0,0 +1,242 @@ +--- +phase: 02-daemon-et-i3-bridge +plan: 02 +type: execute +wave: 1 +depends_on: [] +files_modified: + - workspace.go + - workspace_test.go + - i3bridge.go + - i3bridge_test.go + - go.mod + - go.sum +autonomous: true +requirements: [I3-01, I3-02] + +must_haves: + truths: + - "Un PID Claude Code est resolu vers son workspace i3 via la chaine PPID" + - "Le fuzzy match trouve une session par label, branche ou cwd" + - "Le switch vers un workspace i3 fonctionne via i3 IPC RunCommand" + - "Si i3 ou X11 est absent, le mapping retourne vide sans erreur" + artifacts: + - path: "workspace.go" + provides: "Resolution PID -> workspace via PPID chain + X11 _NET_WM_PID" + exports: ["ReadPPID", "ResolveWorkspace", "BuildTerminalWorkspaceMap"] + - path: "i3bridge.go" + provides: "Interface I3Client, fuzzy match, switch workspace" + exports: ["I3Client", "FuzzyMatch", "SwitchToWorkspace"] + key_links: + - from: "workspace.go" + to: "/proc/PID/status" + via: "ReadPPID lit le PPid" + pattern: "ReadPPID" + - from: "i3bridge.go" + to: "go.i3wm.org/i3/v4" + via: "GetTree, RunCommand" + pattern: "i3\\.GetTree|i3\\.RunCommand" +--- + + +i3 bridge : mapping PID -> workspace via PPID chain + X11, fuzzy match pour switch, abstraction i3 IPC. + +Purpose: Permettre a vmux de savoir dans quel workspace i3 se trouve chaque session Claude Code et d'y switcher. +Output: workspace.go (PPID chain walk + X11 PID resolution), i3bridge.go (interface i3, fuzzy match, switch). + + + +@$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 + + + + +From types.go: +```go +type Process struct { PID int; Cmd []string; Cwd string } +``` + +From protocol.go (cree par plan 02-01, mais ce plan n'en depend pas): +```go +type SessionInfo struct { + PID int `json:"pid"` + Workspace string `json:"workspace"` + Label string `json:"label,omitempty"` + GitBranch string `json:"git_branch"` + Cwd string `json:"cwd"` + // ... +} +``` + + + + + + + + + Task 1: PPID chain walk + workspace resolution + workspace.go, workspace_test.go + proc.go, proc_test.go + + - TestReadPPID: Lire le PPid depuis un faux /proc/PID/status -> retourne le bon PPID + - TestReadPPIDMissing: Fichier inexistant -> erreur + - TestResolveWorkspace: Claude PID 100 -> PPID 50 -> PPID 10 (dans terminalMap) -> retourne "workspace 3" + - TestResolveWorkspaceNotFound: Aucun ancetre dans terminalMap -> retourne "" + - TestResolveWorkspaceMaxDepth: Chaine > 20 niveaux -> retourne "" (securite, per Pitfall 7) + - TestBuildTerminalWorkspaceMapUnit: Avec un mock i3 tree + mock X11 PID resolver, retourne la bonne map + + + 1. Creer workspace.go: + + ```go + // ReadPPID lit le PPid depuis /proc/PID/status. procDir injectable pour les tests. + func ReadPPID(procDir string, pid int) (int, error) + ``` + Parser la ligne "PPid:\tNNN" du fichier status. + + ```go + // ResolveWorkspace remonte la chaine PPID depuis claudePID jusqu'a trouver + // un PID connu dans terminalWorkspaces. Max 20 niveaux (per Pitfall 7). + func ResolveWorkspace(procDir string, claudePID int, terminalWorkspaces map[int]string) string + ``` + + ```go + // X11PIDResolver abstrait la lecture de _NET_WM_PID pour testabilite. + type X11PIDResolver interface { + GetPID(windowID uint32) (int, error) + } + + // I3TreeProvider abstrait i3.GetTree() pour testabilite. + type I3TreeProvider interface { + GetTree() (*i3.Tree, error) + } + + // BuildTerminalWorkspaceMap construit la map terminalPID -> workspaceName. + // Utilise i3 GetTree + X11 _NET_WM_PID (per D-04 corrige par RESEARCH). + func BuildTerminalWorkspaceMap(tree I3TreeProvider, x11 X11PIDResolver) (map[int]string, error) + ``` + Walk recursif de l'arbre i3 : pour chaque node avec Window > 0, appeler x11.GetPID(node.Window). + Tracker le workspace courant via node.Type == i3.WorkspaceNode. + + 2. Creer l'implementation reelle X11 avec xgbutil (per RESEARCH): + ```go + type RealX11Resolver struct { xu *xgbutil.XUtil } + func NewRealX11Resolver() (*RealX11Resolver, error) // verifie $DISPLAY (per Pitfall 5) + func (r *RealX11Resolver) GetPID(windowID uint32) (int, error) // ewmh.WmPidGet + func (r *RealX11Resolver) Close() + ``` + + 3. Tests avec fake /proc (meme pattern que proc_test.go): + - Creer des faux fichiers /proc/PID/status avec PPid + - Mock I3TreeProvider et X11PIDResolver pour tester BuildTerminalWorkspaceMap + - ReadPPID et ResolveWorkspace testes avec le filesystem reel (tmpdir) + + 4. Ajouter go.i3wm.org/i3/v4 dans go.mod (per D-06): + ```bash + nix-shell --run "go get go.i3wm.org/i3/v4@latest" + ``` + Et BurntSushi/xgbutil pour ewmh (dependance transitive mais import explicite necessaire). + + + nix-shell --run "go test -run 'TestReadPPID|TestResolveWorkspace|TestBuildTerminal' -v ./..." + + + - grep -q 'func ReadPPID' workspace.go + - grep -q 'func ResolveWorkspace' workspace.go + - grep -q 'func BuildTerminalWorkspaceMap' workspace.go + - grep -q 'type X11PIDResolver interface' workspace.go + - grep -q 'TestResolveWorkspace' workspace_test.go + - grep -q 'TestReadPPID' workspace_test.go + - grep -q 'go.i3wm.org/i3/v4' go.mod + + ReadPPID lit le PPid depuis /proc, ResolveWorkspace remonte la chaine PPID avec limite de 20, BuildTerminalWorkspaceMap abstrait i3+X11 derriere des interfaces testables. Tous les tests passent. + + + + Task 2: Fuzzy match + switch workspace + i3 client interface + i3bridge.go, i3bridge_test.go + workspace.go + + - TestFuzzyMatchByLabel: sessions avec labels, query "review" matche la session avec label "review MR !456" + - TestFuzzyMatchByBranch: query "auth" matche la session sur branche "feat/auth-flow" + - TestFuzzyMatchByCwd: query "vmux" matche la session avec cwd "/home/pierre/Code/vibe/vmux" + - TestFuzzyMatchPriority: session avec label "auth" ET autre session avec branche "auth" -> le label gagne + - TestFuzzyMatchNoResult: query "inexistant" retourne nil + - TestFuzzyMatchCaseInsensitive: query "AUTH" matche "auth" dans la branche + - TestSwitchToWorkspace: Mock i3 RunCommand, verifier que "workspace number 3" est envoye + + + 1. Creer i3bridge.go: + + ```go + // FuzzyMatch trouve la premiere session matchant query dans : label > branche > cwd. + // Case-insensitive. Retourne nil si aucun match (per D-07). + func FuzzyMatch(query string, sessions []SessionInfo) *SessionInfo + ``` + Priorite : label > GitBranch > Cwd. strings.Contains + strings.ToLower. + + ```go + // I3Commander abstrait i3.RunCommand pour testabilite. + type I3Commander interface { + RunCommand(command string) ([]i3.CommandResult, error) + } + + // SwitchToWorkspace bascule vers le workspace indique via i3 IPC (per D-06). + func SwitchToWorkspace(commander I3Commander, wsName string) error + ``` + Envoie `workspace number `. Verifie le Success du resultat. + + ```go + // RealI3Commander utilise go.i3wm.org/i3/v4 directement. + type RealI3Commander struct{} + func (c RealI3Commander) RunCommand(cmd string) ([]i3.CommandResult, error) + ``` + + 2. Tests : + - FuzzyMatch : pure logique, pas de mock i3. Construire des []SessionInfo en dur. + - SwitchToWorkspace : mock I3Commander qui enregistre la commande recue. + + + nix-shell --run "go test -run 'TestFuzzyMatch|TestSwitchToWorkspace' -v ./..." + + + - grep -q 'func FuzzyMatch' i3bridge.go + - grep -q 'func SwitchToWorkspace' i3bridge.go + - grep -q 'type I3Commander interface' i3bridge.go + - grep -q 'TestFuzzyMatchByLabel' i3bridge_test.go + - grep -q 'TestFuzzyMatchPriority' i3bridge_test.go + - grep -q 'TestSwitchToWorkspace' i3bridge_test.go + + FuzzyMatch cherche dans label > branche > cwd (case-insensitive), SwitchToWorkspace envoie la commande i3 IPC. Tests couvrent les 3 niveaux de priorite, le no-match et le case-insensitive. + + + + + +nix-shell --run "go test -v -race ./..." +grep -q 'func ResolveWorkspace' workspace.go +grep -q 'func FuzzyMatch' i3bridge.go + + + +- Mapping PID -> workspace via PPID chain fonctionnel et teste (I3-01) +- Fuzzy match sur label/branche/cwd avec priorite correcte (I3-02) +- Switch workspace via i3 IPC abstrait derriere une interface testable (I3-02) +- Fallback gracieux si i3/X11 absent (per Pitfall 4, 5) +- go.i3wm.org/i3/v4 ajoute dans go.mod (D-06) +- Tous les tests passent avec -race + + + +After completion, create `.planning/phases/02-daemon-et-i3-bridge/02-02-SUMMARY.md` + diff --git a/.planning/phases/02-daemon-et-i3-bridge/02-03-PLAN.md b/.planning/phases/02-daemon-et-i3-bridge/02-03-PLAN.md new file mode 100644 index 0000000..08f71b2 --- /dev/null +++ b/.planning/phases/02-daemon-et-i3-bridge/02-03-PLAN.md @@ -0,0 +1,265 @@ +--- +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` +