Files
vmux/.planning/research/ARCHITECTURE.md
2026-03-23 11:25:05 +01:00

17 KiB

Architecture Research

Domain: Process monitoring + desktop integration (Claude Code sessions + i3 WM) Researched: 2026-03-23 Confidence: HIGH

System Overview

┌─────────────────────────────────────────────────────────────────┐
│                        Presentation Layer                       │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────┐  │
│  │  TUI (Charm) │  │ i3bar/polybar│  │ Desktop Notifications│  │
│  │  Dashboard    │  │  Status Line │  │  (notify-send)       │  │
│  └──────┬───────┘  └──────┬───────┘  └──────────┬───────────┘  │
│         │                 │                      │              │
├─────────┴─────────────────┴──────────────────────┴──────────────┤
│                        Core Daemon (vmuxd)                      │
│  ┌────────────┐ ┌──────────────┐ ┌────────────┐ ┌───────────┐  │
│  │  Session    │ │   State      │ │  i3        │ │ Event     │  │
│  │  Registry   │ │   Machine    │ │  Bridge    │ │ Bus       │  │
│  └─────┬──────┘ └──────┬───────┘ └─────┬──────┘ └─────┬─────┘  │
│        │               │               │              │         │
├────────┴───────────────┴───────────────┴──────────────┴─────────┤
│                        Data Sources                             │
│  ┌───────────────┐  ┌────────────────┐  ┌───────────────────┐   │
│  │ Claude Hooks  │  │ ~/.claude/     │  │ i3 IPC Socket     │   │
│  │ (HTTP POST)   │  │ projects/      │  │ (Unix Domain)     │   │
│  └───────────────┘  └────────────────┘  └───────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

Architecture Decision: Hook-First, File-Watch Fallback

La decouverte cle de cette recherche : Claude Code expose un systeme de hooks HTTP depuis debut 2026. Un hook HTTP permet a chaque session Claude Code de POSTer ses evenements (Stop, Notification, PreToolUse, PostToolUse) vers un serveur local. Cela change fondamentalement l'architecture.

Strategie retenue : Push via hooks + Pull via fichiers JSONL

  • Push (primary): Chaque session Claude Code envoie ses changements d'etat au daemon via HTTP hooks. Pas de polling, pas de latence.
  • Pull (fallback): Pour les sessions existantes au demarrage du daemon, ou si les hooks ne sont pas configures, on lit les fichiers JSONL de ~/.claude/projects/.

Cette approche est superieure au file-watching pur (fsnotify) car :

  1. Les hooks fournissent des donnees structurees (session_id, cwd, transcript_path, tool_name)
  2. Pas de parsing JSONL en continu
  3. Detection immediate du changement d'etat (vs polling toutes les N secondes)

Component Responsibilities

Component Responsibility Implementation
vmuxd (daemon) Processus central, orchestre tout Go binary, lance au login (systemd user unit)
HTTP Hook Server Recoit les evenements Claude Code net/http sur localhost:7483, endpoints par event type
Session Registry Maintient la liste des sessions actives + leur etat Map en memoire, pas de DB necessaire
State Machine Determine l'etat d'une session (Working / Needs Input / Waiting / Inactive) Transitions basees sur les hooks recus
i3 Bridge Associe sessions aux workspaces, switch workspace go.i3wm.org/i3/v4 (lib officielle)
Event Bus Distribue les changements d'etat aux consumers (TUI, notifications) Channels Go internes
JSONL Scanner Scan initial + fallback pour sessions sans hooks Lecture ~/.claude/projects/ au demarrage
TUI Interface principale, dashboard des sessions Charm Bubbletea ou simple ANSI output
Notifier Alerte quand une session passe a "Needs Input" notify-send / libnotify
vmux/
├── cmd/
│   ├── vmuxd/          # Daemon principal
│   │   └── main.go
│   └── vmux/           # CLI client (query le daemon)
│       └── main.go
├── internal/
│   ├── daemon/         # Lifecycle du daemon, signal handling
│   │   └── daemon.go
│   ├── hooks/          # HTTP server pour recevoir les hooks Claude
│   │   ├── server.go
│   │   └── handlers.go
│   ├── session/        # Session registry + state machine
│   │   ├── registry.go
│   │   ├── state.go
│   │   └── session.go
│   ├── scanner/        # JSONL file scanner (fallback)
│   │   └── scanner.go
│   ├── i3bridge/       # Integration i3 WM
│   │   ├── bridge.go
│   │   └── workspace.go
│   ├── notify/         # Desktop notifications
│   │   └── notify.go
│   └── events/         # Event bus interne
│       └── bus.go
├── go.mod
├── go.sum
└── flake.nix           # NixOS packaging

Structure Rationale

  • cmd/ split (vmuxd + vmux): Le daemon tourne en permanence, le CLI est ephemere (query/switch). Separation nette.
  • internal/: Tout est interne, pas de lib publique a exposer.
  • hooks/ separe de scanner/: Deux sources de donnees distinctes avec des lifecycles differents.
  • i3bridge/ isole: La dependance a i3 est contenue. Si Pierre migre vers Sway un jour, seul ce package change (Sway utilise le meme protocole IPC).

