Architecture design document for a development companion tool that opens Claude Code / tmux sessions per git worktree and allows chat operations from a smartphone browser.
This document defines the technical architecture of CommandMate.
- What processes/components make up the system
- How Claude CLI / tmux / Web UI cooperate
- Which responsibilities belong to which layer
It serves as a guide during implementation.
- Scope: The entire
CommandMateapplication- Next.js (App Router) based Web UI
- Node.js API / WebSocket server
- tmux + Claude CLI session management
- SQLite / local FS persistence
- Out of scope:
- Claude model behavior
- Specific integration implementations with other tools (mySwiftAgent / myAgentDesk, etc.)
Naming / Terminology:
- worktree: A branch directory managed as a git worktree
- worktreeId: A URL-safe identifier (e.g.,
feature-foo) - tmux session: A tmux session named
cw_{worktreeId} - Stop hook: A completion hook set via Claude CLI's
CLAUDE_HOOKS_STOP
- Manage independent Claude CLI sessions per git worktree
- Provide a per-branch chat UI accessible from smartphone or PC browsers
- Use Stop hooks for event-driven updates without polling for the latest responses
- Safely store conversation history / detailed logs locally
- Operating as a multi-user SaaS (this is for local developers)
- Public exposure across security boundaries / multi-tenant operation
- CLI Tool Support (Implemented in Issue #4, extended in Issue #368)
- Claude Code, Codex CLI, Gemini CLI, Vibe-Local (Ollama) support
- Extensible design via Strategy pattern
- Select 2 agents per worktree (
selected_agentscolumn) - Vibe-Local supports Ollama model selection (
vibe_local_modelcolumn)
- Runs on the developer's local machine
- macOS / Linux assumed (tmux / Claude CLI must be available)
- Claude CLI is installed and:
- Can be launched with the
claudecommand - Supports setting
CLAUDE_HOOKS_STOP
- Can be launched with the
- Multiple branch directories are managed via git worktree
- Local mode (default)
MCBD_BIND = 127.0.0.1- Connections from the same machine only
- LAN access mode (optional)
CM_BIND = 0.0.0.0- Reverse proxy authentication recommended (details:
docs/security-guide.md) - Accessible from smartphones on the same network
graph TD
subgraph Browser["Smartphone/PC Browser"]
UI_A[Screen A: Worktree List]
UI_B[Screen B: Chat Screen]
UI_C[Screen C: Log Viewer]
UI_WS[WebSocket Client]
end
subgraph Next["Next.js / Node.js Application"]
App["Next.js UI (App Router)"]
API_Send[/POST /api/worktrees/:id/send/]
API_WT[/GET /api/worktrees/ .../]
API_Hook[/POST /api/hooks/claude-done/]
API_Logs[/GET /api/worktrees/:id/logs/ .../]
WS_Server[WebSocket Server]
DB[(SQLite db.sqlite)]
end
subgraph TmuxClaude["tmux / Claude CLI"]
T[(tmux server)]
S1[tmux session: cw_main]
S2[tmux session: cw_feature-foo]
CL1[Claude CLI Process]
end
subgraph FS["Local Filesystem"]
Logs[".claude_logs/ (Markdown logs)"]
end
UI_A --> App
UI_B --> App
UI_C --> App
UI_WS <---> WS_Server
App --> API_WT
App --> API_Send
App --> API_Logs
API_Send --> T
API_Hook --> T
API_WT --> DB
API_Logs --> Logs
API_Hook --> DB
API_Hook --> Logs
API_Hook --> WS_Server
CL1 -->|"Stop hook"| API_Hook
- Next.js / Node.js process
- Port: MCBD_PORT (e.g., 3000)
- Bind: MCBD_BIND (127.0.0.1 or 0.0.0.0)
- Features:
- HTTP server (UI, API Routes)
- WebSocket server
- SQLite connection
- tmux server
- Uses the existing tmux server on the system
- Launches CLI tools in sessions named
cw_{worktreeId}
- CLI tool process (Claude Code / Codex CLI)
- The selected CLI tool runs within each tmux session
- Sends completion notifications via the command set in
CLAUDE_HOOKS_STOP(for Claude Code)
interface Worktree {
id: string; // URL-safe ID ("main", "feature-foo", etc.)
name: string; // Display name ("main", "feature/foo", etc.)
path: string; // Absolute path "/path/to/root/feature/foo"
lastMessageSummary?: string; // Summary of the last message
updatedAt?: Date; // Timestamp of last message
}- id: Identifier normalized to a URL-parameter-safe format
- Example:
feature/foo→feature-foo
- Example:
- path: Absolute path based on
MCBD_ROOT_DIR - updatedAt: Used for sorting the worktree list by last update time
type ChatRole = "user" | "claude";
interface ChatMessage {
id: string; // UUID
worktreeId: string; // Worktree.id
role: ChatRole;
content: string; // Full text for UI display
summary?: string; // Optional short summary
timestamp: Date;
logFileName?: string; // Corresponding Markdown log file name
requestId?: string; // Future extension: 1 send = 1 UUID
}- content: Text displayed directly in the chat UI (prompt/response).
- summary: Summary for
Worktree.lastMessageSummary(optional). - logFileName: Link to the detailed log file in
.claude_logs/.
interface WorktreeSessionState {
worktreeId: string;
lastCapturedLine: number; // Last line count obtained via tmux capture-pane
}- On Stop hook, the entire scrollback is captured via
tmux capture-pane. - Lines after
lastCapturedLineare treated as "new output" for differential extraction.
- On app startup, scan git worktrees based on
MCBD_ROOT_DIR. - Store/update Worktree records in the DB based on the scanned worktree information.
- If existing
cw_{worktreeId}tmux sessions exist, reconcile them with Worktrees (optional).
Overview
sequenceDiagram
participant UI as UI (Screen B)
participant API as API_Send
participant T as tmux
participant CL as Claude CLI
UI->>API: POST /api/worktrees/:id/send { message }
API->>T: Check session existence (cw_{worktreeId})
alt No session
API->>T: tmux new-session (cw_{worktreeId})
API->>T: export CLAUDE_HOOKS_STOP=...
API->>T: Start claude
end
API->>T: tmux send-keys "message"
API-->>UI: 202 Accepted (send complete)
Note over UI: Display "Sending..." bubble
Detailed Steps
- UI (Screen B) sends
POST /api/worktrees/:id/sendwith{ message }. - API:
- Retrieves the Worktree from worktreeId and references the path.
- Checks session existence with
tmux has-session -t cw_{worktreeId}. - If not found, executes UC-2 (lazy startup flow, described below).
- Sends the message to Claude CLI with
tmux send-keys -t cw_{worktreeId} "<message>" C-m. - API responds immediately with 202 (Accepted). The UI displays the user's message plus a "Sending..." bubble.
sequenceDiagram
participant CL as Claude CLI
participant Hook as CLAUDE_HOOKS_STOP
participant API as API_Hook
participant T as tmux
participant DB as SQLite
participant FS as .claude_logs
participant WS as WebSocket
CL-->>Hook: Completion hook invocation
Hook->>API: POST /api/hooks/claude-done { worktreeId }
API->>T: tmux capture-pane (capture full scrollback)
API->>API: Differential extraction (lines after lastCapturedLine)
API->>FS: Save as Markdown log
API->>DB: Update ChatMessage and Worktree.updatedAt
API->>WS: Publish new ChatMessage
Detailed Steps
- When Claude CLI completes processing, it executes the command set in
CLAUDE_HOOKS_STOP.- Example:
HOOK_COMMAND="curl -X POST http://localhost:3000/api/hooks/claude-done \ -H 'Content-Type: application/json' \ -d '{\"worktreeId\":\"{worktreeId}\"}'" export CLAUDE_HOOKS_STOP="${HOOK_COMMAND}" API_HookreceivesPOST /api/hooks/claude-done:- Reads
WorktreeSessionState.lastCapturedLine(default 0). - Captures scrollback with
tmux capture-pane -p -S -10000 -t cw_{worktreeId}.
- Reads
- Counts lines in the captured text:
- Extracts only lines after
lastCapturedLineas "new output". - Future extension: If markers (
### REQUEST {id} START/END) are introduced, only that range can be extracted.
- Extracts only lines after
- Saves the extracted diff as Markdown under
.claude_logs/. - Creates a
ChatMessagerecord (role = "claude") with the linkedlogFileName. - Updates
Worktree.lastMessageSummaryandupdatedAt. - Publishes the new
ChatMessagevia WebSocket to all clients viewing the relevant worktree.
- Clients subscribe by specifying a
worktreeIdupon connection. - The server manages rooms/channels per
worktreeId. - When a new
ChatMessageis saved, it broadcasts to all clients subscribed to thatworktreeId.
Message format example:
{
"type": "chat_message_created",
"worktreeId": "feature-foo",
"message": {
"id": "uuid",
"role": "claude",
"content": "Claude's response...",
"timestamp": "2025-11-16T03:00:00.000Z",
"logFileName": "20251116-030000-feature-foo-bd2f8c3d.md"
}
}When Stop hook doesn't arrive
- UI side:
- If "Sending..." isn't replaced after a set period (e.g., 120 seconds), display a warning bubble.
- Server side:
- Log a timeout in the Hook API (implementation-dependent).
- Manual checking of
.claude_logs/or tmux state is expected.
When tmux session / Claude process crashes
- During
API_Send, check tmux session / process state withtmux has-session. - If no session / Claude not found, restart via UC-2.
- Re-set
CLAUDE_HOOKS_STOPwhen restarting Claude CLI.
- Session name:
mcbd-{cliToolId}-{worktreeId} - Example:
- Claude:
mcbd-claude-feature-foo
- Claude:
- One session per worktree is maintained.
- Note: Migration from old naming convention
cw_{worktreeId}: Implemented in Issue #4
Design Pattern:
- CLI tool abstraction via Strategy pattern
BaseCLIToolabstract class defines the common interface:abstract class BaseCLITool { abstract id: CLIToolType; abstract name: string; abstract command: string; abstract isInstalled(): Promise<boolean>; abstract isRunning(worktreeId: string): Promise<boolean>; abstract startSession(worktreeId: string, worktreePath: string): Promise<void>; abstract sendMessage(worktreeId: string, message: string): Promise<void>; abstract killSession(worktreeId: string): Promise<boolean>; getSessionName(worktreeId: string): string; }
Implementation Classes:
ClaudeTool- Claude Code CLICodexTool- Codex CLIGeminiTool- Gemini CLI (Issue #368)VibeLocalTool- Vibe-Local / Ollama (Issue #368)
Management:
CLIToolManagersingleton class manages each tool instance- Retrieves the appropriate tool based on the worktree's
cliToolId
Benefits:
- Easy to add new CLI tools (just extend
BaseCLITool) - API layer depends only on abstract interfaces, not specific implementations
- Encapsulates tool-specific logic
# {sessionName} = cw_{worktreeId}
# {worktreePath} = /path/to/root/feature/foo
tmux new-session -d -s "{sessionName}" -c "{worktreePath}"
HOOK_COMMAND="curl -X POST http://localhost:3000/api/hooks/claude-done \
-H 'Content-Type: application/json' \
-d '{\"worktreeId\":\"{worktreeId}\"}'"
tmux send-keys -t "{sessionName}" "export CLAUDE_HOOKS_STOP='${HOOK_COMMAND}'" C-m
tmux send-keys -t "{sessionName}" "claude" C-m- The wrapper for executing tmux commands directly from the API is organized in
src/lib/tmux.ts.
- Simple implementation: Only check "session existence".
- For more robustness:
tmux list-panes -F "#{pane_pid}"→ Usepsto verify the PID isclaude.- If in prompt state (claude has exited), re-set
CLAUDE_HOOKS_STOP+ restart claude.
- Storage location example:
~/.mycodebranchdesk/db.sqlite - Tables (conceptual):
- worktrees:
- id, name, path, last_message_summary, updated_at
- cli_tool_id (added: Issue #4) - CLI tool to use ('claude' | 'codex' | 'gemini' | 'vibe-local')
- selected_agents (added: Issue #368) - Selected 2 agents (JSON array, e.g., '["claude","vibe-local"]')
- vibe_local_model (added: Issue #368) - Ollama model name for Vibe-Local (nullable)
- repository_path, repository_name, description
- last_user_message, last_user_message_at
- favorite, status, link
- chat_messages:
- id, worktree_id, role, content, summary, timestamp, log_file_name, request_id
- message_type, prompt_data
- session_states:
- worktree_id, last_captured_line
- worktrees:
-
A
.claude_logs/directory is created directly under each worktree directory. -
File naming convention:
{YYYYMMDD}-{HHmmss}-{worktreeId}-{uuid}.md -
Content example:
# CommandMate Log
## Worktree
feature/foo
## Timestamp
2025-11-16T03:00:00Z
## User
<User's prompt>
## Claude
<Claude's full response>- Deletion policy:
- v1 does not perform automatic deletion; future cleanup via CLI is under consideration.
MCBD_ROOT_DIR(required)- Root directory for git worktree management
MCBD_PORT(optional, default: 3000)MCBD_BIND(optional, default: 127.0.0.1)- Potentially added in the future:
MCBD_DB_PATH(storage location for db.sqlite)MCBD_LOG_LEVEL(log output level)
A .env.example file is expected to be included in the repository.
- Default is
MCBD_BIND=127.0.0.1 - Access is limited to the same machine.
- Security risk is essentially dependent on the OS user.
- When
CM_BIND=0.0.0.0, access from any device on the same LAN is possible - Reverse proxy authentication (Nginx / Caddy, etc.) is recommended (details:
docs/security-guide.md) - HTTPS / TLS termination is delegated to the reverse proxy
- Future design allows including
requestIdin the Stop hook payload:
{ "worktreeId": "{worktreeId}", "requestId": "{requestId}" }- By embedding it in
ChatMessage.requestIdand log names, you can more precisely trace "which request this response belongs to."
Implementation:
- Claude Code support
- Each worktree manages its CLI tool via the
cliToolIdfield - Abstraction via Strategy pattern:
BaseCLIToolabstract classClaudeToolimplementation classCLIToolManagersingleton for managing tool instances
- Database schema:
worktrees.cli_tool_idcolumn (default: 'claude') - Supported APIs:
POST /api/worktrees/:id/send- Send messagePOST /api/worktrees/:id/respond- Respond to promptPOST /api/worktrees/:id/kill-session- Kill sessionGET /api/worktrees- List worktrees (including session state)
Future Extensions:
- To add other CLIs (openai, lmstudio, etc.), implement by extending
BaseCLITool - If Stop hook specifications differ, handle within each tool class
Implementation:
- Real-time detection of CLI tool state by directly parsing terminal output
- Visual status indicators displayed in the sidebar
- Polling updates every 2 seconds
Status Types:
| Status | Display | Detection Condition |
|---|---|---|
idle |
Gray ● | Session not started |
ready |
Green ● | Input prompt ❯ displayed |
running |
Blue spinner | Thinking indicator detected |
waiting |
Yellow ● | Interactive prompt detected |
Detection Logic:
- Capture terminal output via
captureSessionOutput() - Strip ANSI escape codes
- Filter empty lines and extract the last 15 lines
- Pattern match in priority order:
- Interactive prompt →
waiting - Thinking indicator (
✻ Thinking…) →running - Input prompt (
❯) →ready - Otherwise →
running(assumed processing)
- Interactive prompt →
Related Files:
src/config/status-colors.ts- Centralized status color managementsrc/lib/cli-patterns.ts- CLI tool-specific pattern definitionssrc/lib/prompt-detector.ts- Prompt detectionsrc/types/sidebar.ts- Status determination logic
For details, see Status Indicator.
Implementation:
- API endpoint and UI for deleting registered repositories
- Batch cleanup of related worktrees, sessions, and pollers
- Centralized session stop processing via Facade pattern
API:
DELETE /api/repositories- Delete repository (specifyrepositoryPathin request body)
Request/Response:
// Request
{ "repositoryPath": "/path/to/repository" }
// Response (200 OK)
{
"success": true,
"deletedWorktreeCount": 3,
"warnings": []
}Error Responses:
| Status | Condition |
|---|---|
| 400 | repositoryPath not specified |
| 404 | Repository does not exist |
Deletion Flow:
- Get all worktree IDs belonging to the repository
- Kill tmux sessions for each worktree (if kill fails, record in warnings and continue)
- Stop response-pollers
- Clean up WebSocket subscription state
- CASCADE delete worktree records from DB
- Broadcast
repository_deletedevent
Gradual Error Handling:
- DB deletion continues even when session kill fails
- Failure information is returned in the
warningsarray
UI Implementation:
- Delete button (×) appears on hover over repository filter chips
- Confirmation dialog requires typing "delete"
- Warning icon (
⚠️ ) displayed for repositories set viaWORKTREE_REPOSenvironment variable
Related Files:
src/lib/session-cleanup.ts- Session/poller stop Facadesrc/app/api/repositories/route.ts- DELETE endpointsrc/lib/db.ts-getWorktreeIdsByRepository(),deleteRepositoryWorktrees()src/lib/ws-server.ts-cleanupRooms()src/components/worktree/WorktreeList.tsx- Delete UI
- For the future:
- Response time (latency) measurement
- Error rates
- Per-session history metrics
- Extension fields in ChatMessage and Worktree for metadata to enable visualization.