Keyboard shortcuts

Press ← or → to navigate between chapters

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 pnpm 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 (2 MB, for MCP access)
    ├──> WebSocket broadcast (for browser clients)
    ├──> Grid frame (alacritty_terminal) ──> Frontend: CanvasTerminal renders it
    ├──> OutputParser detects special events
    │    (rate limits, PR URLs, progress, prompts)
    └──> output_watchers.rs: assemble lines, clean, match plugin patterns
              │
              ▼
         Tauri Event ("pty-watcher-lines-{session_id}", {session_id, lines})
         (browser: "watcher-lines" WS frame, "plugin-watcher-lines" SSE)
              │
              ▼
         pluginRegistry.handleWatcherLines() ──> OutputWatcher.onMatch()

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)
ideasStore ──provides──> IdeasPanel (ideas), 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
    ├──> ideasStore.hydrate()           → load_notes
    ├──> keybindingsStore.hydrate()     → load_keybindings
    ├──> agentConfigsStore.hydrate()    → load_agents_config
    └──> agentDetection.detectAll()     → detect installed AI agents

Event System

Tauri Events (Backend → Frontend)

EventPayloadSource
pty-watcher-lines-{session_id}{session_id, lines}PTY reader thread (plugin OutputWatchers)
pty-exit-{session_id}{session_id}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()
    │
    ▼
invoke("github_start_polling", {paths, issueFilter, prHideDrafts})
    │
    ▼
Rust: github_poller loop (60s base, 120s when tab hidden,
      300s max, exponential backoff on errors / rate limit)
    │
    ├──> GraphQL batch query to GitHub API per repo
    │
    ▼
Rust emits Tauri events (only when state changed since last poll):
    ├──> "github-pr-update"    {repo_path, statuses}
    ├──> "github-issues-update" {repo_path, issues}
    └──> "github-transition"   — PR notifications on state changes
         (merged, closed, blocked, ci_failed, changes_requested, ready)
    │
    ▼
githubStore listen() handlers update store state
    │
    ▼
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.

Agent Usage Polling

Claude and Codex usage share one native feature slot managed by src/features/agentUsage.ts. It follows the active terminal’s detected agent, polls only that provider’s usage API, and labels every ticker result with its provider so an asynchronous tab switch cannot render stale limits as the other provider’s headroom.

initAgentUsage()  (called from plugins/index.ts if not disabled)
    │
    active Claude/Codex tab change or every 5 min (API_POLL_MS)
    │
    ▼
invoke("get_claude_usage_api" | "get_codex_usage_api")
    │
    ▼
Rust: read the matching local provider credentials
    → HTTP GET to the provider usage endpoint
    → parse Claude or Codex rate-limit windows
    │
    ▼
statusBarTicker.addMessage({
    id: "claude-usage:rate",
    pluginId: "claude-usage",
    label: "Claude" | "Codex",
    text: "5h: 42% · 7d: 18%",
    priority: 10–90 (based on utilization),
    onClick: openDashboard
})
    │
    ▼
StatusBar renders ticker message
    ├── matching Claude/Codex label → absorbed into active agent badge
    ├── stale provider label on an agent tab → hidden until the new poll wins
    └── non-agent tab → standalone ticker

Status Bar Ticker

The statusBarTicker store provides a priority-based rotating message system used by the shared Agent 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 a provider-matching Claude/Codex usage message
        └── 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