Skip to content

Data Flow

When Claude Code fires an event, the hook system writes status to the filesystem:

Claude Code Event
Hook Script (notify.sh)
├── Reads stdin JSON for event metadata
│ (teammate_name, team_name, session_id, hook_event_name)
├── Determines target file: ~/.claude-sessions/<tmux_session>.json
├── Routes based on teammate presence:
│ ├── No teammate → Update main session status
│ └── Has teammate → Update team.agents[] entry
├── Preserves external agents field (agents.opencode, etc.)
└── Writes JSON atomically

OpenCode (and other agents) write to the same status file via plugins:

OpenCode Lifecycle Event
navi.js Plugin
├── Resolves tmux session name (cached 30s)
├── Read-modify-write: ~/.claude-sessions/<session>.json
│ ├── Preserves all root-level fields (Claude Code's domain)
│ └── Updates agents.opencode entry
├── Duplicate suppression (1s window for same status)
└── Atomic write (temp file + rename)
{
"tmux_session": "my-session",
"status": "working",
"message": "Implementing login feature",
"cwd": "/home/user/projects/app",
"timestamp": 1738972800,
"metrics": {
"started": 1738970000,
"tools": {"Read": 5, "Write": 3, "Bash": 2},
"recent_tools": ["Read", "Write", "Bash"]
},
"team": {
"name": "my-project",
"agents": [
{"name": "researcher", "status": "working", "timestamp": 1738972800}
]
},
"agents": {
"opencode": {
"status": "idle",
"timestamp": 1738972780
}
}
}

The TUI runs multiple concurrent polling loops:

Timer tick
Read ~/.claude-sessions/*.json
Parse each JSON file → []session.Info
Cross-reference with `tmux list-sessions`
├── Remove entries with no matching tmux session
Compute CompositeStatus() per session
├── Consider Claude Code + all external agents
├── Return highest-priority status and source
SortSessions()
├── Priority statuses first (waiting, permission from any agent)
├── Active sessions next (working from any agent)
└── Then by timestamp (most recent first)
Detect status changes
├── Compare session states vs lastSessionStates
├── Compare agent states vs lastAgentStates
└── Fire audio notifications on transitions
Send sessionsMsg to Update
Re-render view
Session list updated / Timer tick
For each session with a CWD:
├── Check cache (5s TTL, 10s max age)
│ ├── Cache hit → skip
│ └── Cache miss ▼
Run git commands in CWD
├── git branch --show-current
├── git status --porcelain
├── git rev-list --count @{upstream}..HEAD
└── git log -1 --format="%h %s"
Send gitInfoMsg to Update
User opens git detail view (G key)
Check PRDetail cache (60s TTL)
├── Cache hit → display immediately
└── Cache miss ▼
gh pr view --json <all fields>
├── Local: uses working directory context
└── Remote: uses -R owner/repo flag
Parse PR metadata → PRDetail
├── Checks: aggregate passed/failed/pending
├── Reviews: per-reviewer decisions
└── Merge status, labels, change stats
Display in git detail view
├── If checks pending → start auto-refresh (30s)
└── Auto-refresh stops when checks terminal
Session CWD
Convert to Claude project path
/home/user/project → -home-user-project
Find most recent .jsonl in ~/.claude/projects/<path>/
Parse JSONL for assistant messages with usage data
Aggregate: input_tokens + cache_read + cache_creation → total input
output_tokens → total output
Timer tick / Manual refresh
Discover .navi.yaml files
For each project config:
├── Check cache (configurable TTL)
│ ├── Cache hit → skip
│ └── Cache miss ▼
Execute provider scripts (4 concurrent workers)
├── Bounded concurrency via semaphore channel
├── Per-project error isolation
└── Results collected deterministically
Parse JSON output → ProviderResult
Send to Update → render task panel
User presses Enter to attach
startAttachMonitor()
├── Pass lastSessionStates and lastAgentStates to monitor
├── Create context with cancel
└── Launch polling goroutine
tea.ExecProcess hands terminal to tmux
Monitor goroutine (500ms tick)
├── Read ~/.claude-sessions/*.json
├── Compare session states against known states
├── Compare agent states against known states
└── Fire audio.Notifier on transitions
User detaches (Ctrl-B D)
stopAttachMonitor()
├── Cancel context → goroutine exits
├── Recover final states via monitor.States() and monitor.AgentStates()
└── Assign back to lastSessionStates and lastAgentStates
TUI resumes polling — no duplicate notifications
tmux status bar runs `navi status` (every 5s)
Read ~/.claude-sessions/*.json via session.ReadStatusFiles()
Count sessions by status
├── Default: show waiting + permission only
└── --verbose: show all non-zero counts
Print summary and exit
PM tick (5min) / View entry / Manual refresh (r key)
DiscoverProjects()
├── Group sessions by expanded CWD
├── Deduplicate via path expansion
└── Derive project name from directory basename
CaptureSnapshot() per project
├── Git state: rev-parse HEAD, branch, ahead, dirty
├── Task state: aggregate provider results by status
├── Session state: composite status from grouped sessions
├── Current PBI: multi-strategy resolver
│ ├── 1. Provider hint (current_pbi_id)
│ ├── 2. Session metadata (current_pbi field)
│ ├── 3. Branch pattern (regex matching)
│ ├── 4. Status heuristic (InProgress > Agreed > ...)
│ └── 5. First group fallback
└── Last activity: max timestamp across sessions
DiffSnapshots(previous, current)
├── task_completed: Done count increased
├── task_started: InProgress count increased
├── commit: HEAD SHA changed (runs git log old..new)
├── session_status_change: composite status changed
├── pbi_completed: all tasks done
├── branch_created: branch name changed
└── pr_created: PR number 0 → non-zero
AppendEvents() to ~/.config/navi/pm/events.jsonl
├── Prune events older than 24 hours
└── Append new events
PMOutput {snapshots, events}
├── Render in PM TUI view (three zones)
└── Evaluate triggers for PM agent invocation
Trigger event (task_completed, commit, on-demand)
BuildInbox(trigger, snapshots, events)
InvokeWithRecovery(inbox)
├── claude -p --output-format json --json-schema <schema>
├── Pipe inbox JSON to stdin
├── Timeout: 120 seconds
├── Success → ParseOutput() → PMBriefing
│ └── CacheOutput() to last-output.json
├── Failure → LoadCachedOutput() (fallback, marked stale)
└── Rate limit → Exponential backoff (1s, 2s, 4s, max 3 retries)
PMBriefing displayed in Zone 1 (briefing area)

The Bubble Tea Update function processes messages in priority order:

  1. Dialog mode — Route to dialog-specific handler (including sound pack picker)
  2. PM view — Route to PM view handler (navigation, expansion, scrolling)
  3. Task panel focus — Route to task panel handler
  4. Preview focus — Route to preview handler
  5. Search mode — Route to search handler
  6. Main keybindings — Handle navigation, actions, toggles
  7. Async messages — Process polling results, command outputs, PM data