Architectural Patterns

Pattern 1: Event-Driven State Machine

What: Chaque session Claude Code est modelisee comme une state machine. Les transitions sont declenchees par les hooks HTTP ou le scan JSONL.

States:

                    ┌──────────┐
         hook:Stop  │          │  hook:PreToolUse
        ┌──────────>│ Waiting  │<──────────┐
        │           │          │           │
        │           └────┬─────┘           │
        │                │                 │
        │    hook:Notification             │
        │    (idle_prompt)                 │
        │                │                 │
        │           ┌────v─────┐      ┌────┴──────┐
        │           │  Needs   │      │  Working  │
        │           │  Input   │      │           │
        │           └──────────┘      └───────────┘
        │                                  ^
        │           ┌──────────┐           │
        │           │ Inactive │───────────┘
        │           │          │  hook:SessionStart
        │           └──────────┘  ou process detecte
        │                ^
        └────────────────┘
          timeout / process mort

Trade-offs: Simple et previsible. Le risque est de manquer un evenement (hook timeout, daemon restart). La mitigation est le scan JSONL periodique (toutes les 30s) qui reconcilie l'etat.

Pattern 2: Daemon + CLI Client via Unix Socket

What: vmuxd expose un socket Unix local. vmux (CLI) s'y connecte pour query/commander.

When to use: Pour toutes les interactions utilisateur : lister les sessions, switcher de workspace, afficher le dashboard.

Trade-offs: Plus propre qu'un HTTP API local pour du tooling CLI. Le socket est dans $XDG_RUNTIME_DIR/vmux.sock.

// vmux CLI query
conn, _ := net.Dial("unix", "/run/user/1000/vmux.sock")
json.NewEncoder(conn).Encode(Request{Action: "list"})
json.NewDecoder(conn).Decode(&sessions)

Pattern 3: i3 Workspace Mapping via Window Tree

What: Associer une session Claude Code a un workspace i3 en matchant le PID du processus Claude avec les fenetres i3 (via GetTree()).

When to use: Au demarrage et quand un hook arrive avec un cwd inconnu.

Mapping strategy:

  1. Le hook fournit cwd (ex: /home/pierre/Code/front-commerce/worktrees/feature-x)
  2. GetTree() retourne l'arbre des fenetres avec leur PID
  3. On matche le cwd du hook avec le cwd du processus terminal (via /proc/PID/cwd)
  4. La fenetre terminale est dans un workspace i3 => association trouvee

Trade-offs: La resolution via /proc/PID/cwd est Linux-specific (OK pour NixOS). Le matching peut echouer si le terminal a change de repertoire depuis.

Data Flow

Flow 1: Hook-Driven State Update (primary path)

Claude Code session (hook: Stop)
    │
    │  HTTP POST localhost:7483/hooks/stop
    │  Body: { session_id, cwd, last_assistant_message, transcript_path }
    │
    v
HTTP Hook Server
    │
    │  Parse, validate, extract session_id + cwd
    │
    v
Session Registry
    │
    │  Update session state: Working -> Waiting
    │  Resolve i3 workspace (if not cached)
    │
    v
Event Bus
    │
    ├──> Notifier: "Session X attend dans workspace 3"
    ├──> TUI: refresh dashboard
    └──> i3bar: update status line

Flow 2: Initial Discovery (startup)

