386 lines
17 KiB
Markdown
386 lines
17 KiB
Markdown
# 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 |
|
|
|
|
## Recommended Project Structure
|
|
|
|
```
|
|
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`.
|
|
|
|
```go
|
|
// 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` :
|
|
|
|
```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
|
|
|
|
- [i3 IPC documentation](https://i3wm.org/docs/ipc.html)
|
|
- [go.i3wm.org/i3/v4 - Official Go library](https://pkg.go.dev/go.i3wm.org/i3/v4)
|
|
- [Claude Code Hooks reference](https://code.claude.com/docs/en/hooks)
|
|
- [claude-sessions-monitor](https://github.com/yepzdk/claude-sessions-monitor) - Existing tool, validates the JSONL + process detection approach
|
|
- [Real-time dashboard for Claude Code sessions](https://www.ksred.com/managing-multiple-claude-code-sessions-building-a-real-time-dashboard/)
|
|
- [Claude Code session storage](https://milvus.io/blog/why-claude-code-feels-so-stable-a-developers-deep-dive-into-its-local-storage-design.md)
|
|
- [fsnotify - Go filesystem notifications](https://github.com/fsnotify/fsnotify) - Considered but hooks are superior
|
|
- [Claude Code CLI reference](https://code.claude.com/docs/en/cli-reference)
|
|
|
|
---
|
|
*Architecture research for: vmux (Claude Code session cockpit + i3 integration)*
|
|
*Researched: 2026-03-23*
|