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)
| Event | Payload | Source |
|---|---|---|
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