Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Data Flow

IPC Communication

TUICommander supports two IPC modes through a unified transport abstraction:

Tauri Mode (Native Desktop)

Frontend (SolidJS) ──invoke()──> Tauri IPC ──> Rust Command
Frontend (SolidJS) <──listen()── Tauri Events <── Rust emit()
  • Zero-overhead RPC via @tauri-apps/api/core.invoke()
  • Event subscription via @tauri-apps/api/event.listen()
  • Used when running as native Tauri application

Browser Mode (HTTP/WebSocket)

Frontend (SolidJS) ──fetch()──> HTTP REST API ──> Rust Handler
Frontend (SolidJS) <──WebSocket── PTY Output Stream
  • HTTP REST for all commands (mapped from Tauri command names)
  • WebSocket for real-time PTY output streaming
  • SSE for MCP JSON-RPC transport
  • Used when running in browser via npm run dev

Transport Abstraction

src/invoke.ts provides invoke<T>(cmd, args) that resolves to the correct transport at module initialization:

// Zero overhead in Tauri - resolved once at import
const invoke = isTauri() ? tauriInvoke : httpInvoke;

src/transport.ts maps Tauri command names to HTTP endpoints:

mapCommandToHttp("get_repo_info", { path }) → GET /repo/info?path=...
mapCommandToHttp("create_pty", config)       → POST /sessions

PTY Output Pipeline

Terminal output flows through multiple processing stages:

PTY Process (shell)
    │
    ▼
Raw Bytes ──> Utf8ReadBuffer
    │         (handles split multi-byte UTF-8 characters)
    ▼
UTF-8 String ──> EscapeAwareBuffer
    │             (prevents splitting ANSI escape sequences)
    ▼
Safe String
    ├──> Ring Buffer (64KB, for MCP access)
    ├──> WebSocket broadcast (for browser clients)
    └──> Tauri Event ("pty-output", {session_id, data})
              │
              ▼
         Frontend: xterm.js terminal.write(data)
              │
              ▼
         OutputParser detects special events
         (rate limits, PR URLs, progress, prompts)

Each PTY session has a dedicated reader thread (spawned in pty.rs) that reads from the PTY master fd in a loop.

State Management

Frontend Stores

SolidJS reactive stores hold all frontend state. Each store follows the pattern:

const [state, setState] = createStore<StoreType>(initialState);

// Public API exposed as object with methods
export const myStore = {
  state,        // Read-only reactive state
  hydrate(),    // Load from Rust backend
  action(),     // Modify state + persist to Rust
};

Store Dependency Graph

repositoriesStore ──references──> terminalsStore (terminal IDs per branch)
githubStore ──provides data to──> Sidebar (CI rings, PR badges), StatusBar (PR/CI badges)
settingsStore ──configures──> Terminal (font, theme, shell)
uiStore ──controls──> panel visibility, sidebar state
promptLibraryStore ──used by──> PromptDrawer, PromptOverlay
dictationStore ──manages──> dictation state, model downloads
notificationsStore ──plays──> sound alerts on terminal events
errorHandlingStore ──retries──> failed operations with backoff
statusBarTicker ──feeds──> StatusBar (rotating priority-based messages)
notesStore ──provides──> NotesPanel (ideas/notes), StatusBar (badge count)
userActivityStore ──tracks──> StatusBar (merged PR grace period)

Persistence Flow

User Action
    │
    ▼
Store.action()
    ├──> setState() (immediate reactive update)
    └──> invoke("save_xxx_config", data) (async persist to Rust)
              │
              ▼
         Rust: save_json_config(filename, data)
              │
              ▼
         JSON file in platform config directory

Hydration Flow (App Startup)

useAppInit.initApp()
    │
    ├──> repositoriesStore.hydrate()    → load_repositories
    ├──> uiStore.hydrate()              → load_ui_prefs
    ├──> settingsStore.hydrate()        → load_app_config
    ├──> notificationsStore.hydrate()   → load_notification_config
    ├──> repoSettingsStore.hydrate()    → load_repo_settings
    ├──> repoDefaultsStore.hydrate()    → load_repo_defaults
    ├──> promptLibraryStore.hydrate()   → load_prompt_library
    ├──> notesStore.hydrate()           → load_notes
    ├──> keybindingsStore.hydrate()     → load_keybindings
    ├──> agentConfigsStore.hydrate()    → load_agent_configs
    └──> agentDetection.detectAll()     → detect installed AI agents

