From 4916a750118de518cd4d68cbeda287b52855fadb Mon Sep 17 00:00:00 2001 From: Pierre Martin Date: Mon, 23 Mar 2026 13:09:19 +0100 Subject: [PATCH] docs(01-session-discovery): create phase plan --- .planning/ROADMAP.md | 6 +- .../phases/01-session-discovery/01-01-PLAN.md | 223 +++++++++ .../phases/01-session-discovery/01-02-PLAN.md | 423 ++++++++++++++++++ 3 files changed, 649 insertions(+), 3 deletions(-) create mode 100644 .planning/phases/01-session-discovery/01-01-PLAN.md create mode 100644 .planning/phases/01-session-discovery/01-02-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index ee6c99e..a5d9314 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -28,11 +28,11 @@ Decimal phases appear between their surrounding integers in numeric order. 2. Chaque session affiche son cwd, worktree git et branche git 3. Chaque session affiche son etat : Working, Needs Input ou Idle 4. Chaque session affiche un apercu des dernieres lignes de sortie -**Plans**: TBD +**Plans**: 2 plans Plans: -- [ ] 01-01: TBD -- [ ] 01-02: TBD +- [ ] 01-01-PLAN.md — Scaffolding projet Go + detection des processus Claude via /proc +- [ ] 01-02-PLAN.md — Parsing JSONL, heuristique d'etat et CLI vmux list ### Phase 2: Daemon et i3 Bridge **Goal**: L'utilisateur peut switcher vers n'importe quelle session Claude Code en une action via son workspace i3 diff --git a/.planning/phases/01-session-discovery/01-01-PLAN.md b/.planning/phases/01-session-discovery/01-01-PLAN.md new file mode 100644 index 0000000..c8243f4 --- /dev/null +++ b/.planning/phases/01-session-discovery/01-01-PLAN.md @@ -0,0 +1,223 @@ +--- +phase: 01-session-discovery +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - shell.nix + - go.mod + - types.go + - proc.go + - proc_test.go +autonomous: true +requirements: + - DISC-01 + - DISC-02 + +must_haves: + truths: + - "vmux detecte les processus Claude Code actifs via /proc" + - "vmux lit le cwd de chaque processus via /proc/PID/cwd" + - "vmux encode le cwd en nom de dossier ~/.claude/projects/" + artifacts: + - path: "shell.nix" + provides: "Environnement de dev NixOS avec Go" + contains: "go" + - path: "go.mod" + provides: "Module Go" + contains: "module github.com/pieMusic/vmux" + - path: "types.go" + provides: "Types partages (Process, Session, SessionState)" + exports: ["Process", "Session", "SessionState", "Working", "NeedsInput", "Idle"] + - path: "proc.go" + provides: "Scan /proc pour les processus Claude" + exports: ["FindClaudeProcesses", "EncodePath"] + - path: "proc_test.go" + provides: "Tests unitaires pour proc.go" + contains: "TestFindClaudeProcesses" + key_links: + - from: "proc.go" + to: "/proc/*/cmdline" + via: "os.ReadFile + strings.Split sur null bytes" + pattern: "ReadFile.*cmdline" + - from: "proc.go" + to: "/proc/*/cwd" + via: "os.Readlink" + pattern: "Readlink.*cwd" +--- + + +Scaffolding du projet Go et detection des processus Claude Code via /proc. + +Purpose: Poser les fondations (module Go, types, environnement NixOS) et implementer la brique de base : trouver les processus Claude actifs avec leur cwd. +Output: Un module Go fonctionnel avec `FindClaudeProcesses()` teste qui retourne les PID + cwd des processus Claude. + + + +@$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/01-session-discovery/01-CONTEXT.md +@.planning/phases/01-session-discovery/01-RESEARCH.md + + + + + + Task 1: Scaffolding projet + types partages + shell.nix, go.mod, types.go + + .planning/phases/01-session-discovery/01-RESEARCH.md + + +Creer 3 fichiers a la racine du projet : + +**shell.nix** : environnement NixOS avec `go` et `gopls` : +```nix +{ pkgs ? import {} }: +pkgs.mkShell { + buildInputs = with pkgs; [ go gopls ]; +} +``` + +**go.mod** : initialiser le module Go : +``` +module github.com/pieMusic/vmux + +go 1.25 +``` + +**types.go** (package main) : definir les types partages utilises par tous les autres fichiers : + +```go +package main + +type SessionState int + +const ( + Working SessionState = iota + NeedsInput + Idle + Unknown +) + +func (s SessionState) String() string // "Working", "Needs Input", "Idle", "Unknown" + +type Process struct { + PID int + Cmd []string // arguments cmdline + Cwd string // readlink /proc/PID/cwd +} + +type Session struct { + Process Process + SessionID string + GitBranch string + State SessionState + Preview string // dernieres lignes de sortie + CwdPath string // cwd du processus + Worktree string // git worktree (peut differer du cwd) +} +``` + +Le `SessionState` a 4 valeurs : `Working`, `NeedsInput`, `Idle`, `Unknown`. +`Unknown` est utilise quand aucun JSONL n'est trouve pour un processus (per D-04, pitfall 4 de la recherche). + + + cd /home/pierre/Code/vibe/vmux && nix-shell --run "go build ./..." + + + - shell.nix contient `go` et `gopls` dans buildInputs + - go.mod contient `module github.com/pieMusic/vmux` + - types.go contient `type SessionState int` + - types.go contient `Working SessionState = iota` + - types.go contient `NeedsInput` + - types.go contient `Idle` + - types.go contient `Unknown` + - types.go contient `type Process struct` + - types.go contient `type Session struct` + - `go build ./...` compile sans erreur + + Le projet compile. Les types Process, Session et SessionState sont definis et exportes. + + + + Task 2: Scan /proc + encoding chemin + tests + proc.go, proc_test.go + + types.go + .planning/phases/01-session-discovery/01-RESEARCH.md + + + - TestFindClaudeProcesses: avec un faux /proc contenant 3 repertoires (2 processus "claude", 1 processus "bash"), retourne exactement 2 Process avec les bons PID, Cmd et Cwd. + - TestFindClaudeProcesses_PermissionDenied: un /proc/PID/cmdline illisible (permission denied) est ignore silencieusement, pas de crash. + - TestFindClaudeProcesses_EmptyProc: un /proc vide retourne un slice vide sans erreur. + - TestEncodePath: "/home/pierre/Code/vibe/vmux" -> "-home-pierre-Code-vibe-vmux" + - TestEncodePath_DotDir: "/home/pierre/.config/app" -> "-home-pierre--config-app" (les `.` sont remplaces par `-`, d'ou le double dash) + + +**proc.go** (package main) : + +`FindClaudeProcesses(procDir string) ([]Process, error)` : +- Accepte le chemin vers /proc en parametre (permet le test avec un faux /proc). En production, passer "/proc". +- Lire `os.ReadDir(procDir)`, filtrer les entrees dont le nom est numerique (PID). +- Pour chaque PID, lire `procDir/PID/cmdline` via `os.ReadFile`. Le contenu est separe par des null bytes (`\x00`). Splitter avec `strings.Split(strings.TrimRight(data, "\x00"), "\x00")`. +- Si le premier argument (index 0) se termine par `claude` ou est exactement `claude`, c'est un processus Claude Code (per D-01). +- Lire le cwd via `os.Readlink(procDir/PID/cwd)` (per D-01). +- Ignorer silencieusement les erreurs de lecture (permission denied, processus disparu) : `continue` dans la boucle. +- Retourner un `[]Process` avec PID (int parse du nom de dossier), Cmd ([]string des arguments) et Cwd (string du readlink). + +`EncodePath(path string) string` : +- Remplacer tous les `/` par `-` et tous les `.` par `-` (per RESEARCH Pattern 2, pitfall 6). +- Utiliser `strings.NewReplacer("/", "-", ".", "-").Replace(path)`. + +**proc_test.go** (package main) : + +Utiliser des fixtures temporaires pour simuler /proc (per strategie TDD de la recherche) : +- `createFakeProc(t *testing.T, entries map[int]fakeProcEntry) string` : cree un repertoire temporaire avec la structure /proc simulee. +- `fakeProcEntry` : struct avec `cmdline string` (contenu brut avec \x00) et `cwd string` (chemin cible du symlink). +- Creer des symlinks pour `cwd` (`os.Symlink`). +- Creer des fichiers pour `cmdline` (`os.WriteFile`). + +5 tests decrits dans la section behavior. + + + cd /home/pierre/Code/vibe/vmux && nix-shell --run "go test -v -run 'TestFindClaudeProcesses|TestEncodePath' ./..." + + + - proc.go contient `func FindClaudeProcesses(procDir string) ([]Process, error)` + - proc.go contient `func EncodePath(path string) string` + - proc_test.go contient `TestFindClaudeProcesses` qui cree un faux /proc + - proc_test.go contient `TestEncodePath` + - proc_test.go contient `TestEncodePath_DotDir` + - Tous les tests passent avec `go test -v -run 'TestFindClaudeProcesses|TestEncodePath' ./...` + - FindClaudeProcesses accepte un parametre procDir (pas de /proc en dur) + + FindClaudeProcesses detecte les processus Claude dans un faux /proc. EncodePath convertit correctement les chemins. Tous les tests passent. + + + + + +- `go build ./...` compile sans erreur +- `go test -v ./...` : tous les tests passent +- `go vet ./...` : pas de warning + + + +- Le projet Go est initialise avec shell.nix et go.mod +- Les types Process, Session, SessionState sont definis +- FindClaudeProcesses retourne les processus Claude depuis /proc +- EncodePath encode les chemins en noms de dossiers ~/.claude/projects/ +- Tests unitaires couvrent les cas normaux et les cas limites + + + +After completion, create `.planning/phases/01-session-discovery/01-01-SUMMARY.md` + diff --git a/.planning/phases/01-session-discovery/01-02-PLAN.md b/.planning/phases/01-session-discovery/01-02-PLAN.md new file mode 100644 index 0000000..e89d783 --- /dev/null +++ b/.planning/phases/01-session-discovery/01-02-PLAN.md @@ -0,0 +1,423 @@ +--- +phase: 01-session-discovery +plan: 02 +type: execute +wave: 2 +depends_on: + - 01-01 +files_modified: + - session.go + - session_test.go + - state.go + - state_test.go + - display.go + - display_test.go + - main.go +autonomous: true +requirements: + - DISC-03 + - STATE-01 + - STATE-02 + +must_haves: + truths: + - "vmux affiche la branche git de chaque session" + - "vmux detecte si une session travaille, attend input ou est idle" + - "vmux affiche un apercu des dernieres lignes de sortie" + - "vmux list affiche toutes les sessions avec leur etat, cwd, branche et apercu" + artifacts: + - path: "session.go" + provides: "Matching PID->JSONL, lecture metadonnees JSONL" + exports: ["FindSessionForProcess", "TailReadJSONL"] + - path: "state.go" + provides: "Heuristique d'etat basee sur le dernier message JSONL" + exports: ["DetectState", "ExtractPreview"] + - path: "display.go" + provides: "Formatage et affichage CLI avec couleurs" + exports: ["DisplaySessions"] + - path: "main.go" + provides: "Point d'entree CLI vmux list" + contains: "func main" + key_links: + - from: "session.go" + to: "~/.claude/projects//*.jsonl" + via: "EncodePath + filepath.Glob" + pattern: "EncodePath.*Glob" + - from: "state.go" + to: "session.go" + via: "TailReadJSONL fournit les messages au DetectState" + pattern: "DetectState.*JSONLMessage" + - from: "main.go" + to: "proc.go + session.go + state.go + display.go" + via: "Pipeline FindClaudeProcesses -> FindSessionForProcess -> DetectState -> DisplaySessions" + pattern: "FindClaudeProcesses.*FindSession.*DetectState.*Display" +--- + + +Parsing JSONL, heuristique d'etat et CLI `vmux list` fonctionnel. + +Purpose: Completer le pipeline de detection : pour chaque processus Claude trouve en Plan 01, trouver le JSONL correspondant, determiner l'etat, extraire un apercu, et afficher le tout. +Output: `vmux list` fonctionnel qui affiche toutes les sessions Claude Code avec etat, cwd, branche git et apercu. + + + +@$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/01-session-discovery/01-CONTEXT.md +@.planning/phases/01-session-discovery/01-RESEARCH.md +@.planning/phases/01-session-discovery/01-01-SUMMARY.md + + + + +From types.go: +```go +package main + +type SessionState int +const ( + Working SessionState = iota + NeedsInput + Idle + Unknown +) +func (s SessionState) String() string + +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 +``` + + + + + + + Task 1: Matching PID->JSONL + tail-read + heuristique d'etat + session.go, session_test.go, state.go, state_test.go + + types.go + proc.go + .planning/phases/01-session-discovery/01-RESEARCH.md + + + - TestTailReadJSONL: fichier JSONL de 50 lignes, lire les 3 dernieres -> retourne exactement 3 messages JSON valides. + - TestTailReadJSONL_TruncatedLastLine: JSONL dont la derniere ligne est incomplete (pas de \n final) -> ignore la ligne tronquee, retourne les lignes completes precedentes. + - TestTailReadJSONL_EmptyFile: fichier vide -> retourne slice vide sans erreur. + - TestTailReadJSONL_SmallFile: fichier de 2 lignes, demander 10 -> retourne les 2 lignes. + - TestFindSessionForProcess: dossier ~/.claude/projects/ simule avec 2 fichiers JSONL, retourne celui avec le mtime le plus recent. + - TestFindSessionForProcess_NoMatch: aucun dossier correspondant -> retourne erreur. + - TestDetectState_EndTurnText: message assistant avec stop_reason="end_turn" et content type "text" -> NeedsInput. + - TestDetectState_ToolUseAskUser: message assistant avec stop_reason="tool_use" et tool name "AskUserQuestion" -> NeedsInput. + - TestDetectState_ToolUseOther: message assistant avec stop_reason="tool_use" et tool name "Read" -> Working. + - TestDetectState_Progress: message type="progress" avec data.type="agent_progress" -> Working. + - TestDetectState_ToolResult: message type="user" avec content type "tool_result" -> Working. + - TestDetectState_IdleThreshold: message timestamp > 60s dans le passe -> Idle. + - TestExtractPreview: message assistant avec content text "Voici le resultat..." -> retourne les 3 premieres lignes du texte. + + +**session.go** (package main) : + +Structure intermediaire pour le parsing JSONL : +```go +type JSONLMessage struct { + Type string `json:"type"` // "assistant", "user", "progress", "system" + Timestamp string `json:"timestamp"` // ISO 8601 + SessionID string `json:"sessionId"` + Cwd string `json:"cwd"` + GitBranch string `json:"gitBranch"` + Message *MessagePayload `json:"message,omitempty"` + Data *ProgressData `json:"data,omitempty"` +} + +type MessagePayload struct { + Role string `json:"role"` + Content []ContentBlock `json:"content"` + StopReason string `json:"stop_reason"` +} + +type ContentBlock struct { + Type string `json:"type"` // "text", "tool_use", "tool_result" + Text string `json:"text,omitempty"` + Name string `json:"name,omitempty"` // pour tool_use : nom de l'outil +} + +type ProgressData struct { + Type string `json:"type"` // "agent_progress", "hook_progress" +} +``` + +`TailReadJSONL(path string, n int) ([]JSONLMessage, error)` : +- Ouvrir le fichier, `Seek(0, io.SeekEnd)` pour obtenir la taille. +- Lire des blocs de 8192 octets en reculant depuis la fin. +- Accumuler les lignes completes (terminees par `\n`). Ignorer la derniere ligne si elle ne se termine pas par `\n` (per pitfall 3 : race condition). +- Arreter quand on a `n` lignes completes ou qu'on atteint le debut du fichier. +- Parser chaque ligne en `JSONLMessage` via `json.Unmarshal`. Ignorer les lignes qui ne parsent pas (lignes vides, corruptions). +- Retourner les messages dans l'ordre chronologique (le plus ancien en premier). + +`FindSessionForProcess(claudeDir string, proc Process) (string, []JSONLMessage, error)` : +- `claudeDir` = chemin vers `~/.claude/projects/` (parametre pour testabilite). En production : `os.Getenv("HOME") + "/.claude/projects/"`. +- Encoder `proc.Cwd` via `EncodePath(proc.Cwd)` pour obtenir le nom du dossier. +- Lister les `*.jsonl` dans `claudeDir//` (PAS les sous-dossiers, per pitfall 5 : exclure subagents/). +- Utiliser `filepath.Glob(filepath.Join(claudeDir, encoded, "*.jsonl"))` puis filtrer ceux qui sont dans un sous-dossier. +- Trier par mtime (le plus recent en premier). Prendre le premier = session active (per RESEARCH Pattern 5). +- Appeler `TailReadJSONL(path, 5)` sur le JSONL trouve. +- Extraire `sessionId` et `gitBranch` depuis la premiere entree retournee. +- Retourner le chemin JSONL, les messages, et nil. Si aucun JSONL trouve, retourner une erreur. + +**state.go** (package main) : + +`DetectState(messages []JSONLMessage, now time.Time) SessionState` : +- Si `messages` est vide, retourner `Unknown`. +- Prendre le dernier message (index len-1). +- Si le timestamp du dernier message est > 60 secondes avant `now` -> `Idle` (const `IdleThreshold = 60 * time.Second`). +- Si `type == "assistant"` et `message.stop_reason == "end_turn"` -> `NeedsInput` (per D-05 : Claude a fini, attend l'utilisateur). +- Si `type == "assistant"` et `message.stop_reason == "tool_use"` : + - Scanner `message.content` pour un `ContentBlock` de type `"tool_use"`. + - Si `name == "AskUserQuestion"` -> `NeedsInput` (per D-05). + - Sinon -> `Working` (outil en cours d'execution, per D-05). +- Si `type == "progress"` -> `Working` (per D-05 : hook ou subagent en cours). +- Si `type == "user"` et un ContentBlock a `type == "tool_result"` -> `Working` (resultat envoye, Claude va repondre). +- Sinon -> `Unknown`. + +Le parametre `now time.Time` permet de tester le seuil Idle sans dependre de l'horloge. + +`ExtractPreview(messages []JSONLMessage) string` : +- Parcourir les messages en ordre inverse pour trouver le dernier `type == "assistant"` avec un `ContentBlock` de type `"text"`. +- Extraire le champ `text` de ce ContentBlock. +- Tronquer a 3 lignes (split sur `\n`, prendre les 3 premieres, rejoin). +- Si le texte depasse 200 caracteres, tronquer a 200 et ajouter "...". +- Si aucun texte trouve, retourner "" (vide). + +**session_test.go** et **state_test.go** : + +Utiliser des fixtures JSONL dans des fichiers temporaires. Pour chaque test : +- Creer un `t.TempDir()`. +- Ecrire des lignes JSON valides representant les cas decrits dans behavior. +- Les fixtures JSONL doivent reproduire le format exact documente dans RESEARCH (voir section "Code Examples"). + +Exemple de fixture pour un message assistant end_turn : +```json +{"type":"assistant","timestamp":"2026-03-23T12:00:00Z","sessionId":"abc","cwd":"/tmp","gitBranch":"main","message":{"role":"assistant","content":[{"type":"text","text":"Done."}],"stop_reason":"end_turn"}} +``` + + + cd /home/pierre/Code/vibe/vmux && nix-shell --run "go test -v -run 'TestTailRead|TestFindSession|TestDetectState|TestExtractPreview' ./..." + + + - session.go contient `func TailReadJSONL(path string, n int) ([]JSONLMessage, error)` + - session.go contient `func FindSessionForProcess(claudeDir string, proc Process) (string, []JSONLMessage, error)` + - state.go contient `func DetectState(messages []JSONLMessage, now time.Time) SessionState` + - state.go contient `func ExtractPreview(messages []JSONLMessage) string` + - state.go contient `IdleThreshold` comme constante a 60 secondes + - session_test.go contient `TestTailReadJSONL` et `TestFindSessionForProcess` + - state_test.go contient `TestDetectState_EndTurnText`, `TestDetectState_ToolUseAskUser`, `TestDetectState_ToolUseOther`, `TestDetectState_IdleThreshold` + - state_test.go contient `TestExtractPreview` + - Tous les tests passent + + TailReadJSONL lit les N derniers messages sans charger le fichier entier. FindSessionForProcess fait le matching PID->JSONL. DetectState applique l'heuristique d'etat. ExtractPreview extrait un apercu. Tous les tests passent. + + + + Task 2: Affichage CLI + main.go + test d'integration + display.go, display_test.go, main.go + + types.go + proc.go + session.go + state.go + + +**display.go** (package main) : + +Constantes ANSI (per D-07, pas de lib externe) : +```go +const ( + colorReset = "\033[0m" + colorGreen = "\033[32m" // Working + colorYellow = "\033[33m" // Needs Input + colorRed = "\033[31m" // Needs Input (AskUserQuestion) + colorGray = "\033[90m" // Idle / Unknown + colorBold = "\033[1m" +) +``` + +`DisplaySessions(w io.Writer, sessions []Session, noColor bool)` : +- Si `sessions` est vide, ecrire "No active Claude Code sessions found.\n" et retourner. +- Pour chaque session, afficher un bloc : +``` +[STATE] cwd (branch) + Preview text here... +``` +- Le `[STATE]` est colore selon l'etat (sauf si `noColor == true`) : + - Working -> colorGreen + "Working" + - NeedsInput -> colorYellow + "Needs Input" + - Idle -> colorGray + "Idle" + - Unknown -> colorGray + "Unknown" +- Le `cwd` est le `Session.CwdPath`. Afficher aussi le `Worktree` si different du cwd. +- Le `(branch)` est `Session.GitBranch`. Si vide, omettre les parentheses. +- Le `Preview` est indente de 9 espaces (alignement avec le texte apres [STATE]). Chaque ligne du preview est prefixee de ces espaces. +- Separer les sessions par une ligne vide. +- Ecrire dans `w` (io.Writer) pour la testabilite. En production : `os.Stdout`. + +`stateColor(state SessionState, noColor bool) string` : helper qui retourne le code ANSI pour un etat, ou "" si noColor. + +**display_test.go** (package main) : + +- `TestDisplaySessions_WithSessions` : 2 sessions (Working + NeedsInput), verifier que la sortie contient les cwds, branches et etats. +- `TestDisplaySessions_NoColor` : meme test avec noColor=true, verifier qu'aucun code ANSI n'est present (pas de `\033`). +- `TestDisplaySessions_Empty` : sessions vide -> "No active Claude Code sessions found." + +Utiliser un `bytes.Buffer` comme io.Writer pour capturer la sortie. + +**main.go** (package main) : + +```go +func main() { + noColor := flag.Bool("no-color", false, "Disable colored output") + flag.Parse() + + // Aussi detecter NO_COLOR env var (convention standard) + if os.Getenv("NO_COLOR") != "" { + *noColor = true + } + + args := flag.Args() + if len(args) == 0 || args[0] != "list" { + fmt.Fprintf(os.Stderr, "Usage: vmux list [--no-color]\n") + os.Exit(1) + } + + processes, err := FindClaudeProcesses("/proc") + if err != nil { + fmt.Fprintf(os.Stderr, "Error scanning processes: %v\n", err) + os.Exit(1) + } + + claudeDir := filepath.Join(os.Getenv("HOME"), ".claude", "projects") + now := time.Now() + var sessions []Session + + for _, proc := range processes { + jsonlPath, messages, err := FindSessionForProcess(claudeDir, proc) + if err != nil { + // Processus sans JSONL -> afficher avec etat Unknown (per pitfall 4) + sessions = append(sessions, Session{ + Process: proc, + State: Unknown, + CwdPath: proc.Cwd, + }) + continue + } + + state := DetectState(messages, now) + preview := ExtractPreview(messages) + + // Extraire metadonnees du JSONL + var sessionID, gitBranch, worktree string + for _, msg := range messages { + if msg.SessionID != "" { + sessionID = msg.SessionID + } + if msg.GitBranch != "" { + gitBranch = msg.GitBranch + } + } + + // Worktree = git rev-parse --show-toplevel depuis le cwd + worktree = resolveWorktree(proc.Cwd) + + _ = jsonlPath // utilise pour le debug futur + + sessions = append(sessions, Session{ + Process: proc, + SessionID: sessionID, + GitBranch: gitBranch, + State: state, + Preview: preview, + CwdPath: proc.Cwd, + Worktree: worktree, + }) + } + + DisplaySessions(os.Stdout, sessions, *noColor) +} + +// resolveWorktree utilise git pour trouver le worktree. +// Retourne le cwd si git echoue (pas un repo git). +func resolveWorktree(cwd string) string { + cmd := exec.Command("git", "-C", cwd, "rev-parse", "--show-toplevel") + out, err := cmd.Output() + if err != nil { + return cwd + } + return strings.TrimSpace(string(out)) +} +``` + +Imports necessaires : `flag`, `fmt`, `os`, `os/exec`, `path/filepath`, `strings`, `time`. + + + cd /home/pierre/Code/vibe/vmux && nix-shell --run "go test -v ./... && go build -o vmux ./..." + + + - display.go contient `func DisplaySessions(w io.Writer, sessions []Session, noColor bool)` + - display.go contient les constantes ANSI colorGreen, colorYellow, colorGray + - display_test.go contient `TestDisplaySessions_WithSessions` + - display_test.go contient `TestDisplaySessions_NoColor` + - display_test.go contient `TestDisplaySessions_Empty` + - main.go contient `func main()` avec `flag.Parse()` et sous-commande "list" + - main.go contient `FindClaudeProcesses("/proc")` + - main.go contient `resolveWorktree` qui appelle `git rev-parse --show-toplevel` + - main.go gere le flag `--no-color` et la variable `NO_COLOR` + - `go build -o vmux ./...` produit un binaire vmux + - `go test -v ./...` : tous les tests passent (proc, session, state, display) + + vmux list fonctionne : detecte les processus Claude, lit les JSONL, determine l'etat, affiche le resultat avec couleurs. Le binaire compile. Tous les tests passent. + + + + + +- `go test -v -race ./...` : tous les tests passent sans race condition +- `go build -o vmux ./...` : le binaire compile +- `./vmux list` : affiche les sessions Claude Code actives sur le poste (test manuel) +- `./vmux list --no-color` : meme sortie sans codes ANSI + + + +- `vmux list` affiche toutes les sessions Claude Code actives (DISC-01) +- Chaque session affiche son cwd et worktree git (DISC-02) +- Chaque session affiche sa branche git (DISC-03) +- Chaque session affiche son etat Working/Needs Input/Idle (STATE-01) +- Chaque session affiche un apercu de la derniere sortie (STATE-02) +- Couleurs actives par defaut, desactivables avec --no-color (D-07) + + + +After completion, create `.planning/phases/01-session-discovery/01-02-SUMMARY.md` +