vmuxd starts
    │
    ├──> Scan ~/.claude/projects/*/          # Find JSONL files
    │    Parse last N lines of each          # Determine state
    │    Check /proc for matching processes   # Alive or dead?
    │
    ├──> i3.GetTree()                         # Map windows to workspaces
    │    i3.GetWorkspaces()                   # List workspace names
    │
    └──> Build Session Registry
         Emit initial state to Event Bus

Flow 3: Workspace Switch (user action)

User selects session in TUI (or `vmux switch <session>`)
    │
    v
vmux CLI ──> Unix Socket ──> vmuxd
    │
    v
Session Registry: lookup workspace for session
    │
    v
i3 Bridge: i3.RunCommand("workspace 3")
    │
    v
i3 focuses workspace 3

Flow 4: Notification on state change

Hook: Notification (type: permission_prompt)
    │
    v
State Machine: session -> Needs Input
    │
    v
Event Bus
    │
    v
Notifier: notify-send "Claude Code attend votre input"
          "--hint=string:x-canonical-private-synchronous:vmux"
          "Session: front-commerce/feature-x (workspace 3)"

Integration Points

External Services

Service Integration Pattern Notes
Claude Code HTTP hooks (Push) + JSONL files (Pull) Hooks configurees dans ~/.claude/settings.json
i3 WM go.i3wm.org/i3/v4 via Unix socket Subscribe WindowEvent + WorkspaceEvent pour mises a jour reactives
Desktop Notifications exec notify-send Ou libnotify via cgo si besoin de callbacks
piaire (futur) HTTP API ou Unix socket Integration possible mais hors scope v1

Internal Boundaries

Boundary Communication Notes
vmuxd <-> vmux CLI Unix socket, JSON protocol Unidirectionnel : CLI query, daemon repond
Hook Server <-> Session Registry Direct Go function call Meme processus, pas besoin d'IPC
Session Registry <-> Event Bus Go channels Fan-out vers multiple consumers
Event Bus <-> TUI/Notifier/i3bar Go channels (subscribe) Chaque consumer recoit tous les events, filtre localement
i3 Bridge <-> i3 WM Unix socket (i3 IPC protocol) Gere par la lib officielle, transparent

Claude Code Hook Configuration

A installer dans ~/.claude/settings.json :

{
  "hooks": {
    "Stop": [{
      "matcher": "",
      "hooks": [{
        "type": "http",
        "url": "http://localhost:7483/hooks/stop"
      }]
    }],
    "Notification": [{
      "matcher": "",
      "hooks": [{
        "type": "http",
        "url": "http://localhost:7483/hooks/notification"
      }]
    }],
    "PreToolUse": [{
      "matcher": "",
      "hooks": [{
        "type": "http",
        "url": "http://localhost:7483/hooks/pre-tool-use",
        "async": true
      }]
    }],
    "PostToolUse": [{
      "matcher": "",
      "hooks": [{
        "type": "http",
        "url": "http://localhost:7483/hooks/post-tool-use",
        "async": true
      }]
    }]
  }
}

Important: PreToolUse et PostToolUse en async: true pour ne pas ralentir Claude Code. Stop et Notification en synchrone (latence negligeable, et on veut la confirmation de reception).

Anti-Patterns

Anti-Pattern 1: Polling-Only Architecture

What people do: Scanner les fichiers JSONL toutes les secondes pour detecter les changements. Why it's wrong: Consommation CPU inutile, latence de 0-1s, parsing JSON couteux en boucle. Avec 5+ sessions, ca s'accumule. Do this instead: Hooks HTTP comme source primaire, scan JSONL uniquement au demarrage et comme reconciliation periodique (toutes les 30s).

Anti-Pattern 2: Embedded Terminal / Session Injection

What people do: Tenter de lire le PTY de la session Claude Code ou d'injecter des inputs. Why it's wrong: Fragile, security issues, peut corrompre la session. Le projet le reconnait deja (out of scope). Do this instead: Observer via hooks + JSONL. Le last_assistant_message du hook Stop fournit l'apercu necessaire.

Anti-Pattern 3: Fat Client Without Daemon

What people do: Un seul binaire CLI qui fait tout (scan, watch, display) a chaque invocation. Why it's wrong: Pas de persistance d'etat entre invocations. Chaque vmux doit re-scanner, re-parser, re-resoudre les workspaces. Lent et incapable de notifier. Do this instead: Daemon leger (vmuxd) qui maintient l'etat, CLI leger (vmux) qui query le daemon.

Anti-Pattern 4: Over-Engineering the IPC

What people do: gRPC, protobuf, message queues entre les composants internes. Why it's wrong: vmux est un outil local single-user. La complexite du serialization/deserialization est pure overhead. Do this instead: Go channels internes + Unix socket JSON pour le CLI. Simple, debuggable.

Build Order (Dependencies Between Components)

L'ordre de construction est dicte par les dependances :

Phase 1: Session Registry + JSONL Scanner
         (peut fonctionner standalone, valide le concept)
              │
Phase 2: HTTP Hook Server
         (depend du Registry, remplace le scanner comme source primaire)
              │
Phase 3: i3 Bridge + Workspace Mapping
         (depend du Registry pour associer sessions <-> workspaces)
              │
Phase 4: TUI Dashboard + CLI
         (depend de tout ce qui precede pour afficher et commander)
              │
Phase 5: Notifications + Polish
         (depend de l'Event Bus et du State Machine)

Rationale de l'ordre :

  1. Le Registry + Scanner valide que la detection fonctionne sans aucune config Claude Code
  2. Les hooks ajoutent la reactivite mais necessitent que le registry existe
  3. L'i3 bridge a besoin du registry peuple pour faire le mapping
  4. Le TUI/CLI est la couche de presentation, besoin de donnees
  5. Les notifications sont du polish, pas du core

Scaling Considerations

Scale Approach
1-5 sessions (usage normal) Architecture actuelle suffit largement
5-15 sessions Aucun changement, Go gere facilement
15+ sessions Improbable pour un seul utilisateur. Si ca arrive, limiter la frequence des hooks async

Le bottleneck n'est pas la perf mais la complexite d'affichage : au-dela de 8 sessions, le TUI doit proposer du filtrage/grouping.

Sources


Architecture research for: vmux (Claude Code session cockpit + i3 integration) Researched: 2026-03-23