Event System

Tauri Events (Backend → Frontend)

EventPayloadSource
pty-output{session_id, data}PTY reader thread
pty-exit{session_id, exit_code}PTY child exit
dictation-progress{percent}Model download
menu-event{id}Native menu click

Frontend Event Handling

Menu events are handled in App.tsx:

listen("menu-event", (event) => {
  switch (event.payload.id) {
    case "new-tab": handleNewTab(); break;
    case "close-tab": closeTerminal(); break;
    case "toggle-sidebar": uiStore.toggleSidebar(); break;
    // ... 30+ menu actions
  }
});

GitHub Polling

githubStore.startPolling()
    │
    every 30s (120s when tab hidden, 300s on rate limit, exponential backoff on errors)
    │
    ▼
invoke("get_repo_pr_statuses", {path})  +  invoke("get_github_status", {path})
    │                                          │
    ▼                                          ▼
Rust: GraphQL batch query to GitHub API   Rust: local git ahead/behind
    │                                          │
    ▼                                          ▼
githubStore.updateRepoData(path, statuses)    setState(remoteStatus)
    │
    ├──> detectTransitions() — emit PR notifications on state changes
    │    (merged, closed, blocked, ci_failed, changes_requested, ready)
    │
    ▼
Reactive updates to Sidebar CI rings, PR badges

PR State Filtering in StatusBar

The StatusBar applies lifecycle rules before displaying PR data:

githubStore.getBranchPrData(repoPath, branch)
    │
    ├── state = CLOSED → never show (filtered out)
    ├── state = MERGED → show for 5 min of accumulated user activity, then hide
    │                     (userActivityStore tracks click/keydown events)
    └── state = OPEN   → show as-is (PR badge + CI badge)

Per-Repo Immediate Polls

On repo-changed events (git index/refs/HEAD changes), githubStore.pollRepo(path) triggers an immediate re-poll for that repo, debounced to 2 seconds to coalesce rapid git events.

Claude Usage Polling

Claude Usage is a native feature (not a plugin) managed by src/features/claudeUsage.ts. It polls the Anthropic OAuth usage API and posts results to the status bar ticker.

initClaudeUsage()  (called from plugins/index.ts if not disabled)
    │
    every 5 min (API_POLL_MS)
    │
    ▼
invoke("get_claude_usage_api")
    │
    ▼
Rust: read ~/.claude/.credentials OAuth token
    → HTTP GET to Anthropic usage endpoint
    → parse UsageApiResponse (five_hour, seven_day, per-model buckets)
    │
    ▼
statusBarTicker.addMessage({
    id: "claude-usage:rate",
    pluginId: "claude-usage",
    text: "Claude: 5h: 42% · 7d: 18%",
    priority: 10–90 (based on utilization),
    onClick: openDashboard
})
    │
    ▼
StatusBar renders ticker message
    ├── Standalone ticker (when active agent is not claude)
    └── Absorbed into agent badge (when active agent is claude)

Status Bar Ticker

The statusBarTicker store provides a priority-based rotating message system used by the Claude Usage feature and available to plugins via the ui:ticker capability.

statusBarTicker
    │
    ├── Messages sorted by priority (descending)
    ├── Equal-priority messages rotate every 5s
    ├── Expired messages scavenged every 1s (TTL-based)
    │
    └── StatusBar rendering:
        ├── Agent badge absorbs claude-usage messages when active agent is claude
        └── Standalone ticker for all other messages

Keyboard Shortcut Flow

KeyDown Event
    │
    ▼
useKeyboardShortcuts (global listener)
    │
    ├── Platform modifier detection (Cmd on macOS, Ctrl on Win/Linux)
    ├── Shortcut matching against registered handlers
    │
    ▼
Handler execution (e.g., handleNewTab, toggleSidebar)
    │
    ▼
Store updates → Reactive UI updates

Quick Switcher (held-key UI):

Cmd+Ctrl pressed (macOS) / Ctrl+Alt pressed (Win/Linux)
    │
    ▼
Show branch overlay with numbered shortcuts
    │
    ▼
Press 1-9 while holding modifier
    │
    ▼
switchToBranchByIndex(index) → handleBranchSelect()
    │
    ▼
Release modifier → hide overlay