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

TUICommander

Multi-agent terminal orchestrator

Run multiple AI coding agents in parallel across isolated Git worktrees. Observe, control, and merge AI development from a single terminal workspace.

Key Features

  • Terminal Management — Tabbed terminals with split panes, drag-and-drop reordering, and per-branch isolation via Git worktrees
  • AI Agent Support — Auto-detect Claude Code, Aider, Codex, Gemini CLI and more. Monitor progress, costs, and active subtasks in real time
  • Git Worktrees — Each branch gets its own working directory. Create, merge, archive, and batch-manage worktrees from the UI
  • GitHub Integration — PR badges, CI status rings, merge from the UI, post-merge cleanup, and notification bell for PR events
  • Plugin System — Extend TUICommander with JavaScript plugins. Full API for terminals, git, notifications, and status bar widgets
  • MCP Bridge — Expose app capabilities to AI agents via Model Context Protocol

Getting Started

Head to the Getting Started guide.

TUICommander — Complete Feature Reference

Canonical feature inventory. Update this file when adding, changing, or removing features. See AGENTS.md for the maintenance requirement.

Version: 1.0.5 | Last verified: 2026-04-15


1. Terminal Management

1.1 PTY Sessions

  • Up to 50 concurrent PTY sessions (configurable in Rust MAX_SESSIONS)
  • Each tab runs an independent pseudo-terminal with the user’s shell
  • Terminals are never unmounted — hidden tabs stay alive with full scroll history
  • Session persistence across app restarts (lazy restore on branch click)
  • Agent session restore shows a clickable banner (“Agent session was active — click to resume”) instead of auto-injecting the resume command; Space/Enter resumes, other keys dismiss
  • Foreground process detection (macOS: libproc, Windows: CreateToolhelp32Snapshot)
  • PTY environment: TERM=xterm-256color, COLORTERM=truecolor, LANG=en_US.UTF-8
  • Pause/resume PTY output (pause_pty / resume_pty Tauri commands) — suspends reader thread without killing the session

1.2 Tab Bar

  • Create: Cmd+T, + button (click = new tab, right-click = split options)
  • Close: Cmd+W, middle-click, context menu
  • Reopen last closed: Cmd+Shift+T (remembers last 10 closed tabs)
  • Switch: Cmd+1 through Cmd+9, Ctrl+Tab / Ctrl+Shift+Tab
  • Rename: double-click tab name (inline editing)
  • Reorder: drag-and-drop with visual drop indicators
  • Tab status dot (left of name): grey=idle, blue-pulse=busy, green=done, purple=unseen (completed while not viewed), orange-pulse=question (needs input), red-pulse=error
  • Tab type colors: red gradient=diff, blue gradient=editor, teal gradient=markdown, purple gradient=panel, amber gradient=remote PTY session
  • Remote PTY sessions (created via HTTP/MCP) show “PTY:” prefix and amber styling
  • Progress bar (OSC 9;4)
  • Context menu (right-click): Close Tab, Close Other Tabs, Close Tabs to the Right, Detach to Window, Copy Path (on diff/editor/markdown file tabs)
  • Detach to Window: right-click a tab to open it in a floating OS window
    • PTY session stays alive in Rust — floating window reconnects to the same session
    • Closing the floating window automatically returns the tab to the main window
    • Requires an active PTY session (disabled for tabs without a session)
  • Overflow menu on scroll arrows (right-click) shows clipped tabs; the + button always stays visible regardless of scroll position
  • Tab pinning: pinned tabs are visible across all branches (not scoped to branch key)

1.3 Split Panes

  • Vertical split: Cmd+\ (side by side)
  • Horizontal split: Cmd+Alt+\ (stacked)
  • Navigate: Alt+←/→ (vertical), Alt+↑/↓ (horizontal)
  • Close active pane: Cmd+W
  • Drag-resize divider between panes
  • Up to 6 panes in same direction (N-way split)
  • Split layout persists per branch

1.4 Zoom (Per-Terminal)

  • Zoom in: Cmd+= (+2px)
  • Zoom out: Cmd+- (-2px)
  • Reset: Cmd+0
  • Range: 8px to 32px
  • Current zoom shown in status bar

1.5 Copy & Paste

  • Copy selection: Cmd+C
  • Paste to terminal: Cmd+V
  • Copy on Select — When enabled (Settings > Appearance), selecting text in the terminal automatically copies it to the clipboard. A brief ‘Copied to clipboard’ confirmation appears in the status bar.

1.6 Clear Terminal

  • Cmd+L — clears display, running processes unaffected

1.7 Clickable File Paths

  • File paths in terminal output are auto-detected and become clickable links
  • Paths validated against filesystem before activation (Rust resolve_terminal_path)
  • .md/.mdx → opens in Markdown panel; preview-capable files (HTML, PDF, images, video, audio, plain text/data) → open in the Preview tab (section 3.15); all other code files → open in the built-in code editor
  • file:// URLs are recognized in addition to plain paths — the prefix is stripped and the path resolved like any other
  • Supports :line and :line:col suffixes for precise navigation
  • Recognized extensions: rs, ts, tsx, js, jsx, py, go, java, kt, swift, c, cpp, cs, rb, php, lua, zig, css, scss, html, vue, svelte, json, yaml, toml, sql, graphql, tf, sh, dockerfile, and more

1.8 Find in Content

  • Cmd+F opens search overlay — context-aware: routes to terminal, markdown tab, or diff tab based on active view
  • Terminal: incremental search via @xterm/addon-search with highlight decorations
  • Markdown viewer: DOM-based search with cross-element matching (finds text spanning inline tags)
  • Diff viewer: DOM-based search via SearchBar + DomSearchEngine (same engine as markdown viewer)
  • Yellow highlight for matches, orange for active match
  • Navigate matches: Enter / Cmd+G (next), Shift+Enter / Cmd+Shift+G (previous)
  • Toggle options: case sensitive, whole word, regex
  • Match counter shows “N of M” results
  • Escape closes search and refocuses content

1.9 International Keyboard Support

  • Terminal handles international keyboard input correctly
  • Rate-limit false positives reduced for non-ASCII input

1.10 Move Terminal to Worktree

  • Right-click a terminal tab → “Move to Worktree” submenu lists available worktrees (excluding the current one)
  • Selecting a worktree sends cd to the PTY; OSC 7 auto-reassigns the terminal to the target branch
  • Also available via Command Palette: dynamic “Move to worktree: <branch>” entries appear when the active terminal belongs to a repo with multiple worktrees
  • Only shown when the repo has more than one worktree

1.11 OSC 7 CWD Tracking

  • Terminals report their current working directory via OSC 7 escape sequences
  • Parsed in the frontend via xterm.js registerOscHandler(7, ...), then sent to Rust via update_session_cwd IPC and stored per-session as session_cwd
  • When a terminal’s CWD falls inside a known worktree path, the session is automatically reassigned to the correct branch in the sidebar
  • Enables accurate branch association even when the user cds into a different worktree from a single terminal

1.12 Kitty Keyboard Protocol

  • Supports Kitty keyboard protocol flag 1 (disambiguate escape codes)
  • Per-session flag tracking via get_kitty_flags Tauri command
  • Enables correct handling of Shift+Enter (multi-line input), Ctrl+Backspace, and modifier key combinations in agents that request the protocol (e.g. Claude Code)

1.13 File Drag & Drop

  • Drag files from Finder/Explorer onto the terminal area or any panel
  • Uses Tauri’s native onDragDropEvent API (not HTML5 File API — Tauri webviews do not expose file paths via HTML5)
  • Active PTY session: dropped file paths are forwarded directly to the terminal as text (enables Claude Code image drops and similar workflows)
  • No active PTY session: .md/.mdx files open in Markdown viewer, preview-capable files open in the Preview tab (section 3.15), all other files open in Code Editor
  • Multiple files can be dropped at once
  • Visual overlay with dashed border appears during drag hover
  • Global dragover/drop preventDefault prevents the Tauri webview from treating drops as browser navigation (which would replace the UI with a white screen)
  • macOS file association: .md/.mdx files registered with TUICommander — double-click in Finder opens them directly
  • Type ~ in the command palette (Cmd+P) to search text across all open terminal buffers
  • Results show terminal name, line number, and highlighted match text
  • Selecting a result switches to the correct terminal tab/pane and scrolls to the matched line (centered in viewport)
  • Minimum 3 characters after prefix
  • Also accessible via the explicit “Search Terminals” command in the palette

1.15 Terminal Bell

  • Terminal Bell — Configurable bell behavior when the terminal receives a BEL character (\x07). Four modes: none (silent), visual (screen flash animation), sound (plays the Info notification sound), both (flash + sound). Configure in Settings > Appearance.

2. Sidebar

2.1 Repository List

  • Add repository via + button or folder dialog
  • Click repo header to expand/collapse branch list
  • Click again to toggle icon-only mode (shows initials)
  • button: Repo Settings, Switch Branch (via context menu on main worktree), Create Worktree, Move to Group, Park Repository, Remove
  • macOS TCC access dialog: when the OS denies access to a repository directory (e.g. Desktop, Documents), a dialog explains the issue and guides the user to grant Full Disk Access in System Settings

2.2 Repository Groups

  • Named, colored groups for organizing repositories
  • Create: repo → Move to Group → New Group…
  • Move repo: drag onto group header, or repo → Move to Group → select group
  • Remove from group: repo → Move to Group → Ungrouped
  • Group context menu (right-click header): Rename, Change Color, Delete
  • Collapse/expand: click group header
  • Reorder groups: drag-and-drop
  • Color inheritance: repo color > group color > none

2.2.1 Switch Branch

Right-click the main worktree row → Switch Branch submenu to checkout a different branch. The submenu shows all local branches with a checkmark on the current one. If the working tree is dirty, prompts to stash changes first. Blocks switching when a terminal has a running process.

2.3 Branch Items

  • Click: switch to branch (shows its terminals, creates worktree if needed)
  • Double-click branch name: rename branch
  • Right-click context menu: Copy Path, Add Terminal, Create Worktree, Merge & Archive, Delete Worktree, Open in IDE, Rename Branch
  • CI ring: proportional arc segments (green=passed, red=failed, yellow=pending)
  • PR badge: colored by state (green=open, purple=merged, red=closed, gray=draft) — click for detail popover
  • Diff stats: +N / -N additions/deletions
  • Merged badge: branches merged into main show a “Merged” badge
  • Question indicator: ? icon (orange, pulsing) when agent asks a question
  • Idle indicator: branch icons turn grey when the repo has no active terminals
  • Quick switcher badge: numbered index shown when Cmd+Ctrl held
  • Remote-only branches with open PRs: shown in sidebar with PR badge and inline accordion actions (Checkout, Create Worktree). Additional actions when PR popover is open: Merge, View Diff, Approve, Dismiss
  • Dismiss/Show Dismissed: remote-only PRs can be dismissed from the sidebar; a “Show Dismissed” toggle reveals them again
  • Branch sorting: main/master/develop always first, then alphabetical; merged PR branches sorted last

2.4 Git Quick Actions

  • Bottom of sidebar when a repo is active
  • Pull, Push, Fetch, Stash buttons — execute in active terminal

2.5 Sidebar Resize

  • Drag right edge to resize (200-500px range)
  • Toggle visibility: Cmd+[
  • Width persists across sessions

2.6 Quick Branch Switcher

  • Hold Cmd+Ctrl (macOS) or Ctrl+Alt (Win/Linux): show numbered overlay
  • Cmd+Ctrl+1-9: switch to branch by index

2.7 Park Repos

  • Right-click any repo in the sidebar to park or unpark it
  • Parked repos are hidden from the main repository list
  • Sidebar footer button opens a popover showing all parked repos
  • Unpark a repo from the popover to restore it to the main list

3. Panels

3.1 Panel System

  • File Browser, Markdown, Diff, and Plan panels are mutually exclusive — opening one closes the others
  • Ideas panel is independent (can be open alongside any of the above)
  • Subtle fade transition when closing side panels (opacity + transform animation)
  • All panels have drag-resize handles on their left edge (200-800px)
  • Min-width constraints prevent panels from collapsing (Markdown: 300px, File Browser: 200px)
  • Toggle buttons in status bar with hotkey hints visible during quick switcher

3.2 Diff Panel (Removed in 0.9.0)

Replaced by the Git Panel’s Changes tab (section 3.8). Cmd+Shift+D now opens the Git Panel

3.3 Markdown Panel (Cmd+Shift+M)

  • Renders .md and .mdx files with syntax-highlighted code blocks
  • File list from repository’s markdown files
  • Clickable file paths in terminal open .md files here
  • Auto-show: adding any markdown tab automatically opens the Markdown panel if it’s closed
  • Header bar shows file path (or title for virtual tabs) with Edit button (pencil icon) to open in CodeEditor
  • Cmd+F search: find text in rendered markdown with highlight navigation (shared SearchBar component)
  • Interactive GFM checkboxes: - [ ], - [x], and - [~] task-list items render as clickable checkboxes. Clicking cycles through unchecked → checked → in-progress → unchecked. Changes are written back to the source .md file on disk. The [~] state renders as an indeterminate (half-filled) checkbox — non-standard GFM extension for tracking in-progress items
  • Inline review comments (tweaks): select text in rendered markdown, add a comment — stored as HTML comment markers invisible to standard renderers but readable by humans and LLMs

3.4 File Browser Panel (Cmd+E)

  • Directory tree of active repository
  • Auto-refresh: directory watcher detects external file changes (create/delete/rename) and refreshes automatically within ~1s, preserving selection
  • Navigation: ↑/↓ (navigate), Enter (open/enter dir), Backspace (parent dir)
  • Breadcrumb toolbar: always-visible path bar with click-to-navigate segments + inline sort dropdown (funnel icon)
  • Search filter: text input with * and ** glob wildcard support
  • Git status indicators: orange (modified), green (staged), blue (untracked)
  • Context menu (right-click): Copy (Cmd+C), Cut (Cmd+X), Paste (Cmd+V), Rename, Delete, Add to .gitignore
  • Keyboard shortcuts work when panel is focused (copy/cut/paste)
  • Sort dropdown: Name (alphabetical, directories first) or Date (newest first, directories first)
  • View modes: flat list (default) and tree view — toggle via toolbar buttons. Tree view shows a collapsible hierarchy with lazy-loaded subdirectories on expand. Switching to tree resets to repo root. Search always uses flat results
  • Click file to open in code editor tab

3.4.1 Content Search (Cmd+Shift+F)

  • Full-text search across file contents — toggle from filename search via the C button in the search bar
  • Options: case-sensitive, regex, whole-word
  • Results stream progressively and are grouped by file with match count per file
  • Each result row shows file path, line number, and highlighted match context
  • Click a result to open the file in the code editor at the matched line
  • Binary files and files larger than 1 MB are automatically skipped
  • Backed by search_content Tauri command; results delivered via content-search-batch events

3.5 Code Editor (CodeMirror 6)

  • Opens in main tab area when clicking a file in file browser
  • Syntax highlighting auto-detected from extension (disabled for files > 500 KB)
  • Line numbers, bracket matching, active line highlight, Tab-to-indent
  • Find/Replace: Cmd+F (find), Cmd+G / Cmd+Shift+G (next/prev), Cmd+H (replace), selection match highlighting
  • Save: Cmd+S (when editor tab is focused)
  • Read-only toggle: padlock icon in editor header
  • Unsaved changes: dot indicator in tab bar and header
  • Disk conflict detection: banner with “Reload” (discard local) or “Keep mine” options
  • Auto-reloads silently when file changes on disk and editor is clean

3.6 Ideas Panel (Cmd+Alt+N)

  • Quick notes / idea capture with send-to-terminal
  • Enter submits idea, Shift+Enter inserts newline
  • Per-idea actions: Edit (copies back to input), Send to Terminal (sends + return), Delete
  • Mark as used: notes sent to terminal are timestamped (usedAt) for tracking
  • Badge count: status bar toggle shows count of notes visible for the active repo
  • Per-repo filtering: notes can be tagged to a repository; untagged notes visible everywhere
  • Image paste: Ctrl+V / Cmd+V pastes clipboard images as thumbnails attached to the note
    • Images saved to config_dir()/note-images/<note-id>/ on disk
    • Thumbnails displayed inline below note text and in the input area before submit
    • Image-only notes (no text) are supported
    • Images removed from disk when the note is deleted
    • Send to terminal appends absolute image paths so AI agents can read them
    • Max 10 MB per image; accepted formats: PNG, JPEG, WebP, GIF
  • Edit preserves note identity (in-place update, no ID change)
  • Escape cancels edit mode
  • Data persisted to Rust config backend

3.7 Help Panel (Cmd+?)

  • Shows app info and links (About, GitHub, docs)
  • Keyboard shortcuts are now in Settings > Keyboard Shortcuts tab (auto-generated from actionRegistry.ts)

3.8 Git Panel (Cmd+Shift+D)

Tabbed side panel with four tabs: Changes, Log, Stashes, Branches. Replaces the former Git Operations Panel floating overlay and the standalone Diff Panel.

Changes tab:

  • Porcelain v2 working tree status via get_working_tree_status (branch, upstream, ahead/behind, stash count, staged/unstaged/untracked files)
  • Sync row: Pull, Push, Fetch buttons (background execution via run_git_command)
  • Stage / unstage individual files or stage all / unstage all
  • Discard unstaged changes (with confirmation dialog)
  • Inline commit form with message input and Amend toggle
  • Click a file row to open its diff in the diff panel
  • Status icons per file: Modified, Added, Deleted, Renamed, Untracked
  • Per-file diff counts (additions/deletions) shown inline
  • Glob filter to narrow the file list
  • Path-traversal validation on all stage/unstage/discard operations
  • History sub-panel (collapsible): per-file commit history via get_file_history (follows renames), paginated with virtual scroll
  • Blame sub-panel (collapsible): per-line blame via get_file_blame (porcelain format), age heatmap (green=recent, fading to neutral), commit metadata per line

Log tab:

  • Paginated commit log via get_commit_log (default 50, max 500)
  • Virtual scroll via @tanstack/solid-virtual for large histories
  • Canvas-based commit graph via get_commit_graph: lane assignment, Bezier curve connections, 8-color palette, ref badges (branch, tag, HEAD). Graph follows HEAD only
  • Click a commit row to expand and see its changed files (via get_changed_files)
  • Click a file in an expanded commit to open its diff at that commit hash
  • Relative timestamps (e.g., “3h ago”)

Stashes tab:

  • List all stash entries via get_stash_list
  • Per-stash actions: Apply, Pop, Drop (via run_git_command)

Branches tab (Cmd+G — opens Git Panel directly on this tab):

  • Local and Remote branches in collapsible sections
  • Rich info per branch: ahead/behind counts (↑N ↓M), relative date, merged badge, stale dimming (branches with last commit > 30 days)
  • Prefix folding: groups branches by / separator (e.g. feature/, bugfix/), toggle to expand/collapse groups
  • Recent Branches section from git reflog
  • Inline search/filter to narrow branch list
  • Checkout (Enter / double-click): switches to the selected branch, with dirty worktree dialog (stash / force / cancel)
  • n — Create new branch (inline form, optional checkout)
  • d — Delete branch (safe + force options; refuses main branch and current branch)
  • R — Rename branch (inline edit)
  • M — Merge selected branch into current
  • r — Rebase current onto selected branch
  • P — Push branch (auto-detects missing upstream and sets tracking)
  • p — Pull current branch
  • f — Fetch all remotes
  • Context menu (right-click): Checkout, Create Branch from Here, Delete, Rename, Merge into Current, Rebase Current onto This, Push, Pull, Fetch, Compare (shows diff --name-status)
  • Backend: get_branches_detail, delete_branch, create_branch, get_recent_branches
  • Click on sidebar “GIT” vertical label also opens Git Panel on the Branches tab

Keyboard navigation:

  • Escape to close the panel
  • Ctrl/Cmd+1–4 to switch between tabs (1=Changes, 2=Log, 3=Stashes, 4=Branches)
  • Auto-refreshes via repo revision subscription

3.9 Quick Branch Switch (Cmd+B)

  • Fuzzy-search dialog to switch branches instantly
  • Shows all local and remote branches for the active repo
  • Badges: current, remote, main branch indicators
  • Keyboard navigation: Arrow keys, Enter to switch, Escape to close
  • Remote branches auto-checkout as local tracking branch
  • Fetches live branch list via get_git_branches

3.10 Task Queue Panel (Cmd+J)

  • Task management with status tracking (pending, running, completed, failed, cancelled)
  • Drag-and-drop task reordering

3.11 Command Palette (Cmd+P)

  • Fuzzy-search across all app actions by name
  • Recency-weighted ranking: recently used actions surface first
  • Each row shows action label, category badge, and keybinding hint
  • Keyboard-navigable: ↑/↓ to move, Enter to execute, Esc to close
  • Search modes: type ! to search files by name, ? to search file contents, ~ to search across all open terminal buffers. File/content results open in editor tab (content matches jump to the matched line). Terminal results navigate to the terminal tab/pane and scroll to the matched line. Leading spaces after prefix are ignored
  • Discoverable search commands: “Search Terminals”, “Search Files”, “Search in File Contents” appear as regular palette commands and pre-fill the corresponding prefix
  • Powered by actionRegistry.ts (ACTION_META map)

3.12 Activity Dashboard (Cmd+Shift+A)

  • Real-time view of all active terminal sessions in a compact list
  • Each row shows: terminal name, project name badge (last segment of CWD), agent type, status, last activity time
  • Sub-rows (up to one shown per terminal, in priority order):
    • currentTask (gear icon) — current agent task from status-line parsing (e.g. “Reading files”). Suppressed for Claude Code (spinner verbs are decorative)
    • agentIntent (crosshair icon) — LLM-declared intent via intent: token
    • lastPrompt (speech bubble icon) — last user prompt (>= 10 words). Shown only when no agentIntent is present
  • Status color codes: green=working, yellow=waiting, red=rate-limited, gray=idle
  • Rate limit indicators with countdown timers
  • Click any row to switch to that terminal and close the dashboard
  • Relative timestamps auto-refresh (“2s ago”, “1m ago”)

3.13 Error Log Panel (Cmd+Shift+E)

  • Centralized log of all errors, warnings, and info messages across the app
  • Sources: App, Plugin, Git, Network, Terminal, GitHub, Dictation, Store, Config
  • Level filter tabs: All, Error, Warn, Info, Debug
  • Source filter dropdown to narrow by subsystem
  • Text search across all log messages
  • Each entry shows timestamp, level badge (color-coded), source tag, and message
  • Copy individual entries or all visible entries to clipboard
  • Clear button to flush the log
  • Status bar badge shows unseen error/warning count (red, resets when panel opens)
  • Global error capture: uncaught exceptions and unhandled promise rejections are automatically logged
  • Ring buffer of 1000 entries (oldest dropped when full), Rust-backed — warn/error entries survive webview reloads via push_log/get_logs Tauri commands
  • Also accessible via Command Palette: “Error log”

3.14 Plan Panel (Cmd+Shift+P)

  • Lists active plan files for the current repository from the activity store
  • Plans are detected via structured plan-file events from the output parser and via plans/ directory watcher
  • Click a plan to open it as a virtual markdown tab (frontmatter auto-stripped)
  • Plan count badge in the header
  • Repo-scoped: only shows plans belonging to the active repository
  • Auto-open: restores the active plan from .claude/active-plan.json on startup; new plans opened as background tabs on first detection (no focus change)
  • Directory watcher: monitors plans/ directory for new plan files created externally
  • Mutually exclusive with Markdown, Diff, and File Browser panels
  • Panel width and visibility persist across restarts via UIPrefsConfig

3.15 Preview Tab

  • Multi-format file previewer opened from clickable file paths, drag & drop, File Browser, or Command Palette
  • File routing handled by classifyFile() in src/utils/filePreview.ts
  • Supported formats:
    • HTML — rendered in sandboxed iframe with “Open in browser” button
    • PDF — rendered via asset protocol in embedded iframe
    • Images — PNG, JPG/JPEG, GIF, WebP, SVG, AVIF, ICO, BMP — rendered as <img> via asset protocol
    • Video — MP4, WebM, OGG, MOV — rendered as <video> with native controls
    • Audio — MP3, WAV, FLAC, AAC, M4A — rendered as <audio> with native controls
    • Text / data — TXT, JSON, CSV, LOG, XML, YAML, TOML, INI, CFG, CONF — raw text in a <pre> block
  • Header bar shows shortened file path with “Open in browser” / “Open externally” button
  • File content auto-refreshes on repository revision bumps (git change detection)
  • Uses Tauri’s convertFileSrc() asset protocol for binary files, read_external_file IPC for text content
  • CSP allows asset: and http://asset.localhost in frame-src and media-src

3.16 Focus Mode (Cmd+Alt+Enter)

  • Hides sidebar, tab bar, and all side panels to maximize the active tab’s content area
  • Toolbar and status bar remain visible for repo/branch state and mode exit
  • Session-only (not persisted across restarts)
  • Toggle again to restore the previous layout

4. Toolbar

4.1 Sidebar Toggle

  • button (left side) — same as Cmd+[
  • Hotkey hint visible during quick switcher

4.2 Branch Display

  • Center: shows repo / branch name
  • Click to open branch rename dialog

4.3 Plan File Button

  • Appears when an AI agent emits a plan file path (e.g., PLAN.md)
  • Click: .md/.mdx files open in Markdown panel; others open in IDE
  • Dismiss (×) button to hide without opening

4.4 Notification Bell

  • Bell icon with count badge when notifications are available
  • Click: opens popover listing all active notifications
  • Empty state: shows “No notifications” when nothing is pending
  • PR Updates section — types: Merged, Closed, Conflicts, CI Failed, CI Passed, Changes Requested, Ready
  • Git section — background git operation results (push, pull, fetch) with success/failure status
  • Worktrees section — worktree creation events (from MCP/agent)
  • Plugin activity sections — registered by plugins via activityStore
  • Click PR notification: opens full PR detail popover for that branch
  • Individual dismiss (×) per notification, section “Dismiss All”, auto-dismiss after 5min focused time

4.5 IDE Launcher

  • Button with current IDE icon — click to open repo/file in IDE
  • Dropdown: shows all detected installed IDEs, grouped by category
  • Categories: Code Editors, Terminals, Git Tools, System
  • File-capable editors open the focused file (from editor or MD tab); others open the repo
  • Run command button: Cmd+R (run), Cmd+Shift+R (edit & run)

5. Status Bar

5.1 Left Section

  • Zoom indicator: current font size (shown when != default)
  • Status info text (with pendulum ticker for overflow)
  • CWD path: shortened with ~/, click to copy to clipboard (shows “Copied!” feedback)
  • Unified agent badge with priority cascade:
    1. Rate limit warning (highest): count + countdown timer when sessions are rate-limited
    2. Claude Usage API ticker: live utilization from Anthropic API (click opens dashboard)
    3. PTY usage limit: weekly/session percentage from terminal output detection
    4. Agent name (lowest): icon + name of detected agent
    • Color coding: blue < 70%, yellow 70-89%, red pulsing >= 90%
    • Claude usage ticker absorbed into badge when active agent is Claude (avoids duplicate display)
  • Shared ticker area: multi-source rotating messages from plugins with source labels, counter badge (1/3 ▸), click-to-cycle, right-click popover, and priority tiers (low/normal/urgent)
  • Update badge: “Update vX.Y.Z” (click to download & install), progress percentage during download

5.2 GitHub Section (center)

  • Branch badge: name + ahead/behind counts — click for branch popover
  • PR badge: number + state color — click for PR detail popover
    • PR lifecycle filtering: CLOSED PRs hidden immediately; MERGED PRs hidden after 5 minutes of accumulated user activity
  • CI badge: ring indicator — click for PR detail popover

5.3 Right Section — Panel Toggles

  • Ideas (lightbulb icon) — Cmd+Alt+N
  • File Browser (folder icon) — Cmd+E
  • Markdown (MD icon) — Cmd+Shift+M
  • Git (diff icon) — Cmd+Shift+D (opens Git Panel)
  • Mic button (when dictation enabled): hold to record, release to transcribe

6. AI Agent Support

6.1 Supported Agents

AgentBinaryResume Command
Claude Codeclaudeclaude --resume <uuid> (session-aware) / claude --continue (fallback)
Gemini CLIgeminigemini --resume <uuid> (session-aware) / gemini --resume (fallback)
OpenCodeopencodeopencode -c
Aideraideraider --restore-chat-history
Codex CLIcodexcodex resume <uuid> (session-aware) / codex resume --last (fallback)
Ampampamp threads continue
Cursor Agentcursor-agentcursor-agent resume
Warp Ozoz
Droid (Factory)droid
Git (background)git

6.1.1 Session-Aware Resume

When an agent is detected running in a terminal, TUICommander automatically discovers its session ID from the filesystem and stores it per-terminal (agentSessionId). On restore, this enables session-specific resume instead of generic fallback commands.

  • Claude Code — Sessions stored as ~/.claude/projects/<slug>/<uuid>.jsonl; UUID from filename
  • Gemini CLI — Sessions stored in ~/.gemini/tmp/<hash>/chats/session-*.json; sessionId field from JSON
  • Codex CLI — Sessions stored in ~/.codex/sessions/YYYY/MM/DD/rollout-*-<UUID>.jsonl; UUID from filename

Discovery runs once per terminal on null→agent transition. Multiple concurrent agents are handled via a claimed_ids deduplication list. On agent exit, the stored session ID is cleared to allow re-discovery on next launch.

6.1.2 TUIC_SESSION Environment Variable

Every terminal tab has a stable UUID (tuicSession) injected as the TUIC_SESSION environment variable in the PTY shell. This UUID persists across app restarts and enables:

  • Manual session binding: claude --session-id $TUIC_SESSION to start a session bound to this tab
  • Automatic resume: On restore, TUICommander verifies if the session file exists on disk (verify_agent_session) before using --resume $TUIC_SESSION
  • UI spawn coherence: When spawning agents via the context menu, TUIC_SESSION is used as --session-id automatically
  • Custom scripts: $TUIC_SESSION is available as a stable key for any tab-specific state

6.2 Agent Detection

  • Auto-detection from terminal output patterns
  • Multi-agent status line detection via regex patterns anchored to line start: Claude Code (*//· + task text + .../), [Running] Task format, Aider (Knight Rider scanner ░█ + token reports), Codex CLI (/ bullet spinner with time suffix), Copilot CLI (// indicators), Gemini CLI (braille dots ⠋⠙⠹...)
  • Status lines rejected when they appear in diff output, code listings, or block comments
  • Brand SVG logos for each agent (fallback to capital letter)
  • Agent badge in status bar showing active agent
  • Binary detection: Rust probes well-known directories via resolve_cli() for reliable PATH resolution in desktop-launched apps
  • Foreground process detection: tcgetpgrp() on the PTY master fd, then proc_pidpath() to get the binary name. Handles versioned binary paths (e.g. Claude Code installs as ~/.local/share/claude/versions/2.1.87) by scanning parent directory names when the basename is not a known agent

6.3 Rate Limit Detection

  • Provider-specific regex patterns detect rate limit messages
  • Status bar warning with countdown timer
  • Per-session tracking: rate-limit events are only accepted for sessions where agent activity has been detected (prevents false warnings in plain shell sessions)
  • Auto-expire: rate limits are cleared automatically after retry_after_ms (or 120s default) without requiring agent output

6.4 Question Detection

  • Recognizes interactive prompts (yes/no, multiple choice, numbered options)
  • Tab dot turns orange (pulsing) when awaiting input; sidebar branch icon shows ? in orange
  • Prompt overlay: keyboard navigation (↑/↓, Enter, number keys 1-9, Escape)
  • Two detection strategies run in priority order:
    1. Screen-based (Strategy 1): reads the live terminal screen, finds the last chat line above the prompt box (delimited by separator lines), checks if it ends with ?. Works with Claude Code, Codex ( prompt), and Gemini (> prompt) layouts
    2. Silence-based (Strategy 2, fallback): if terminal output stops for 10s after a line ending with ?, the session is treated as awaiting input
  • Stale candidate clearing: candidates that fail screen verification are purged so the same question can re-fire in a future agent cycle
  • Echo suppression: user-typed input echoed by PTY is ignored for 500ms to prevent false question detection
  • extract_question_line() scans all changed rows (not just the last) for question text, applied in both normal and headless reader threads
  • Question state auto-clears when a status-line event fires (agent is actively working, so it’s no longer awaiting input)

6.5 Usage Limit Detection

  • Claude Code weekly and session usage percentage (from PTY output patterns)
  • Color-coded badge in status bar (blue < 70%, yellow 70-89%, red pulsing >= 90%)
  • Integrated into unified agent badge (see section 5.1)

6.6 Claude Usage Dashboard

  • Native SolidJS component (not a plugin panel — renders as a first-class tab)
  • Opens via status bar agent badge click or Cmd+Shift+A action
  • Rate Limits section: Live utilization bars from Anthropic OAuth usage API
    • 5-Hour, 7-Day, 7-Day Opus, 7-Day Sonnet, 7-Day Cowork buckets
    • Color-coded bars: green < 70%, yellow 70-89%, red >= 90%
    • Reset countdown per bucket
  • Usage Over Time chart: SVG line chart of token usage over 7 days
    • Input tokens (blue) and output tokens (red) stacked area
    • Interactive hover crosshair with tooltip
  • Insights: Session count, message totals, input/output tokens, cache stats, tokens-per-hour metric (based on real active hours from session timestamps)
  • Activity heatmap: 52-week GitHub-style contribution grid
    • Tooltip shows date, message count, and top 3 projects
  • Model Usage table: Per-model breakdown (messages, input, output, cache)
  • Projects breakdown: Per-project token usage with click to filter
  • Scope selector: Filter all analytics by project slug
  • Auto-refresh: API data polled every 5 minutes
  • Rust data layer: Incremental JSONL parsing of ~/.claude/projects/*/ transcripts
    • File-size-based cache (only new bytes parsed on each scan)
    • Cache persisted to disk as JSON for fast restarts

6.7 Intent Event Tracking

  • Agents declare work phases via intent: text (Title) tokens at column 0, colorized dim yellow in terminal output
  • Colorization is agent-gated (only applied in sessions with a detected agent) to prevent false positives
  • Structural tokens stripped from log lines served to PWA/REST consumers via LogLine::strip_structural_tokens()
  • Structured Intent events emitted for LLM-declared work phase tracking
  • Centralized debounced busy signal with completion notifications for accurate idle/active status

6.8 API Error Detection

  • Detects API errors (server errors, auth failures) from agent output and provider-level JSON error responses
  • Covers Claude Code, Aider, Codex CLI, Gemini CLI, Copilot, and raw API error JSON from providers (OpenAI, Anthropic, Google, OpenRouter, MiniMax)
  • Triggers error notification sound and logs to the Error Log Panel

6.9 Agent Configuration (Settings > Agents)

  • Agent list: All supported agents with availability status and version detection
  • Run configurations: Named command templates per agent (binary, args, env vars)
  • Default config: One run config per agent marked as default for quick launching
  • MCP bridge install: One-click install/remove of tui-mcp-bridge into agent’s native MCP config file
  • Supported MCP agents: Claude, Cursor, Windsurf, VS Code, Zed, Amp, Gemini
  • Edit agent config: Opens agent’s own configuration file in the user’s preferred IDE
  • Context menu integration: Right-click terminal > Agents submenu with per-agent run configurations
  • Busy detection: Agents submenu disabled when a process is already running in the active terminal
  • Environment Flags — Per-agent environment variables injected into every new terminal session. Configure in Settings > Agents > expand an agent > Environment Flags. Useful for setting feature flags like CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 without manual export.

6.10 Agent Teams

  • Purpose: Enables Claude Code’s Agent Teams feature to use TUIC tabs instead of tmux panes
  • Approach: Environment variable CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 injected into PTY sessions, which unlocks Claude Code’s TeamCreate/TaskCreate/SendMessage tools. Agent spawning uses direct MCP tool calls (agent spawn) instead of the deprecated it2 shim
  • Session lifecycle events: MCP-spawned sessions emit session-created and session-closed events so they automatically appear as tabs and clean up on exit
  • Settings toggle: Settings > Agents > Agent Teams
  • Suggest follow-ups: Agents can propose follow-up actions via suggest: A | B | C tokens, displayed as floating chip bar
  • Deprecated: The it2 shim approach (iTerm2 CLI emulation) is commented out — superseded by direct MCP tool spawning

6.11 Suggest Follow-up Actions

  • Protocol: Agents emit suggest: action1 | action2 | action3 at column 0 after completing a task
  • Token concealment: Suggest tokens are concealed in terminal output via line erasure or space replacement — the raw token never appears on screen. Concealment is agent-gated
  • Desktop: Floating chip bar (SuggestOverlay) above terminal with larger buttons and keyboard shortcut badges (19 to select, Esc to dismiss). Auto-dismiss after 30s, on typing, or on Esc
  • Mobile: Horizontal scrollable pill buttons above CommandInput in SessionDetailScreen
  • Action: Clicking a chip (or pressing its number key) sends the text to the PTY via write_pty
  • Settings: Configurable via Settings > Agents > Show suggested follow-up actions

6.12 Slash Menu Detection

  • When the user types / in a terminal, slash_mode activates and the output parser scans the bottom screen rows for slash command menus
  • Detection: 2+ consecutive rows starting with /command patterns, with highlight for the selected item
  • Produces ParsedEvent::SlashMenu { items } — used by mobile PWA to render a native bottom-sheet overlay
  • slash_mode cleared on user-input events and status-line events

6.13 Inter-Agent Messaging

  • New messaging MCP tool for agent-to-agent coordination when multiple agents are spawned in parallel
  • Identity: Each agent uses its $TUIC_SESSION env var (stable tab UUID) as its messaging identity
  • Actions: register (announce presence), list_peers (discover other agents), send (message a peer by tuic_session), inbox (poll for messages)
  • Dual delivery: Real-time push via MCP notifications/claude/channel over SSE when the client supports channels; polling fallback via inbox always available
  • Channel support: TUICommander declares experimental.claude/channel capability; spawned Claude Code agents automatically get --dangerously-load-development-channels server:tuicommander
  • Lifecycle: Peer registrations cleaned up on MCP session delete and TTL reap; PeerRegistered/PeerUnregistered events broadcast via event bus for frontend visibility
  • Limits: 64 KB max message size, 100 messages per inbox (FIFO eviction), optional project filtering for list_peers
  • TUICommander acts as the messaging hub — no external daemon needed

6.14 AI Chat Panel (Cmd+Alt+A)

  • Conversational AI companion docked on the right, streaming markdown with syntax-highlighted code blocks. Every code block has Run (sends to the attached terminal via sendCommand()), Copy, and Insert actions
  • Multi-provider: Ollama (local, auto-detected on localhost:11434 with live model list), Anthropic, OpenAI, OpenRouter, custom OpenAI-compatible endpoint. Provider abstraction via genai crate
  • Per-turn terminal context: last context_lines rows from VtLogBuffer (ANSI-stripped, alt-screen suppressed), SessionState, recent ParsedEvents, git branch/diff. Attach / detach / auto-attach terminal via header dropdown
  • API keys stored in OS keyring (service tuicommander-ai-chat, user api-key) — masked with eye-toggle in Settings
  • Streaming via Tauri Channel<ChatStreamEvent> (chunk/end/error); cancellable mid-stream
  • Conversation persistence: save / load / delete with in-memory cap of 100 messages per conversation. Files at <config_dir>/ai-chat-conversations/<id>.json
  • Terminal context menu: Send selection to AI Chat, Explain this error. Toolbar toggle + hotkey
  • Full user guide: docs/user-guide/ai-chat.md

6.15 AI Agent Loop (ReAct)

  • Autonomous loop that observes and acts in a terminal. Same panel as AI Chat, mode toggle in the header
  • Six tools exposed to the model: read_screen, send_input, send_key, wait_for (regex or stability), get_state, get_context
  • Safety gates via the SafetyChecker trait — three verdicts: Allow, NeedsApproval { reason }, Block { reason }. Destructive commands (rm -rf, git reset --hard, git push --force, DROP TABLE, dd of=, …) surface a pending-approval card; hard-coded blocks refuse patterns like rm -rf /
  • Pause / resume / cancel between iterations with clean state transitions. Tool-call cards collapse/expand in the panel. Conversation schema v2 persists tool-call records alongside messages
  • Session knowledge store (Level 3): command outcomes (exit code, duration, CWD, classification, output snippet), auto-correlated error→fix pairs, CWD history, tui_apps_seen, terminal mode. Injected into the agent system prompt as a compact markdown summary
  • OSC 133 semantic prompts feed exact exit codes when the shell supports them; a silence-timer fallback records Inferred outcomes otherwise. Persisted to <config_dir>/ai-sessions/<session_id>.json with a 2 s debounced flush
  • SessionKnowledgeBar — collapsible footer under the panel showing live command count, last 5 outcomes with kind badges, recent errors with inferred error_type, TUI mode indicator. A History button opens the knowledge history overlay (two-pane sessions/detail browser with full-text search, errors-only filter, and 24h/7d/30d date window)
  • Experimental AI block enrichment (opt-in, Settings > AI Chat) — after each completed OSC 133 D block, a bounded mpsc worker asks the active AI provider for a one-line semantic_intent and stamps it onto the CommandOutcome (identified by stable id: u64). Rate-limited ~10/min, silent drop on full queue, never blocks the PTY path
  • TUI app detection via alternate-screen tracking (ESC[?1049h/l). TerminalMode::FullscreenTui { app_hint, depth } is set when the terminal enters vim/htop/lazygit/less/tmux/…; the agent adapts (prefers send_key + wait_for over line-oriented send_input)
  • External MCP surface — same six tools exposed as ai_terminal_read_screen, ai_terminal_send_input, ai_terminal_send_key, ai_terminal_wait_for, ai_terminal_get_state, ai_terminal_get_context. Input operations always require user confirmation and are rejected while the internal agent loop is active on the target session

6.16 ChoicePrompt Detection

  • New ParsedEvent::ChoicePrompt { title, options, dismiss_key, amend_key } recognises Claude-Code-style numbered confirmation menus (footer matches Esc to cancel · Tab to amend)
  • Options parsed by regex with optional cursor marker (, , >). Title heuristics require ? or a verb prefix (proceed, confirm, do you want, …) to avoid matching Markdown numbered lists. Minimum two options
  • Destructive labels (no, cancel, reject, abort, deny, don't) flagged for styling
  • Piped into SessionState.choice_prompt; dispatched to plugins via pluginRegistry.dispatchStructuredEvent("choice-prompt", …); rendered as PWA overlay
  • Single-key replies routed through sendPtyKey() (src/utils/sendCommand.ts) — never text + \r. Desktop listener plays a warning sound when the prompt arrives on an inactive tab

7. Git Integration

7.1 Repository Info

  • Branch name, remote URL, ahead/behind counts
  • Read directly from .git/ files (no subprocess for basic info)
  • Repo watcher: monitors .git/index, .git/refs/, .git/HEAD, .git/MERGE_HEAD for changes

7.2 Worktrees

  • Auto-creation on branch select (non-main branches)
  • Configurable storage strategies: sibling (__wt), app directory, inside-repo (.worktrees/), or Claude Code default (.claude/worktrees/)
  • Sci-fi themed auto-generated names
  • Three creation flows: dialog (with base ref dropdown), instant (auto-name), right-click branch (quick-clone with hybrid {branch}--{random} name)
  • Base ref selection: choose which branch to start from when creating new worktrees
  • Per-repo settings: storage strategy, prompt on create, delete branch on remove, auto-archive, orphan cleanup, PR merge strategy, after-merge behavior
  • Setup script: runs once after creation (e.g., npm install)
  • Archive script: runs before a worktree is archived or deleted; non-zero exit blocks the operation
  • Merge & Archive: right-click → merge branch into main, then archive or delete based on setting
  • External worktree detection: monitors .git/worktrees/ for changes from CLI or other tools
  • Remove via sidebar × button or context menu (with confirmation)
  • Worktree Manager panel (Cmd+Shift+W or Command Palette → “Worktree manager”):
    • Dedicated overlay listing all worktrees across all repos with metadata: branch name, repo badge, PR state (open/merged/closed), dirty stats, last commit timestamp
    • Orphan worktree detection with warning badge and Prune action
    • Repo filter pills and text search for branch names
    • Multi-select with checkboxes and select-all for batch operations
    • Batch delete and batch merge & archive
    • Single-row actions: Open Terminal, Merge & Archive, Delete (disabled on main worktrees)

7.3 Auto-Fetch

  • Per-repo configurable interval (5/15/30/60 minutes, default: disabled)
  • Background git fetch --all via non-interactive subprocess
  • Bumps revision counter to refresh branch stats and ahead/behind counts
  • Errors logged to appLogger, never blocking
  • Master-tick architecture: single 1-minute timer checks all repos

7.4 Unified Repo Watcher

  • Single watcher per repository monitoring the entire working tree recursively (replaces separate HEAD/index watchers)
  • Uses raw notify::RecommendedWatcher with manual per-category trailing debounce
  • Event categories: Git (HEAD, refs, index, MERGE_HEAD), WorkTree (source files), Config (app config changes)
  • Each category has its own debounce window — git metadata changes propagate faster than file edits
  • Respects .gitignore rules — ignored paths do not trigger refreshes
  • Gitignore hot-reload: editing .gitignore rebuilds the ignore filter without restarting the watcher
  • When a terminal runs git checkout -b new-branch in the main working directory (not a worktree), the sidebar renames the existing branch entry in-place (preserving all terminal state) instead of creating a duplicate

7.5 Diff

  • Working tree diff and per-commit diff via Git Panel Changes tab
  • Per-file diff counts (additions/deletions) shown inline in Changes tab
  • Click a file row to view its diff
  • Side-by-side (split), unified (inline), and scroll (all files) view modes — toggle in toolbar, preference persisted
  • Scroll mode (all-files diff) — shows every changed file (staged + unstaged) in a continuous scrollable view with collapsible file sections, per-file addition/deletion stats, sticky header with totals, and clickable filenames that open in the editor. Reactively reloads on git operations via revision tracking
  • Auto-unified for new/deleted files — split view is forced to unified when the diff is one-sided
  • Word-level diff highlighting via @git-diff-view/solid with virtualized rendering
  • Hunk-level restore — hover a hunk header to reveal a revert button (discard for working tree, unstage for staged)
  • Line-level restore — click individual addition/deletion lines to select them (shift+click for ranges), then restore only the selected lines via partial patch
  • Text selection and copy enabled in diff panels (user-select: text)
  • Cmd+F search in diff tabs via SearchBar + DomSearchEngine
  • Submodule entries are filtered from working tree status (not shown as regular files)
  • Standalone DiffPanel removed in v0.9.0 (see section 3.2)

8. GitHub Integration

8.1 PR Monitoring

  • GraphQL API (replaces gh CLI for data fetching)
  • PR badge colors: green (open), purple (merged), red (closed), gray (draft)
  • Merge state: Ready to merge, Checks failing, Has conflicts, Behind base, Blocked, Draft
  • Review state: Approved, Changes requested, Review required
  • PR lifecycle rules: CLOSED PRs hidden from sidebar and status bar; MERGED PRs shown for 5 minutes of accumulated user activity then hidden
  • Auto-show PR popover filters out CLOSED and MERGED PRs (configurable in Settings > General)

8.2 CI Checks

  • Ring indicator with proportional segments
  • Individual check names and status in PR detail popover
  • Labels with GitHub-matching colors

8.3 PR Detail Popover

  • Title, number, link to GitHub
  • Author, timestamps, state, merge readiness, review decision
  • CI check details, labels, line changes, commit count
  • View Diff button: opens PR diff as a dedicated panel tab with collapsible file sections, dual line numbers, and color-coded additions/deletions
  • Merge button: visible when PR is open, approved, CI green — merges via GitHub API. Merge method auto-detected from repo-allowed methods; auto-fallback to squash on HTTP 405 rejection
  • Approve button: submit an approving review via GitHub API (remote-only PRs)
  • Post-merge cleanup dialog: after merge, offers checkable steps (switch to base, pull, delete local/remote branch)
  • Review button: if the branch’s active agent has a run config named “review”, spawns a terminal running the interpolated command with {pr_number}, {branch}, {base_branch}, {repo}, {pr_url}. Hidden when no matching config exists
  • Triggered from: sidebar PR badge, status bar PR badge, status bar CI badge, toolbar notification bell

8.4 CI Auto-Heal

  • When CI checks fail on a branch with an active agent terminal, auto-heal can fetch failure logs and inject them into the agent
  • Toggle per-branch via checkbox in PR detail popover (visible when CI checks are failing)
  • Fetches logs via gh run view --log-failed, truncated to ~4000 chars
  • Waits for agent to be idle/awaiting input before injecting
  • Max 3 attempts per failure cycle, then stops and logs a warning
  • Attempt counter visible in PR detail popover
  • Status tracked per-branch in BranchState.ciAutoHeal

8.5 PR Notifications

  • Types: Merged, Closed, Conflicts, CI Failed, Changes Requested, Ready
  • Toolbar bell with count badge
  • Individual dismiss or dismiss all
  • Click to open PR detail popover

8.5 Merge PR via GitHub API

  • Merge PRs directly from TUICommander without switching to GitHub web
  • Configurable merge strategy per repo: merge commit, squash, or rebase (Settings > Repository > Worktree tab)
  • Merge method auto-detected from repo’s allowed methods via GitHub API (get_repo_merge_methods); auto-fallback to squash on HTTP 405 rejection
  • Triggered from: PR detail popover (local branches), remote-only PR popover, Merge & Archive workflow (sidebar context menu)
  • Post-merge cleanup dialog: sequential steps executed via Rust backend (not PTY — terminal may be occupied by AI agent)
    • Switch to base branch (auto-stash if dirty — inline warning shown with “Unstash after switch” checkbox)
    • Pull base branch (ff-only)
    • Close terminals + delete local branch (safe delete, refuses default branch)
    • Delete remote branch (gracefully handles “already deleted”)
    • Steps are checkable — user can toggle which to execute
    • Per-step status reporting: pending → running → success/error
  • After-merge behavior setting for worktrees: archive (auto-archive), delete (remove), ask (show dialog)
  • When afterMerge=ask: unified cleanup dialog includes an archive/delete worktree step (with inline selector) alongside branch cleanup steps — replaces the old 3-button MergePostActionDialog

8.6 Auto-Delete Branch on PR Close

  • Per-repo setting: Off (default) / Ask / Auto
  • Triggered when GitHub polling detects PR merged or closed transition
  • If branch has a linked worktree, removes worktree first then deletes branch
  • Safety: never deletes default/main branch; dirty worktrees always escalate to ask mode
  • Uses safe git branch -d (refuses unmerged branches)
  • Deduplication prevents double-firing on the same PR

8.7 GitHub Issues Panel

  • Issues displayed in a collapsible section within the GitHub panel alongside PRs
  • Filter modes: Assigned (default), Created, Mentioned, All, Disabled
  • Filter persisted in app config (issue_filter field) and configurable in Settings > GitHub
  • Each issue shows: number, title, state (OPEN/CLOSED), author, labels, assignees, milestone, comment count, timestamps
  • Labels rendered with GitHub-matching colors (background opacity 0.7, contrast-aware text color)
  • Issue actions: Open in GitHub, Close/Reopen, Copy issue number
  • Expand accordion to see full details (milestone, assignees, labels, timestamps)
  • Skeleton loading rows shown during first fetch
  • Empty state message when no issues match the filter
  • MCP HTTP endpoint: GET /repo/issues?path=... returns issues JSON
  • MCP HTTP endpoint: POST /repo/issues/close closes an issue

8.8 Polling

  • Active window: every 30 seconds
  • Hidden window: every 2 minutes
  • API budget: ~2 calls/min/repo

8.9 Token Resolution

  • Priority: GH_TOKEN env → GITHUB_TOKEN env → OAuth keyring token → gh_token crate → gh auth token CLI
  • gh_token crate with empty-string bug workaround
  • Fallback to gh auth token CLI

8.10 OAuth Device Flow Login

  • One-click GitHub authentication from Settings > GitHub tab
  • Uses GitHub OAuth App Device Flow (no client secret, works on desktop)
  • Token stored in OS keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service)
  • Requested scopes: repo, read:org
  • Shows user avatar, login name, and token source after authentication
  • Logout removes OAuth token, falls back to env/gh CLI
  • On 401: auto-clears invalid OAuth token and prompts re-auth

9. Voice Dictation

9.1 Whisper Inference

  • Local processing via whisper-rs (no cloud)
  • macOS: GPU-accelerated via Metal
  • Linux/Windows: CPU-only

9.2 Models

ModelSizeQuality
tiny~75 MBLow
base~140 MBFair
small~460 MBGood
medium~1.5 GBVery good
large-v3-turbo~1.6 GBBest (recommended)

9.3 Push-to-Talk

  • Default hotkey: F5 (configurable, registered globally)
  • Mic button in status bar: hold to record, release to transcribe
  • Transcribed text inserts into the focused input element (textarea, input, contenteditable); falls back to active terminal PTY when no text input has focus. Focus target captured at key-press time.

9.4 Streaming Transcription

  • Real-time partial results during push-to-talk via adaptive sliding windows
  • First partial within ~1.5s, subsequent windows grow to 3s for quality
  • VAD energy gate skips silence windows (prevents hallucination)
  • Floating toast shows partial text above status bar during recording
  • 200ms audio window overlap (keep_ms) carries context across windows for continuity
  • Final transcription pass on full captured audio at key release

9.5 Microphone Permission Detection (macOS)

  • On first use, checks microphone permission via macOS TCC (Transparency, Consent, and Control) framework
  • Permission states: NotDetermined (will prompt), Authorized, Denied, Restricted
  • If denied, shows a dialog guiding the user to System Settings > Privacy & Security > Microphone with an “Open Settings” button
  • Linux/Windows: always returns Authorized (no TCC framework)

9.6 Configuration

  • Enable/disable, hotkey, language (auto-detect or explicit), model download
  • Audio device selection
  • Text correction dictionary (e.g., “new line” → \n)
  • Auto-send — Enable in Settings > Services > Dictation to automatically submit (press Enter) after transcription completes.

10. Prompt Library

10.1 Access

  • Cmd+K to open drawer
  • Toolbar button

10.2 Prompts

  • Create, edit, delete saved prompts
  • Variable substitution: {{variable_name}}
  • Built-in variables: {{diff}}, {{changed_files}}, {{repo_name}}, {{branch}}, {{cwd}}
  • Custom variables prompt user for input
  • Categories: Custom, Recent, Favorites
  • Pin prompts to top
  • Search by name or content

10.3 Keyboard Navigation

  • ↑/↓: navigate, Enter: insert (restores terminal focus), Ctrl+N: new, Ctrl+E: edit, Ctrl+F: toggle favorite, Esc: close

10.4 Run Commands

  • Cmd+R: run saved command for active branch
  • Cmd+Shift+R: edit command before running
  • Configure per-repo in Settings → Repository → Scripts

10.5 Smart Prompts

AI automation layer with 24 built-in context-aware prompts. Each prompt includes a description explaining what it does. Prompts auto-resolve git context variables and execute via inject (PTY write), shell script (direct run), headless (one-shot subprocess), or API (direct LLM call) mode.

  • Open: Cmd+K or toolbar lightning bolt button
  • Drawer with category filtering (All/Custom/Recent/Favorites), search by name/description, and enable/disable toggles
  • Prompt rows show inline badges: execution mode (inject/shell/headless/api), built-in, placement tags
  • Prompts are context-aware: 31 variables auto-resolved from git, GitHub, and terminal state
  • Variable Input Dialog: unresolved variables show a compact form with variable name + description before execution
  • Edit Prompt dialog: full editor with name, description, content textarea, variable insertion dropdown (grouped by Git/GitHub/Terminal with descriptions), placement checkboxes, execution mode + auto-execute side-by-side, keyboard shortcut capture
  • Auto-execute: when enabled, inject-mode prompts send Enter immediately via agent-aware sendCommand; when disabled, text is pasted without Enter so the user can review before sending
  • API execution mode: calls LLM providers directly via HTTP API (genai crate) without terminal or agent CLI. Per-prompt system prompt field. Output routed via the same outputTarget options (clipboard, commit-message, toast, panel). Tauri-only (PWA shows “requires desktop app”)
  • LLM API config (Settings > Agents): global provider/model/API key for all API-mode prompts. Supports OpenAI, Anthropic, Gemini, OpenRouter, Ollama, and any OpenAI-compatible endpoint via custom base URL. API key stored in OS keyring. Test button validates connection

10.6 Built-in Prompts by Category

CategoryPrompts
Git & CommitSmart Commit, Commit & Push, Amend Commit, Generate Commit Message
Code ReviewReview Changes, Review Staged, Review PR, Address Review Comments
Pull RequestsCreate PR, Update PR Description, Generate PR Description
Merge & ConflictsResolve Conflicts, Merge Main Into Branch, Rebase on Main
CI & QualityFix CI Failures, Fix Lint Issues, Write Tests, Run & Fix Tests
InvestigationInvestigate Issue, What Changed?, Summarize Branch, Explain Changes
Code OperationsSuggest Refactoring, Security Audit

10.7 Context Variables

Variables are resolved from the Rust backend (resolve_context_variables) and frontend stores:

VariableSourceDescription
{branch}gitCurrent branch name
{base_branch}gitDetected default branch (main/master/develop)
{repo_name}gitRepository directory name
{repo_path}gitFull filesystem path to the repository root
{repo_owner}gitGitHub owner parsed from remote URL
{repo_slug}gitRepository name parsed from remote URL
{diff}gitFull working tree diff (truncated to 50KB)
{staged_diff}gitStaged changes diff (truncated to 50KB)
{changed_files}gitShort status output
{dirty_files_count}gitNumber of modified files (derived from changed_files)
{commit_log}gitLast 20 commits (oneline)
{last_commit}gitLast commit hash + message
{conflict_files}gitFiles with merge conflicts
{stash_list}gitStash entries
{branch_status}gitAhead/behind remote tracking branch
{remote_url}gitRemote origin URL
{current_user}gitGit config user.name
{pr_number}GitHub storePR number for current branch
{pr_title}GitHub storePR title
{pr_url}GitHub storePR URL
{pr_state}GitHub storePR state (OPEN, MERGED, CLOSED)
{pr_author}GitHub storePR author username
{pr_labels}GitHub storePR labels (comma-separated)
{pr_additions}GitHub storeLines added in PR
{pr_deletions}GitHub storeLines deleted in PR
{pr_checks}GitHub storeCI check summary (passed/failed/pending)
{merge_status}GitHub storePR mergeable status
{review_decision}GitHub storePR review decision
{agent_type}terminal storeActive agent type (claude, gemini, etc.)
{cwd}terminal storeActive terminal working directory
{issue_number}manualPrompted from user at execution time

10.8 Execution Modes

  • Inject (default): writes the resolved prompt text into the active terminal’s PTY. Checks agent idle state before sending (configurable via requiresIdle). Appends newline for auto-execution
  • Shell script: executes the prompt content directly as a shell script via execute_shell_script Tauri command. No agent involved — runs content as-is via sh -c (macOS/Linux) or cmd /C (Windows) in the repo directory. Output routed via outputTarget. 60-second timeout cap. No prerequisites (no terminal, agent, or API config needed)
  • Headless: runs a one-shot subprocess via execute_headless_prompt Tauri command. Requires a per-agent headless template configured in Settings → Agents (e.g. claude -p "{prompt}"). Output routed to clipboard or toast depending on outputTarget. Falls back to inject in PWA mode. 5-minute timeout cap

10.9 UI Integration Points

LocationPrompts shownTrigger
Toolbar dropdownAll enabled prompts with toolbar placementCmd+Shift+K or lightning bolt button
Git Panel — Changes tabSmartButtonStrip with git-changes placementInline buttons above changed files
PR Detail PopoverSmartButtonStrip with pr-popover placementInline buttons in PR detail view
Command PaletteAll prompts with Smart: prefixCmd+P then type “Smart”
Branch context menuPrompts with git-branches placementRight-click branch in Branches tab

10.10 Smart Prompts Management (Cmd+Shift+K Drawer)

  • All prompt management consolidated in the Cmd+Shift+K drawer (Settings tab removed)
  • Enable/disable individual prompts via toggle button on each row
  • Edit prompt: opens modal with name, description, content, variable dropdown, placement, execution mode, auto-execute, keyboard shortcut
  • Variable insertion dropdown below content textarea: grouped by Git/GitHub/Terminal, click to insert {variable} at cursor
  • Create custom smart prompts with + New Prompt button
  • Built-in prompts show a “Reset to Default” button when content is overridden

10.11 Headless Template Configuration

  • Settings → Agents → per-agent “Headless Command Template” field
  • Template uses {prompt} placeholder for the resolved prompt text
  • Example: claude -p "{prompt}", gemini -p "{prompt}"
  • Required for headless execution mode; without it, headless prompts fall back to inject

11. Settings

11.1 General

  • Language, Default IDE, Shell
  • Confirmations: quit, close tab (only when a process is running — agents or busy shell; idle shells close immediately)
  • Power management: prevent sleep when busy
  • Updates: auto-check, check now
  • Git integration: auto-show PR popover
  • Experimental Features: master toggle + per-feature sub-flags (AI Chat)
  • Repository defaults: base branch, file handling, setup/run scripts, worktree defaults (storage strategy, prompt on create, etc.)

11.2 Appearance

  • Terminal theme: multiple themes, color swatches
  • Terminal font: 11 bundled monospace fonts (JetBrains Mono default)
  • Default font size: 8-32px slider
  • Split tab mode: separate / unified
  • Max tab name length: 10-60 slider
  • Repository groups: create, rename, delete, color-coded
  • Reset panel sizes: restore sidebar and panel widths to defaults

11.3 Services

  • HTTP API server: always active on IPC listener (Unix domain socket on macOS/Linux, named pipe \\.\pipe\tuicommander-mcp on Windows). TCP port only for remote access
  • MCP connection info: bridge sidecar auto-installs configs for supported agents (Claude Code, Cursor, etc.)
  • TUIC native tool toggles: enable/disable individual MCP tools (session, agent, repo, ui, plugin_dev_guide, config, debug) to restrict what AI agents can access
  • MCP Upstreams: add/edit/remove upstream MCP servers (HTTP or stdio with optional cwd), per-upstream enable/disable, reconnect, credential storage via OS keyring, live status dots, tool count and metrics. Saved upstreams auto-connect on boot
  • MCP Per-Repo Scoping: each repo can define which upstream MCP servers are relevant via an allowlist in repo settings (3-layer: per-repo > .tuic.json > defaults). Null/empty allowlist = all servers. Quick toggle via Cmd+Shift+M popup
  • Remote access: port, username, password (bcrypt hash), URL display, QR code, token duration, IPv6 dual-stack, LAN auth bypass
  • Voice dictation: full setup (see section 9)

11.4 Repository Settings (per-repo)

  • Display name
  • Worktree tab: storage strategy, prompt on create, delete branch on remove, auto-archive, orphan cleanup, PR merge strategy, after-merge action (each overridable from global defaults)
  • Scripts tab: setup script (post-worktree), run script (Cmd+R), archive script (pre-archive/delete hook)
  • Repo-local config: .tuic.json in repo root provides team-shared settings. Three-tier precedence: .tuic.json > per-repo app settings > global defaults. Scripts (setup, run, archive) are intentionally excluded from .tuic.json merging — arbitrary script execution by a checked-in file poses a security risk; scripts are always sourced from the local per-repo app settings only

11.5 Notifications

  • Master toggle, volume (0-100%)
  • Per-event: question, error, completed, warning, info
  • Test buttons per sound
  • Reset to defaults

11.6 Keyboard Shortcuts

  • Settings > Keyboard Shortcuts tab (Cmd+, to open Settings), also accessible from Help > Keyboard Shortcuts
  • All app actions listed with their current keybinding
  • Click the pencil icon to rebind — inline key recorder with pulsing accent border
  • Conflict detection: warns when the new combo is already bound to another action, with option to replace
  • Overridden shortcuts highlighted with accent color; per-shortcut reset icon to revert to default
  • “Reset all to defaults” button at the bottom
  • Custom bindings stored in keybindings.json in the platform config directory
  • Auto-populated from actionRegistry.ts (ACTION_META map) — new actions appear automatically
  • Global Hotkey: configurable OS-level shortcut to toggle window visibility from any application. Set in the “Global Hotkey” section at the top of the Keyboard Shortcuts tab. No default — user must configure. Toggle: hidden/minimized → show+focus, visible but unfocused → focus, focused → instant hide (no dock animation). Cmd and Ctrl are distinct modifiers. Uses tauri-plugin-global-shortcut (no Accessibility permission required on macOS). Hidden in browser/PWA mode.

11.7 Agents

  • See 6.9 Agent Configuration for full details
  • Claude Usage Dashboard enable/disable toggle (under Claude agent section)

12. Persistence

12.1 Rust Config Backend

All data persisted to platform config directory via Rust:

  • app_config.json — general settings
  • notification_config.json — sound settings
  • ui_prefs.json — sidebar visibility/width
  • repo_settings.json — per-repo worktree/script settings
  • repositories.json — repository list, groups, branches
  • agents.json — per-agent run configurations
  • prompt_library.json — saved prompts
  • notes.json — ideas panel data
  • dictation_config.json — dictation settings
  • .tuic.json — repo-root team config (read-only from app, highest precedence for overridable fields)
  • claude-usage-cache.json — incremental session transcript parse cache

12.2 Hydration Safety

  • save() blocks before hydrate() completes to prevent data loss

13. Cross-Platform

13.1 Supported Platforms

  • macOS (primary), Windows, Linux

13.2 Platform Adaptations

  • CmdCtrl key abstraction
  • resolve_cli(): probes well-known directories when PATH unavailable (release builds)
  • Windows: cmd.exe shell escaping, CreateToolhelp32Snapshot for process detection
  • IDE detection: .app bundles (macOS), registry entries (Windows), PATH probing (Linux)

14. System Features

14.1 Auto-Update

  • Check for updates on startup via tauri-plugin-updater
  • Status bar badge with version
  • Download progress percentage
  • One-click install and relaunch
  • Menu: Check for Updates (app menu and Help menu)

14.2 Sleep Prevention

  • keepawake integration prevents system sleep while agents are working
  • Configurable in Settings

14.3 Splash Screen

  • Branded loading screen on app start

14.4 Confirmation Dialogs

  • In-app ConfirmDialog component replaces native Tauri ask() dialogs
  • Dark-themed to match the app (native macOS sheets render in light mode)
  • useConfirmDialog hook provides a confirm()Promise<boolean> API
  • Pre-built helpers: confirmRemoveWorktree(), confirmCloseTerminal(), confirmRemoveRepo()
  • Keyboard support: Enter to confirm, Escape to cancel

14.5 Error Handling

  • ErrorBoundary crash screen with recovery UI
  • WebGL canvas fallback (graceful degradation)
  • Error classification with backoff calculation

14.6 MCP & HTTP Server

  • REST API on localhost for external tool integration
  • Exposes terminal sessions, git operations, agent spawning
  • WebSocket streaming, Streamable HTTP transport
  • Used by Claude Code, Cursor, and other tools via MCP protocol
  • tuic-bridge ships as a Tauri sidecar; auto-installs MCP configs on first launch for Claude Code, Cursor, Windsurf, VS Code, Zed, Amp, Gemini
  • Local connections use Unix domain socket (<config_dir>/mcp.sock) on macOS/Linux or named pipe (\\.\pipe\tuicommander-mcp) on Windows; TCP port reserved for remote access only
  • Unix socket lifecycle is crash-safe: RAII guard removes the socket file on Drop; bind retries 3× (×100 ms) removing any stale file before each attempt; liveness check uses a real connect() probe so a dead socket from a crashed run never blocks MCP tool loading

14.7 Cross-Repo Knowledge Base

  • Knowledge base functionality is available via the mdkb MCP upstream server (configure in MCP Upstreams settings)
  • Provides hybrid BM25 + semantic search across docs, code, symbols, and memory
  • Call graph queries (calls, callers, impact analysis) via code_graph tool
  • Requires mdkb binary on PATH (installed separately)

14.8 macOS Dock Badge

  • Badge count for attention-requiring notifications (questions, errors)

14.9 Tailscale HTTPS

  • Auto-detects Tailscale daemon and FQDN via tailscale status --json (cross-platform)
  • Provisions TLS certificates from Tailscale Local API (Unix socket on macOS/Linux, CLI on Windows)
  • HTTP+HTTPS dual-protocol on same port via axum-server-dual-protocol
  • Graceful fallback: HTTP-only when Tailscale unavailable or HTTPS not enabled
  • QR code uses https:// scheme with Tailscale FQDN when TLS active
  • Background cert renewal every 24h with hot-reload via RustlsConfig::reload_from_pem()
  • Session cookie gets Secure flag on TLS connections
  • Settings panel shows Tailscale status with actionable guidance

15. Keyboard Shortcut Reference

Terminal

ShortcutAction
Cmd+TNew terminal tab
Cmd+WClose tab / close active split pane
Cmd+Shift+TReopen last closed tab
Cmd+1Cmd+9Switch to tab by number
Ctrl+Tab / Ctrl+Shift+TabNext / previous tab
Cmd+LClear terminal
Cmd+CCopy selection
Cmd+VPaste to terminal
Cmd+HomeScroll to top
Cmd+EndScroll to bottom
Shift+PageUpScroll one page up
Shift+PageDownScroll one page down
Cmd+RRun saved command
Cmd+Shift+REdit and run command

Zoom

ShortcutAction
Cmd+=Zoom in (+2px)
Cmd+-Zoom out (-2px)
Cmd+0Reset zoom

Split Panes

ShortcutAction
Cmd+\Split vertically
Cmd+Alt+\Split horizontally
Alt+←/→Navigate vertical panes
Alt+↑/↓Navigate horizontal panes
Cmd+Shift+EnterMaximize / restore active pane
Cmd+Alt+EnterFocus mode (hide sidebar, tab bar, panels)

AI

ShortcutAction
Cmd+Alt+AToggle AI Chat panel (toggle-ai-chat)
Cmd+Enter (panel focused)Send message
Esc (panel focused)Cancel in-flight stream

Panels

ShortcutAction
Cmd+[Toggle sidebar
Cmd+Shift+DToggle Git Panel
Cmd+Shift+MToggle markdown panel
Cmd+Alt+NToggle Ideas panel
Cmd+EToggle file browser
Cmd+OOpen file… (picker)
Cmd+NNew file… (picker for name + location)
Cmd+PCommand palette
Cmd+Shift+PToggle plan panel
Cmd+,Open settings
Cmd+?Toggle help panel
Cmd+Shift+KPrompt library
Cmd+JTask queue
Cmd+Shift+EError log
Cmd+Shift+WWorktree manager
Cmd+Shift+AActivity dashboard
Cmd+Shift+MMCP servers popup (per-repo)

Git

ShortcutAction
Cmd+BQuick branch switch (fuzzy search)
Cmd+Shift+DGit Panel (opens on last active tab)
Cmd+GGit Panel — Branches tab

Branches Panel (when panel is focused)

ShortcutAction
/ Navigate branches
EnterCheckout selected branch
nCreate new branch
dDelete branch
RRename branch (inline edit)
MMerge selected into current
rRebase current onto selected
PPush branch
pPull current branch
fFetch all remotes

File Browser (when focused)

ShortcutAction
↑/↓Navigate files
EnterOpen file / enter directory
BackspaceGo to parent directory
Cmd+CCopy file
Cmd+XCut file
Cmd+VPaste file
Cmd+Shift+FOpen file browser and activate content search

Code Editor (when focused)

ShortcutAction
Cmd+FFind
Cmd+GFind next
Cmd+Shift+GFind previous
Cmd+HFind and replace
Cmd+SSave file

Ideas Panel (when textarea focused)

ShortcutAction
EnterSubmit idea
Shift+EnterInsert newline
Cmd+V / Ctrl+VPaste image from clipboard
EscapeCancel edit mode

Quick Switcher

ShortcutAction
Hold Cmd+CtrlShow quick switcher overlay
Cmd+Ctrl+1-9Switch to branch by index

Voice Dictation

ShortcutAction
Hold F5Push-to-talk (configurable)

Mouse Actions

ActionWhereEffect
ClickSidebar branchSwitch to branch
Double-clickSidebar branch nameRename branch
Double-clickTab nameRename tab
Right-clickTabTab context menu
Right-clickSidebar branchBranch context menu
Right-clickSidebar repo Repo context menu
Right-clickSidebar group headerGroup context menu
Right-clickFile browser entryFile context menu
Middle-clickTabClose tab
DragTabReorder tabs
DragSidebar right edgeResize sidebar
DragPanel left edgeResize panel
DragSplit pane dividerResize panes
DragRepo onto groupMove repo to group
ClickStatus bar CWD pathCopy to clipboard
ClickPR badge (sidebar/status)Open PR detail popover
ClickCI ringOpen PR detail popover
ClickToolbar bellOpen notifications popover
ClickStatus bar panel buttonsToggle panels
HoldMic button (status bar)Record dictation

16. Build & Release

16.1 Makefile Targets

TargetDescription
devStart development server
buildBuild production app
build-dmgBuild macOS DMG
signCode sign the app
notarizeNotarize with Apple
releaseBuild + sign + notarize
build-github-releaseBuild for GitHub release (CI)
publish-github-releasePublish GitHub release
github-releaseOne-command release
cleanClean build artifacts

16.2 CI/CD

  • GitHub Actions for cross-platform builds
  • macOS code signing and notarization
  • Linux: libasound2-dev dependency, -fPIC flags
  • Updater signing with dedicated keys

17. Plugin System

17.1 Architecture

  • Obsidian-style plugin API with 4 capability tiers
  • Built-in plugins (TypeScript, compiled with app) and external plugins (JS, loaded at runtime)
  • Hot-reload: file changes in plugin directories trigger automatic re-import
  • Per-plugin error logging with ring buffer (500 entries)
  • Capability-gated access: pty:write, ui:markdown, ui:sound, ui:panel, ui:ticker, ui:context-menu, ui:sidebar, ui:file-icons, net:http, credentials:read, invoke:read_file, invoke:list_markdown_files, fs:read, fs:list, fs:watch, fs:write, fs:rename, exec:cli, git:read
  • CLI execution API: sandboxed execution of whitelisted CLI binaries (mdkb) with timeout and size limits
  • Filesystem API: sandboxed read, write, rename, list, tail-read, and watch operations restricted to $HOME
  • HTTP API: outbound requests scoped to manifest-declared URL patterns (SSRF prevention)
  • Credential API: cross-platform credential reading (macOS Keychain, Linux/Windows JSON file) with user consent
  • Panel API: rich HTML panels in sandboxed iframes (sandbox="allow-scripts") with structured message bridge (onMessage/send) and automatic CSS theme variable injection
  • Shared ticker system: setTicker/clearTicker API with source labels, priority tiers (low <10, normal 10-99, urgent >=100), counter badge, click-to-cycle, right-click popover
  • Agent-scoped plugins: agentTypes manifest field restricts output watchers and structured events to terminals running specific agents (e.g. ["claude"])
  • Plugin manifest fields use camelCase (minAppVersion, agentTypes, contentUri) — matches Rust serde serialization

17.2 Plugin Management (Settings > Plugins)

  • Installed tab: List all plugins with enable/disable toggle, logs viewer, uninstall button
  • Browse tab: Discover plugins from the community registry with one-click install/update
  • Enable/Disable: Persisted in AppConfig.disabled_plugin_ids
  • ZIP Installation: Install from local .zip file or HTTPS URL
  • Folder Installation: Install from a local folder (copies plugin directory into plugins dir)
  • Uninstall: Removes plugin directory (confirmation required)

17.3 Plugin Registry

  • Remote JSON registry hosted on GitHub (tuicommander-plugins repo)
  • Fetched on demand with 1-hour TTL cache
  • Version comparison for “Update available” detection
  • Install/update via download URL
  • tuic://install-plugin?url=https://... — Download and install plugin (HTTPS only, confirmation dialog)
  • tuic://open-repo?path=/path — Switch to repo (must be in sidebar)
  • tuic://settings?tab=plugins — Open Settings to specific tab
  • tuic://open/<path> — Open markdown file in tab (iframe SDK only, path validated against repos)
  • tuic://terminal?repo=<path> — Open terminal in repo (iframe SDK only)

17.4.1 TUIC SDK (window.tuic)

  • Injected automatically into every plugin iframe (inline and same-origin URL mode)
  • Feature detection: if (window.tuic)tuic.version reports SDK version
  • Files: tuic.open(path, {pinned?}), tuic.edit(path, {line?}), tuic.getFile(path): Promise<string>
  • Path resolution: relative paths resolve against active repo; absolute paths match longest repo prefix; ../ traversal outside repo root is blocked
  • Repository: tuic.activeRepo() returns active repo path; tuic.onRepoChange(cb) / tuic.offRepoChange(cb) for live updates
  • Terminal: tuic.terminal(repoPath) — open terminal in repository
  • UI feedback: tuic.toast(title, {message?, level?, sound?}) — native toast notifications with optional sound (info blip, warn double-beep, error descending sweep); tuic.clipboard(text) — copy to clipboard from sandboxed iframe
  • Messaging: tuic.send(data) / tuic.onMessage(cb) — bidirectional host↔plugin communication
  • Theme: tuic.theme — current theme as JS object (camelCase CSS vars); tuic.onThemeChange(cb) for live updates
  • <a href="tuic://open/..."> and <a href="tuic://terminal?repo=..."> links intercepted automatically
  • data-pinned attribute on links sets pinned flag
  • Interactive test page: docs/examples/sdk-test.html (see docs/tuic-sdk.md for launch instructions)

17.5 Built-in Plugins

  • Plan Tracker — Detects Claude Code plan files from structured events

Note: Claude Usage Dashboard was promoted from a plugin to a native SolidJS feature (see section 6.6). It is managed via Settings > Agents > Claude > Usage Dashboard toggle.

17.6 Example External Plugins

See examples/plugins/ for reference implementations:

  • hello-world — Minimal output watcher example
  • auto-confirm — Auto-respond to Y/N prompts
  • ci-notifier — Sound notifications and markdown panels
  • repo-dashboard — Read-only state and dynamic markdown
  • report-watcher — Generic report file watcher with markdown viewer
  • claude-status — Agent-scoped plugin (agentTypes: ["claude"]) tracking usage and rate limits
  • wiz-stories-kanban — Kanban board panel for file-based stories with drag-and-drop, filters, and work log timeline

18. Mobile Companion UI

Phone-optimized progressive web app for monitoring AI agents remotely. Separate SolidJS entry point (src/mobile/) served by the existing HTTP server at /mobile.

18.1 Architecture

  • Separate Vite entry point (mobile.html + src/mobile/index.tsx)
  • Shares transport layer, stores, and notification manager with desktop
  • Server-side routing: /mobile/*mobile.html, everything else → index.html
  • Session state accumulator enriches GET /sessions with question/rate-limit/busy state
  • SSE endpoint (/events) and WebSocket JSON framing for real-time updates

18.2 Sessions Screen

  • Hero metrics header: active session count + awaiting input count with large tabular-nums display
  • Elevated session cards with agent icon, status badge, project/branch, relative time
  • Rich sub-rows per card: agent intent (crosshair icon) or last prompt (speech bubble), current task (gear icon) with inline progress bar, usage limit percentage
  • Question state highlighted via inset gold box-shadow
  • Pull-to-refresh spinner via touch events
  • Loading skeletons during initial data fetch
  • Empty state with instructional hint
  • Tap card to open session detail

18.3 Session Detail Screen

  • Live output via WebSocket with format=log (VT100-extracted clean lines, auto-scrolling, 500-line buffer)
  • Semantic colorization: log lines are color-coded by type (info, warning, error, diff +/-, file paths) via classifyLine() utility
  • Search/filter in output: text search bar filters visible log lines in real time
  • Rich header: agent intent line (italic), current task line, progress bar, usage percentage (red above 80%)
  • Error bar (red tint) when last_error is set
  • Rate-limit bar (orange tint) with live countdown timer (formatRetryCountdown)
  • Suggest follow-up chips: horizontal scrollable pills from suggested_actions, tap to send
  • Slash menu overlay: frosted glass bottom sheet showing detected /command entries; tap to send Ctrl-U + command + Enter
  • Quick-action chips: Yes, No, y, n, Enter, Ctrl-C
  • TerminalKeybar: context-aware row of special key buttons above the main input. Shows Ctrl+C, Ctrl+D, Tab, Esc, Enter, arrow keys for terminal operations. When the agent is awaiting input, adds Yes/No quick-reply buttons. Consolidated from the former separate QuickActions component
  • CLI command widget: agent-specific quick commands (e.g., /compact, /status for Claude Code) accessible via expandable button
  • Text command input with 16px font (prevents iOS auto-zoom), inputmode="text"
  • Offline retry queue: write_pty calls that fail due to network disconnection are queued and retried when connectivity resumes
  • Back navigation to session list

18.4 Question Banner

  • Persistent overlay when any session has awaiting_input state
  • Shows agent name, truncated question, Yes/No quick-reply buttons
  • Visible on all screens, between top bar and content
  • Stacks multiple questions

18.5 Activity Feed

  • Chronological event feed grouped by time (NOW, EARLIER, TODAY, OLDER)
  • Reads from shared activityStore
  • Throttled grouping: items snapshot every 10s to prevent constant reordering with multiple active sessions; new items/removals trigger immediate refresh
  • Sticky section headers, tap to navigate to session

18.6 Session Management

  • Session kill: swipe or long-press a session card to kill/close the PTY session
  • New session: create a new PTY session from the sessions screen (optional shell/cwd selection)

18.7 Settings

  • Connection status: connectivity indicator with real-time Connected/Disconnected state
  • Server URL display
  • Notification sound toggle (localStorage-persisted)
  • Open Desktop UI link

18.8 PWA Support

  • Web app manifest (mobile-manifest.json) with standalone display mode
  • iOS Safari and Android Chrome Add to Home Screen support
  • apple-mobile-web-app-capable meta tags
  • PNG icons (192x192, 512x512) for PWA installability

18.8.1 Push Notifications

  • Web Push from TUICommander directly to mobile PWA clients (no relay dependency)
  • VAPID ES256 key generation on first enable, persisted in config
  • Service worker (sw.js) handles push events and notification clicks
  • PushManager.subscribe() flow with user gesture (click handler) for iOS/Firefox
  • Push subscriptions stored in push_subscriptions.json, survive restarts
  • API endpoints: POST/DELETE /api/push/subscribe, GET /api/push/vapid-key, POST /api/push/test
  • Triggers: agent awaiting_input (question, orange dot) and PtyExit (session completed, purple/unseen dot)
  • Deep link: notification click navigates to /mobile/session/<id>, opening the specific session detail
  • Delivery gate: push is sent whenever the desktop window is not focused (minimized, hidden, or on another workspace). This prevents duplicate alerts while the user is at the desktop and still wakes the PWA service worker when the phone is locked
  • Rate limited: max 1 push per session per 30 seconds
  • Stale subscriptions cleaned on HTTP 410 Gone
  • iOS standalone detection: shows “Add to Home Screen” guidance when not installed
  • HTTP detection: shows “Push requires HTTPS (enable Tailscale)” when not on HTTPS

18.9 Notification Sounds

  • Audio playback via Rust rodio crate (Tauri command play_notification_sound), replacing the previous Web Audio API approach
  • Eliminates AudioContext suspend issues on WebKit and works in headless/remote modes
  • State transition detection: question, rate-limit, error, completion
  • Completion notifications deferred 10s and suppressed when active sub-tasks are running (detected via ⏵⏵/›› mode-line prefix)

18.10 Visual Polish

  • Frosted glass bottom tabs: backdrop-filter: blur(20px) saturate(1.8) with semi-transparent background
  • Elevated card design: border-radius: var(--radius-xl), background: var(--bg-secondary), margin spacing
  • Safe-area-inset padding for notched devices
  • font-variant-emoji: text on output view — forces Unicode symbols (●, ○, ◉) to render as monochrome text glyphs instead of colorful emoji

18.11 Standalone CSS

  • Mobile PWA uses its own standalone stylesheet (src/mobile/mobile.css), independent from the desktop global.css
  • Shares core color palette and border radius tokens; differs in font stacks, layout approach, and iOS-specific rules
  • WebSocket state deduplication: duplicate state pushes are filtered to reduce unnecessary re-renders

19. MCP Proxy Hub

TUICommander aggregates upstream MCP servers and exposes them through its own /mcp endpoint. Any MCP client (Claude Code, Cursor, VS Code) connecting to TUIC automatically gains access to all configured upstream tools.

19.1 Architecture

  • TUIC acts as both an MCP server (to downstream clients) and an MCP client (to upstream servers)
  • All upstream tools are exposed via the single POST /mcp Streamable HTTP endpoint
  • Native TUIC tools (session, git, agent, config, workspace, notify, plugin_dev_guide) coexist with upstream tools
  • Tool routing: names containing __ are routed to the upstream registry; all others handled natively

19.2 Tool Namespace

  • Upstream tools are prefixed: {upstream_name}__{tool_name}
  • Double underscore (__) is the routing discriminator — native tool names never contain it
  • Tool descriptions are annotated with [via {upstream_name}] to identify origin
  • Clients always see the merged tool list in a single tools/list response

19.3 Supported Transports

  • HTTP (Streamable HTTP, spec 2025-03-26) — connects to any MCP server with an HTTP endpoint
  • Stdio — spawns local processes (npm packages, Python scripts, etc.) communicating via newline-delimited JSON-RPC

19.4 Circuit Breaker (per upstream)

  • 3 consecutive failures → circuit opens
  • Backoff: 1s → exponential growth → 60s cap
  • After 10 retry cycles without recovery → permanent Failed state
  • Recovery: successful tool call or health check resets the circuit breaker

19.5 Health Checks

  • Background task probes every Ready upstream every 60 seconds via tools/list (HTTP) or process liveness check (stdio)
  • CircuitOpen upstreams with expired backoff are also probed for recovery

19.6 Tool Filtering (per upstream)

  • Allow list: only matching tools are exposed
  • Deny list: all tools except matching ones are exposed
  • Pattern syntax: exact match or trailing-* prefix glob

19.6.1 Per-Repo Scoping

  • Each repository can define an allowlist of upstream server names in RepoSettings.mcpUpstreams
  • 3-layer merge: per-repo user settings > .tuic.json (team-shareable) > defaults (null = all servers)
  • Quick toggle via Cmd+Shift+M popup: shows all upstream servers with status, transport, tool count, and per-repo checkboxes
  • Toggling a checkbox immediately persists to repo settings (reactive, no refresh needed)

19.7 Hot-Reload

  • Adding, removing, or changing upstreams takes effect on save without restarting TUIC or AI clients
  • Config diff computed by stable id field; only changed entries are reconnected

19.8 Credential Management

  • Bearer tokens stored in OS keyring (Keychain / Credential Manager / Secret Service)
  • Config file (mcp-upstreams.json) never contains secrets
  • Per-upstream credential lookup at call time
  • OAuth 2.1 token sets persisted as structured JSON in the keyring ({"type": "oauth2", "access_token", "refresh_token", "expires_at"})

19.8.1 OAuth 2.1 Upstream Authentication

  • Full RFC 9728 (Protected Resource Metadata) + RFC 8414 (Authorization Server Discovery) flow with PKCE S256
  • UpstreamAuth::OAuth2 { client_id, scopes, authorization_endpoint?, token_endpoint? } joins Bearer as a credential type; endpoints auto-discovered from the resource server’s WWW-Authenticate challenge when omitted
  • Completion via native deep link tuic://oauth-callback?code=…&state=… — callbacks never touch the WebView console
  • TokenManager shared across every HttpMcpClient refresh path with a per-upstream semaphore that defeats thundering-herd refresh. 60 s expiry margin; None expires_at treated as valid
  • UpstreamError::NeedsOAuth { www_authenticate } transitions the registry to needs_auth; Services tab shows an Authorize button
  • Auto-triggered OAuth is gated behind explicit user consent; the confirm dialog surfaces the Authorization Server origin to defend against AS mix-up
  • Status values extended: authenticating (“Awaiting authorization…”) + needs_auth
  • Tauri commands: start_mcp_upstream_oauth, mcp_oauth_callback, cancel_mcp_upstream_oauth

19.9 Environment Sanitization (stdio)

  • Parent environment is cleared before spawning to prevent credential leakage
  • Safe allowlist re-applied: PATH, HOME, USER, LANG, LC_ALL, TMPDIR, TEMP, TMP, SHELL, TERM
  • User-configured env overrides applied on top

19.10 SSE Events

  • upstream_status_changed events emitted on status transitions (connecting, ready, circuit_open, disabled, failed)
  • tools/list_changed notification emitted when upstream tool lists change, enabling live tool-list updates for connected MCP clients
  • Delivered via GET /events SSE stream

19.11 Metrics (per upstream, lock-free)

  • call_count — total tool calls routed
  • error_count — total failed calls
  • last_latency_ms — last observed round-trip time

19.12 Validation

  • Names: must match [a-z0-9_-]+, must be unique
  • HTTP URLs: must use http:// or https:// scheme only
  • Self-referential URL detection: rejects URLs pointing to TUIC’s own MCP port
  • Stdio: command must be non-empty
  • All errors collected (not just first) and returned to caller
  • Respects sound toggle from Settings screen

20. Performance

20.1 PTY Write Coalescing

  • Terminal writes accumulated per animation frame via requestAnimationFrame (~60 flushes/sec)
  • High-throughput agent output (hundreds of events/sec) batched into single terminal.write() calls
  • Reduces xterm.js render passes and WebGL texture uploads during burst output
  • Flow control (pause/resume at HIGH_WATERMARK) unchanged

20.2 Async Git Commands

  • All ~25 Tauri git commands run inside tokio::task::spawn_blocking
  • Prevents git subprocess calls from blocking Tokio worker threads
  • get_changed_files merged from 2 sequential subprocesses to 1

20.3 Watcher-Driven Git Cache

  • Unified repo_watcher (FSEvents/inotify) monitors entire working tree with per-category debounce
  • CategoryEmitter routes events to Git, WorkTree, or Config handlers with trailing debounce
  • .gitignore-aware filtering prevents unnecessary cache invalidations
  • Cache hit ~0.2ms vs git subprocess ~20-30ms
  • 60s TTL as safety net for missed watcher events

20.4 Process Name via Syscall

  • proc_pidpath (macOS) / /proc/pid/comm (Linux) replaces ps fork
  • Eliminates ~100 fork+exec/min with 5 terminals open

20.5 MCP Concurrent Tool Calls

  • HttpMcpClient uses RwLock instead of Mutex
  • Tool calls use read lock (concurrent); only reconnect takes write lock

20.6 Serialization

  • PTY parsed events serialized once with serde_json::to_value
  • Reused for both Tauri IPC emit and event bus broadcast (was serialized twice)

20.7 Frontend Bundle Splitting

  • Vite manualChunks: xterm, codemirror, diff-view, markdown as separate chunks
  • SettingsPanel, ActivityDashboard, HelpPanel lazy-loaded with lazy() + Suspense
  • PTY read buffer increased from 4KB to 64KB for natural batching

20.8 Conditional Timers

  • StatusBar 1s timer only active when merged PR countdown or rate limit is displayed
  • ActivityDashboard snapshot signal uses default equality check (no forced re-render every 10s)

20.9 Profiling Infrastructure

  • Scripts in scripts/perf/: IPC latency, PTY throughput, CPU recording, Tokio console, memory snapshots
  • tokio-console feature flag for async task inspection
  • See docs/guides/profiling.md

Getting Started

What is TUICommander?

TUICommander is a desktop terminal orchestrator for running multiple AI coding agents in parallel (Claude Code, Gemini CLI, Aider, OpenCode, Codex). Built with Tauri + SolidJS + xterm.js + Rust.

Key capabilities:

  • Up to 50 concurrent terminal sessions with split panes and detachable tabs
  • 10 AI agents supported (Claude Code, Codex, Aider, Gemini, Amp, and more)
  • Git worktree isolation per branch
  • GitHub PR monitoring with CI status and notifications
  • Obsidian-style plugin system with community registry
  • Voice dictation with local Whisper (no cloud)
  • Command palette, configurable keybindings, prompt library
  • Built-in file browser and code editor
  • MCP HTTP server for external AI tool integration
  • Remote access from a browser on another device
  • Auto-update, 13 bundled fonts, cross-platform (macOS, Windows, Linux)

First Launch

  1. Add a repository — Click the + button at the top of the sidebar, or use the “Add Repository” option. Select a git repository folder.

  2. Select a branch — Click a branch name in the sidebar. If it’s not the main branch, TUICommander creates a git worktree so you work in an isolated copy.

  3. Start typing — The terminal is ready. Your default shell is loaded. Type commands, run AI agents, or execute scripts.

  4. Open more tabs — Press Cmd+T (macOS) or Ctrl+T (Windows/Linux) to add more terminal tabs for the same branch.

Workflow Overview

Add Repository → Select Branch → Worktree Created → Terminal Opens
                                                    ├── Run AI agent
                                                    ├── Open more tabs
                                                    ├── Split panes
                                                    └── View diffs/PRs

Each branch has its own set of terminals. When you switch branches, your previous terminals are preserved and hidden. Switch back and they reappear exactly as you left them.

The sidebar shows all your repositories and their branches.

Repository groups:

  • Repositories can be organized into named, colored groups
  • Drag a repo onto a group header to move it
  • Right-click a group to rename, change color, or delete it
  • Groups collapse/expand by clicking the group header

Repository entry:

  • Click to expand/collapse branch list
  • Click again to toggle icon-only mode (shows initials)
  • + button: Create new worktree for a new branch
  • button: Repo settings, remove, move to group

Branch entry:

  • Click: Switch to this branch (shows its terminals)
  • Double-click the branch name: Rename branch
  • CI ring: Shows CI check status (green/red/yellow segments)
  • PR badge: Shows PR number with color-coded state — click for detail popover. CLOSED and MERGED PRs are automatically hidden (MERGED PRs fade after 5 minutes of user activity).
  • Stats: Shows +additions/-deletions

Git quick actions (bottom of sidebar when a repo is active):

  • Pull, Push, Fetch, Stash buttons — run the git command in the active terminal

Next Steps

Terminal Features

Terminal Sessions

Each terminal tab runs an independent PTY (pseudo-terminal) session with your shell. Up to 50 concurrent sessions.

Creating Terminals

  • Cmd+T — New terminal for the active branch
  • + button on sidebar branch — Add terminal to specific branch
  • + button on tab bar — New terminal tab

New terminals inherit the working directory from the active branch’s worktree path.

CWD Tracking (OSC 7)

When your shell reports directory changes via OSC 7, TUICommander updates the terminal’s working directory in real time. If the new directory falls inside a different worktree, the terminal tab is automatically reassigned to the corresponding branch in the sidebar.

Terminal Lifecycle

Terminals are never unmounted from the DOM. When you switch branches or tabs, terminals are hidden but remain alive. Switch back and your process, scroll position, and output are exactly as you left them.

Closing Terminals

  • Cmd+W — Close active tab (confirmation only when a user-launched process is running — e.g. Claude Code, htop, npm; idle shells and shells still loading .zshrc close immediately)
  • Middle-click on tab — Close tab
  • Right-click → Close Tab — Context menu
  • Right-click → Close Other Tabs — Close all except this one
  • Right-click → Close Tabs to the Right — Close tabs after this one

Reopening Closed Tabs

  • Cmd+Shift+T — Reopen the last closed tab
  • Last 10 closed tabs are remembered with their name, font size, and working directory
  • Reopened tabs start a fresh shell session in the original directory

Tab Management

Tab Names

  • Default naming: “Terminal 1”, “Terminal 2”, etc.
  • Double-click a tab to rename it (inline editing)
  • Press Enter to confirm, Escape to cancel
  • Custom names persist through the session

Tab Reordering

Drag tabs to reorder them. Visual drop indicators show where the tab will land.

Tab Indicators

IndicatorMeaning
Grey dot (dim)Idle — no session or command never ran
Blue pulsing dotBusy — producing output now
Green dotDone — command completed
Purple dotUnseen — completed while you were viewing another tab (clears when selected)
Orange pulsing dotQuestion — agent needs user input
Red pulsing dotError — API error or agent stuck
Question iconAgent is asking a question
Progress barOperation in progress (OSC 9;4)
Amber gradientSession created via HTTP/MCP (remote session)

Sidebar branch icons also show purple when they contain unseen terminals.

Tab Shortcuts

Hover a tab to see its shortcut badge: “Terminal N (Cmd+N)”. Use Cmd+1 through Cmd+9 to jump directly. Ctrl+Tab / Ctrl+Shift+Tab — Next / previous tab.

Scroll Shortcuts

ShortcutAction
Cmd+HomeScroll to top
Cmd+EndScroll to bottom
Shift+PageUpScroll one page up
Shift+PageDownScroll one page down

Zoom

Per-terminal font size control:

ActionShortcutEffect
Zoom inCmd+=+2px font size
Zoom outCmd+--2px font size
ResetCmd+0Back to default size

Range: 8px to 32px. Each terminal has its own zoom level. The current zoom is shown in the status bar.

Split Panes

Split the terminal area into two panes:

Creating Splits

  • Cmd+\ — Split vertically (side by side)
  • Cmd+Alt+\ — Split horizontally (stacked)

The new pane opens a fresh terminal in the same working directory. Maximum 2 panes at a time.

  • Alt+←/→ — Switch between vertical panes
  • Alt+↑/↓ — Switch between horizontal panes
  • The active pane receives keyboard input

Resizing Split Panes

Drag the divider between the two panes to adjust the split ratio. Both terminals re-fit automatically when you release.

Maximizing a Split Pane

  • Cmd+Shift+Enter — Maximize / restore active pane (zoom pane). Expands the focused pane to fill the full terminal area; press again to restore the split.

Closing Split Panes

  • Cmd+W closes the active pane and collapses back to a single pane
  • The surviving pane automatically receives focus

Split Layout Persistence

Split layouts are stored per branch. When you switch branches and come back, your split configuration is restored.

Detachable Tabs

Float any terminal into its own OS window:

  1. Right-click a tab → Detach to Window
  2. The terminal opens in an independent floating window
  3. The PTY session stays alive — the floating window reconnects to the same session

When you close the floating window, the tab automatically returns to the main window.

Requirements: The tab must have an active PTY session. Tabs without a session (e.g., just created but not connected) cannot be detached.

Find in Terminal

Search within terminal output with Cmd+F:

  1. Press Cmd+F — a search overlay appears at the top of the active terminal pane
  2. Type your search query — matches highlight as you type (yellow for all matches, orange for active match)
  3. Navigate matches:
    • Enter or Cmd+G — Next match
    • Shift+Enter or Cmd+Shift+G — Previous match
  4. Toggle search options: Case sensitive, Whole word, Regex
  5. Match counter shows “N of M” results
  6. Press Escape to close the search and refocus the terminal

Uses @xterm/addon-search for native integration with the terminal buffer.

Search text across all open terminal buffers from the command palette:

  1. Press Cmd+P and type ~ followed by your search query (e.g. ~error)
  2. Results show terminal name, line number, and highlighted match text
  3. Press Enter or click a result to switch to that terminal and scroll to the matched line (centered in viewport)
  4. Minimum 3 characters after the ~ prefix

Also accessible via the “Search Terminals” command in the palette.

Copy & Paste

  • Copy: Select text in the terminal, then Cmd+C
  • Paste: Cmd+V writes clipboard content to the active terminal

Copy on Select

When enabled (Settings > Appearance), selecting text in the terminal automatically copies it to the clipboard. A brief confirmation appears in the status bar. This is enabled by default.

Clear Terminal

Cmd+L clears the terminal display. Running processes are unaffected.

Terminal Bell

The bell behavior when receiving BEL character (\x07) is configurable in Settings > Appearance:

  • none — silent
  • visual — brief screen flash
  • sound — plays notification sound
  • both — flash and sound

Clickable File Paths

File paths appearing in terminal output are automatically detected and become clickable links. Hover over a path to see the link underline, then click to open it.

  • .md / .mdx files open in the Markdown viewer panel
  • All other code files open in your configured IDE, at the line number if a :line or :line:col suffix is present

Paths are validated against the filesystem before becoming clickable — only real files show as links.

Recognized extensions include: .rs, .ts, .tsx, .js, .jsx, .py, .go, .java, .css, .html, .json, .yaml, .toml, .sql, and many more.

Plan File Detection

When an AI agent emits a plan file path (e.g., PLAN.md), a button appears in the toolbar showing the file name. Click it to open the plan — Markdown files open in the viewer panel, others open in the IDE. Click the dismiss button (x) to hide it.

Working with AI Agents

TUICommander detects rate limits, prompts, and status messages from AI agents:

  • Rate limit detection — Recognizes rate limit messages from Claude, Aider, Gemini, OpenCode, Codex
  • Prompt interception — Detects when agents ask yes/no questions or multiple choice
  • Status tracking — Parses token usage and timing from agent output
  • Progress indicators — Shows progress bars for long-running operations

When an agent asks a question, the tab indicator changes and a notification sound plays (if enabled).

Session Restore

When you restart TUICommander and a terminal had an active agent session, a clickable banner appears: “Agent session was active — click to resume.” Clicking the banner sends the agent’s resume command. Press Escape or click the x button to dismiss the banner without resuming.

Terminal output that uses the OSC 8 standard for hyperlinks (e.g., URLs emitted by ls --hyperlink) is supported. Clicking an OSC 8 hyperlink opens the URL in your system browser.

Sidebar

The sidebar is your primary navigation for repositories, branches, and git operations.

Toggle & Resize

  • Toggle visibility: Cmd+[
  • Resize: Drag the right edge (200–500px range)
  • Width persists across sessions

Repository Management

Adding Repositories

Click the + button at the top of the sidebar and select a git repository folder.

Repository Entry

Each repo shows a header with the repo name and action buttons:

  • Click the header to expand/collapse the branch list
  • Click again to toggle icon-only mode (shows repo initials — saves space)
  • button — Opens a menu with: Repo Settings, Create Worktree, Move to Group (with submenus for existing groups, Ungrouped, and New Group), Park Repository, Remove Repository
  • Right-click main worktree rowSwitch Branch submenu: shows all local branches with a checkmark on the current one. If the working tree is dirty, prompts to stash changes first. Blocks switching when a terminal has a running process.

Removing Repositories

Repo → Remove. This only removes the repo from the sidebar — it does not delete any files.

Repository Groups

Organize repos into named, colored groups.

Creating a Group

  • Repo Move to GroupNew Group…
  • Enter a name in the dialog

Moving Repos Between Groups

  • Drag a repo onto a group header
  • Or: Repo Move to Group → select a group
  • To ungroup: Repo Move to GroupUngrouped

Managing Groups

Right-click a group header for:

  • Rename — Change the group name
  • Change Color — Pick a new accent color
  • Delete — Remove the group (repos become ungrouped)

Groups can be collapsed/expanded by clicking the header, and reordered by drag-and-drop.

Branches

Selecting a Branch

Click a branch name to switch to it. This:

  1. Creates a git worktree (for non-main branches) if one doesn’t exist
  2. Shows the branch’s terminals (or creates a new one)
  3. Hides terminals from the previous branch

Branch Indicators

Each branch row can show:

IndicatorMeaning
CI ringProportional arc segments — green (passed), red (failed), yellow (pending)
PR badgeColored by state — green (open), purple (merged), red (closed), gray (draft). Click for detail popover.
Diff stats+N / -N additions and deletions
Merged badgeBranches merged into main show a “Merged” badge
Question iconAn agent in this branch’s terminal is asking a question
Grey iconNo active terminals in the repo — branch icons dim to grey

Branch Actions

  • Double-click the branch name to rename the branch
  • Right-click for context menu: Copy Path, Add Terminal, Create Worktree (for branches without a worktree), Delete Worktree, Open in IDE, Rename Branch/Worktree, Merge & Archive

Remote-Only PRs

When a repository has open PRs on branches that only exist on the remote (not checked out locally), a badge appears in the branch section. Click it to open a popover listing these PRs. Each row shows the PR number, title, and state badge. Click a row to expand an inline accordion showing PR details, with action buttons:

  • Checkout — Create a local tracking branch
  • Create Worktree — Create a worktree for the branch
  • Merge — Merge the PR via GitHub API (shown when PR is mergeable)
  • View Diff — Open PR diff in a panel tab
  • Approve — Submit an approving review

Dismiss & Show Dismissed

Remote-only PRs can be dismissed to reduce sidebar clutter. Right-click the remote PRs badge or use the “Dismiss” action in the accordion. A “Show Dismissed” toggle at the bottom reveals dismissed PRs again.

Park Repos

Temporarily hide repos you’re not actively using.

Parking

Right-click any repo in the sidebar → Park. The repo disappears from the main list.

Viewing Parked Repos

A button in the sidebar footer shows all parked repos with a count badge. Click it to open a popover listing them.

Unparking

Click Unpark on any repo in the parked repos popover. It returns to the main sidebar list.

Quick Branch Switcher

Switch branches by number without the mouse:

  1. Hold Cmd+Ctrl (macOS) or Ctrl+Alt (Windows/Linux)
  2. All branches show numbered badges (1, 2, 3…)
  3. Press a number (1–9) to switch to that branch instantly
  4. Release the modifier to dismiss the overlay

Git Quick Actions

When a repo is active, the bottom of the sidebar shows quick action buttons:

  • Pullgit pull in the active terminal
  • Pushgit push
  • Fetchgit fetch
  • Stashgit stash

For more git operations (staging, commit, push, pull, stash, blame, history), use the Git Panel (Cmd+Shift+D).

File Browser & Code Editor

File Browser Panel

Toggle with Cmd+E or the folder icon in the status bar. The file browser shows the directory tree of the active repository (or linked worktree when on a worktree branch).

The file browser, Markdown viewer, and Diff panels are mutually exclusive — opening one closes any other that is open.

  • Arrow keys (Up / Down) — Move selection
  • Enter — Open a file or enter a directory
  • Backspace — Go up to the parent directory
  • .. row — Click to go up one level (appears when inside a subdirectory)
  • Breadcrumb bar — Click any path segment to jump directly to that directory; click the root / to return to the repo root

Directories are listed first, then files. The entry count is shown as a badge in the panel header.

Sorting

Click the funnel icon in the toolbar to switch sort order:

ModeOrder
Name (default)Directories first, then alphabetical
DateDirectories first, then newest modified first

Live Refresh

The panel watches the current directory for filesystem changes. When a file is created, deleted, or renamed outside the app, the listing refreshes automatically without a visible loading spinner.

Git Status Indicators

The panel header shows a legend for git status colors:

ColorLabelMeaning
OrangemodModified (unstaged changes)
GreenstagedStaged for commit
BluenewUntracked (new file)

File and directory names inherit these colors based on their git status. Gitignored entries are shown in a dimmed style.

View Modes

The file browser supports two view modes, toggled via toolbar buttons:

ModeDescription
List (default)Flat directory listing with breadcrumb navigation and .. parent entry
TreeCollapsible hierarchy with lazy-loaded subdirectories. Expand folders by clicking the chevron

Switching to tree mode resets to the repository root regardless of the current flat-mode subdirectory. When a search query is active, the view always shows flat results.

Type in the search box to search recursively across the entire repository by filename. Supports * and ** glob wildcards (e.g., *.ts, src/**/*.test.ts). Results appear with their full path. Clear the query with the × button or by emptying the input.

The search icon button to the left of the input toggles between filename mode (file icon) and content mode (magnifier icon).

Switch to content mode (magnifier icon) to search inside file contents across the repository. Results are grouped by file, showing the matching line number and a highlighted excerpt. Click any result to open the file in the editor, jumping to that line.

Content search options (shown when in content mode):

ToggleMeaning
Aa (case icon)Match case
.* (regex icon)Use regular expression
|ab| (word icon)Match whole word

A status bar below the search box shows match counts, files searched, files skipped (binary/large), and a “results limited” notice when the result set is truncated. Search begins after a short debounce and requires at least 3 characters.

File Operations (Context Menu)

Right-click any entry to open the context menu:

ActionShortcutNotes
Copy PathCopies the full absolute path to the clipboard
CopyCmd+CFiles only; stores file in the internal clipboard
CutCmd+XFiles only; cut entries are shown dimmed
PasteCmd+VPastes into the current directory; disabled when clipboard is empty
Rename…Opens a rename dialog; enter the new name and confirm
DeleteRequires confirmation; directories are deleted recursively
Add to .gitignoreAppends the entry’s path to .gitignore; disabled if already ignored

The keyboard shortcuts (Cmd+C, Cmd+X, Cmd+V) also work when the file browser has focus, without opening the context menu.

Cut + Paste performs a move (rename). Copy + Paste duplicates the file into the current directory. Pasting into the same directory where the file already exists is a no-op.

Opening Files

  • Click a file — Opens it in the code editor (see below), or in the Markdown viewer if the extension is .md or .mdx
  • Click a content search result — Opens the file and jumps to the matching line number

Panel Resize

Drag the left edge of the panel to resize it. Range: 200–800 px.


Code Editor

Clicking a non-Markdown file opens it in an in-app code editor tab in the main tab area, alongside terminal tabs.

Features

  • Syntax highlighting — Auto-detected from file extension; disabled for files larger than 500 KB
  • Line numbers, bracket matching, active line highlight, indentation support
  • SaveCmd+S saves the file when the editor tab is focused

Read-Only Mode

Click the padlock icon in the editor tab header to toggle read-only mode. When locked, the file cannot be edited.

Unsaved Changes

An unsaved-changes dot appears in both the tab bar and the editor header when the file has been modified but not saved.

Disk Conflict Detection

If the file changes on disk while you have unsaved edits, a conflict banner appears with two options:

  • Reload — Discard local edits and load the disk version
  • Keep mine — Dismiss the banner; the next save overwrites the disk version

When the editor has no unsaved changes, files reload silently when they change on disk.


Markdown Viewer

.md and .mdx files open in the Markdown viewer panel instead of the code editor. The viewer renders Markdown with syntax-highlighted code blocks.

See ai-agents.md for how AI-generated plan files are detected and surfaced as a one-click shortcut to open in the viewer.

Command Palette & Activity Dashboard

Command Palette

Open with Cmd+P (macOS) / Ctrl+P (Windows/Linux). The command palette gives you fast keyboard access to every registered action in the app.

How It Works

  1. Press Cmd+P — the palette opens with a search input focused
  2. Type to filter actions by name or category (substring match, case-insensitive)
  3. Navigate with / arrow keys
  4. Press Enter to execute the selected action
  5. Press Escape or click outside the palette to close

What You See

Each row shows:

  • Action label — What the action does (e.g., “Git panel”, “New terminal tab”)
  • Category badge — The action’s category (Terminal, Panels, Git, Navigation, Zoom, Split Panes, File Browser)
  • Keybinding hint — The assigned keyboard shortcut, if any

Search Behavior

Filtering matches against the action label and its category simultaneously. Typing “git” surfaces all Git actions; typing “panel” surfaces all panel-toggle actions regardless of category. There is no minimum query length — results update on every keystroke.

Recency Ranking

When the search box is empty, recently used actions float to the top, ordered by most recent first. Remaining actions are sorted alphabetically. The ranking persists across palette opens so your most-used commands are always one keystroke away.

Mouse Support

Hovering over a row highlights it (same as keyboard selection). Clicking a row executes the action immediately.

Powered by the Action Registry

The palette is auto-populated from actionRegistry.ts. Every action registered there — with its label, category, and keybinding — appears in the palette automatically. No manual configuration is needed, and plugin-contributed actions appear alongside built-in ones.

Search Modes

The command palette supports three search prefixes:

PrefixModeDescription
!Filename searchSearch files by name (min 1 char)
?Content searchSearch inside file contents (min 3 chars)
~Terminal searchSearch across all open terminal buffers (min 3 chars)
  • Leading spaces after the prefix are ignored (~ error = ~error)
  • File results show as a flat list with file path
  • Content matches include line number and highlighted match text
  • Terminal matches include terminal name, line number, and highlighted match text
  • Press Enter or click to open the file in an editor tab, or navigate to the terminal match
  • Terminal match navigation switches to the correct tab/pane and scrolls to the matched line
  • Delete the prefix to return to command mode
  • Search runs with a 300ms debounce
  • Footer shows !, ?, and ~ hints when in command mode

If no repository is selected, file/content modes show “No repository selected”. If no terminals are open, terminal mode shows “No terminals open”.

Discoverable Search Commands

You can also access search modes via explicit commands in the palette:

CommandAction
Search TerminalsOpens palette with ~ prefix
Search FilesOpens palette with ! prefix
Search in File ContentsOpens palette with ? prefix

These appear as regular commands in the palette — type “Search” to find them.


Activity Dashboard

Open with Cmd+Shift+A. A real-time overview of all your terminal sessions.

What You See

A compact list where each row shows:

ColumnDescription
Terminal nameThe tab name
Agent typeDetected agent (Claude, Aider, etc.) with brand icon
StatusCurrent state with color indicator
Last activityRelative timestamp (“2s ago”, “1m ago”) — auto-refreshes

Status Colors

ColorMeaning
GreenAgent is actively working
YellowAgent is waiting for input
RedAgent is rate-limited (with countdown)
GrayTerminal is idle

Interactions

  • Click any row — Switches to that terminal and closes the dashboard
  • Rate limit indicators — Show countdown timers for when the limit expires

The dashboard is useful when running many agents in parallel — you can spot at a glance which ones need attention, which are stalled, and which are making progress.

Keyboard Shortcuts

All shortcuts use Cmd on macOS and Ctrl on Windows/Linux unless noted.

Customizing Keybindings

From the UI

Open Help > Keyboard Shortcuts (or Cmd+? → Keyboard Shortcuts). Click the pencil icon next to any shortcut to enter recording mode, then press your new key combination. The app warns you if the combo is already used by another action. Overridden shortcuts are highlighted in accent color with a reset icon to revert individually. A “Reset all to defaults” button is at the bottom.

By editing the config file

You can also edit the keybindings.json file directly in your config directory:

  • macOS: ~/Library/Application Support/tuicommander/keybindings.json
  • Windows: %APPDATA%\tuicommander\keybindings.json
  • Linux: ~/.config/tuicommander/keybindings.json

The file is a JSON array of override objects. Only include shortcuts you want to change — anything not listed uses the default:

[
  { "action": "toggle-git-ops", "key": "Cmd+Shift+Y" },
  { "action": "toggle-markdown", "key": "Cmd+Shift+M" }
]
  • "key" uses Cmd as the platform-agnostic modifier (resolved to Meta on macOS, Ctrl on Win/Linux)
  • Set "key": "" or "key": null to unbind an action
  • Changes made via the UI are saved to this same file
  • The file is loaded at startup — restart TUICommander to pick up manual edits

See the action table below for all available action names.

Global Hotkey

A configurable OS-level shortcut to toggle TUICommander’s visibility from any application.

  • Configure: Settings > Keyboard Shortcuts > Global Hotkey (top of the tab)
  • No default — you must set it yourself (e.g., Ctrl+Space, `Cmd+``)
  • Toggle behavior: hidden/minimized → show+focus, visible but unfocused → focus, focused → instant hide
  • Cmd and Ctrl are treated as distinct modifiers
  • Uses tauri-plugin-global-shortcut (no Accessibility permission required on macOS)
  • Not available in browser/PWA mode
  • Persists across app restarts

Terminal Operations

ShortcutAction
Cmd+TNew terminal tab
Cmd+WClose tab (or close active pane in split mode)
Cmd+Shift+TReopen last closed tab
Cmd+RRun saved command
Cmd+Shift+REdit and run command
Cmd+LClear terminal
Cmd+FFind in terminal / diff tab
Cmd+GGit Panel — Branches tab (or Find next match when search is open)
EnterFind next match (when search is open)
Cmd+Shift+G / Shift+EnterFind previous match (when search is open)
EscapeClose search overlay
Cmd+CCopy selection
Cmd+VPaste to terminal
Cmd+HomeScroll to top
Cmd+EndScroll to bottom
Shift+PageUpScroll page up
Shift+PageDownScroll page down

Tab Navigation

ShortcutAction
Cmd+1 through Cmd+9Switch to tab by number
Ctrl+TabNext tab
Ctrl+Shift+TabPrevious tab

Zoom

ShortcutAction
Cmd+= (or Cmd++)Zoom in (active terminal)
Cmd+-Zoom out (active terminal)
Cmd+0Reset zoom to default (active terminal)
Cmd+Shift+= (or Cmd+Shift++)Zoom in all terminals
Cmd+Shift+-Zoom out all terminals
Cmd+Shift+0Reset zoom all terminals

Font size range: 8px to 32px, step 2px per action.

Split Panes

ShortcutAction
Cmd+\Split vertically (side by side)
Cmd+Alt+\Split horizontally (stacked)
Alt+← / Alt+→Navigate panes (vertical split)
Alt+↑ / Alt+↓Navigate panes (horizontal split)
Cmd+WClose active pane (collapses to single)
Cmd+Shift+EnterMaximize / restore active pane
Cmd+Alt+EnterFocus mode — hide sidebar, tab bar, and all side panels (keeps toolbar + status bar)

Panels

ShortcutAction
Cmd+[Toggle sidebar
Cmd+Shift+DToggle Git Panel
Cmd+Shift+MToggle markdown panel
Cmd+Alt+NToggle Ideas panel
Cmd+EToggle file browser
Cmd+OOpen file… (picker)
Cmd+NNew file… (picker for name + location)
Cmd+,Open settings
Cmd+?Toggle help panel
Cmd+Shift+KPrompt library
Cmd+KClear scrollback
Cmd+Shift+WWorktree Manager
Cmd+JTask queue
Cmd+Shift+PToggle plan panel
Cmd+Shift+EToggle error log
Cmd+Shift+MMCP servers popup (per-repo)

Note: File browser and Markdown panels are mutually exclusive — opening one closes the other.

ShortcutAction
Cmd+PCommand palette
Cmd+Shift+AActivity dashboard

Git

ShortcutAction
Cmd+Shift+DGit Panel (opens on last active tab)
Cmd+GGit Panel — Branches tab
Cmd+BQuick branch switch (fuzzy search)

Branches Panel (when panel is focused)

ShortcutAction
/ Navigate branch list
Enter / double-clickCheckout selected branch
nCreate new branch (inline form)
dDelete branch (safe; hold to force)
RRename branch (inline edit)
MMerge selected branch into current
rRebase current onto selected branch
PPush branch (auto-sets upstream if missing)
pPull current branch
fFetch all remotes
Ctrl/Cmd+1–4Switch Git Panel tab (1=Changes, 2=Log, 3=Stashes, 4=Branches)

Quick Branch Switcher

ShortcutAction
Hold Cmd+Ctrl (macOS) or Ctrl+Alt (Win/Linux)Show quick switcher overlay
Cmd+Ctrl+1-9Switch to branch by index

While holding the modifier, all branches show numbered badges. Press a number to switch instantly.

File Browser (when panel is focused)

ShortcutAction
/ Navigate files
EnterOpen file or enter directory
BackspaceGo to parent directory
Cmd+CCopy selected file
Cmd+XCut selected file
Cmd+VPaste file into current directory

Code Editor (when editor tab is focused)

ShortcutAction
Cmd+SSave file

Ideas Panel (when textarea is focused)

ShortcutAction
EnterSubmit idea
Shift+EnterInsert newline

Voice Dictation

ShortcutAction
Hold F5Push-to-talk (configurable in Settings)

Hold to record, release to transcribe and inject text into active terminal.

Tab Context Menu (Right-click on tab)

ActionShortcut
Close TabCmd+W
Close Other Tabs
Close Tabs to the Right
Copy Path— (diff/editor/markdown file tabs)
Rename Tab(double-click tab name)
Detach to Window

Mouse Actions

ActionWhereEffect
ClickSidebar branchSwitch to branch
Double-clickSidebar branch nameRename branch
Double-clickTab nameRename tab
Right-clickTabContext menu
Right-clickSidebar branchBranch context menu
Middle-clickTabClose tab
DragTabReorder tabs
DragSidebar right edgeResize sidebar (200-500px)
ClickPR badge / CI ringOpen PR detail popover
ClickStatus bar CWD pathCopy path to clipboard
ClickStatus bar panel buttonsToggle Git/MD/FB/Ideas panels
DragPanel left edgeResize right-side panel (200-800px)
DragSplit pane dividerResize split terminal panes

Action Names Reference (for keybindings.json)

Action NameDefault ShortcutDescription
zoom-inCmd+=Zoom in
zoom-outCmd+-Zoom out
zoom-resetCmd+0Reset zoom
zoom-in-allCmd+Shift+=Zoom in all terminals
zoom-out-allCmd+Shift+-Zoom out all terminals
zoom-reset-allCmd+Shift+0Reset zoom all terminals
new-terminalCmd+TNew terminal tab
close-terminalCmd+WClose terminal/pane
reopen-closed-tabCmd+Shift+TReopen closed tab
clear-terminalCmd+LClear terminal
run-commandCmd+RRun saved command
edit-commandCmd+Shift+REdit and run command
split-verticalCmd+\Split vertically
split-horizontalCmd+Alt+\Split horizontally
prev-tabCtrl+Shift+TabPrevious tab
next-tabCtrl+TabNext tab
switch-tab-1..9Cmd+1..9Switch to tab N
toggle-sidebarCmd+[Toggle sidebar
toggle-markdownCmd+Shift+MToggle markdown panel
toggle-notesCmd+Alt+NToggle ideas panel
open-fileCmd+OOpen file picker
new-fileCmd+NCreate new file
toggle-file-browserCmd+EToggle file browser
prompt-libraryCmd+Shift+KPrompt library
toggle-settingsCmd+,Open settings
toggle-task-queueCmd+JTask queue
toggle-helpCmd+?Toggle help panel
toggle-git-opsCmd+Shift+DGit Panel
toggle-git-branchesCmd+GGit Panel — Branches tab
worktree-managerCmd+Shift+WWorktree Manager panel
quick-branch-switchCmd+BQuick branch switch
find-in-terminalCmd+FFind in terminal
command-paletteCmd+PCommand palette
activity-dashboardCmd+Shift+AActivity dashboard
toggle-error-logCmd+Shift+EToggle error log
toggle-mcp-popupCmd+Shift+MMCP servers popup (per-repo)
toggle-planCmd+Shift+PToggle plan panel
switch-branch-1..9Cmd+Ctrl+1..9Switch to branch N
scroll-to-topCmd+HomeScroll to top
scroll-to-bottomCmd+EndScroll to bottom
scroll-page-upShift+PageUpScroll page up
scroll-page-downShift+PageDownScroll page down
zoom-paneCmd+Shift+EnterMaximize/restore pane
toggle-focus-modeCmd+Alt+EnterFocus mode — hide sidebar/tab bar/panels
toggle-file-browser-content-searchCmd+Shift+FFile content search
toggle-diff-scrollCmd+Shift+GDiff scroll view
toggle-global-workspaceCmd+Shift+XToggle global workspace

Settings

Open settings with Cmd+,. Settings are organized into tabs.

General Tab

SettingDescription
LanguageUI language
Default IDEIDE for “Open in…” actions. Detected IDEs grouped by category: Editors (VS Code, Cursor, Zed, Sublime, Vim, Neovim, Emacs), IDEs (IntelliJ, WebStorm, PyCharm, GoLand, etc.), Terminals (iTerm2, Warp, Kitty, etc.), Git (GitKraken, GitHub Desktop, Tower, etc.)
ShellCustom shell path (e.g., /bin/zsh, /usr/local/bin/fish). Leave empty for system default.
Confirm before quittingShow dialog when closing app with active terminals
Confirm before closing tabAsk before closing terminal tab
Prevent sleep when busyKeep machine awake while agents are working
Auto-check for updatesCheck for new versions on startup
Auto-show PR popoverAutomatically display PR details when switching branches. Only shows for OPEN pull requests — CLOSED PRs are hidden, and MERGED PRs fade after 5 minutes of user activity.
Repository defaultsBase branch, file handling, setup/run scripts applied to new repos

Appearance Tab

SettingTypeDefaultDescription
Terminal themeColor theme with preview swatches
Terminal fontJetBrains Mono13 bundled monospace fonts: Fira Code, Hack, Cascadia Code, Source Code Pro, IBM Plex Mono, Inconsolata, Ubuntu Mono, Anonymous Pro, Roboto Mono, Space Mono, Monaspace Neon, Geist Mono
Default font size8–32px slider. Applies to new terminals; existing terminals keep their zoom level.
Split tab modeSeparate or unified tab appearance
Max tab name length10–60 slider
Repository groupsCreate, rename, delete, and color-code groups
Reset panel sizesRestore sidebar and panel widths to defaults
Copy on SelectbooleantrueAuto-copy terminal selection to clipboard
Bell Stylenone/visual/sound/bothvisualTerminal bell behavior

Agents Tab

Each supported agent has an expandable row showing detection status, version, and MCP badge.

SettingDescription
Agent DetectionAuto-detects running agents from terminal output patterns. Shows “Available” or “Not found” for each agent.
Run ConfigurationsCustom launch configs (binary path, args, model, prompt) per agent. Add, set default, or delete configurations. A config named “review” enables the Review button in the PR Detail Popover — its args are interpolated with {pr_number}, {branch}, {base_branch}, {repo}, {pr_url}.
MCP IntegrationInstall/remove TUICommander as MCP server for supported agents. Shows install status with a dot indicator.
Claude Usage Dashboard(Claude Code only) Toggle under Features when the Claude row is expanded. Enables rate limit monitoring, session analytics, token usage charts, activity heatmap, and per-project breakdowns. Usage data appears in the status bar agent badge and in a dedicated dashboard tab.

See AI Agents for details on agent detection, rate limits, and the usage dashboard.

GitHub Tab

GitHub authentication and token management:

SettingDescription
OAuth LoginDevice Flow login — click “Sign in with GitHub”, enter code on github.com. Token stored in OS keyring.
Auth StatusShows current login, avatar, token source (OAuth/env/CLI), and available scopes
DisconnectClear all GitHub tokens (keyring + env cache). Falls back to next available source.
DiagnosticsToken source details, scope verification, API connectivity check
Issue FilterWhich issues to show in the GitHub panel: Assigned (default), Created, Mentioned, All, or Disabled
Auto-show PR popoverAutomatically show PR detail popover when opening a branch with an active PR
Auto-delete on PR closeOff (default), Ask, or Auto — controls branch cleanup when a PR is merged/closed

Token priority: GH_TOKEN env → GITHUB_TOKEN env → OAuth keyring → gh CLI config → gh auth token subprocess.

Services Tab

HTTP API Server

Enable the HTTP API server for external tool integration:

  • Serves the REST API and MCP protocol for AI agents and automation tools
  • Local MCP connections use a Unix domain socket at <config_dir>/mcp.sock — no port configuration needed
  • AI agents connect via the tuic-bridge sidecar (auto-installed on first launch for Claude Code, Cursor, Windsurf, VS Code, Zed, Amp, Gemini)
  • Shows server status (running/stopped) and active session count

TUIC Tools

Native tools exposed to AI agents via MCP. Each tool can be individually enabled or disabled to restrict what agents can access.

Collapse tools (checkbox) — when enabled, replaces the full tool list sent to AI agents with 3 lazy-discovery meta-tools (search_tools, get_tool_schema, call_tool). Cuts the baseline MCP context cost from ~35k tokens to ~500 tokens per agent turn; the agent fetches schemas on demand via BM25-ranked search. Default: off. Toggling refreshes connected clients via notifications/tools/list_changed.

Tools:

  • session — PTY terminal session management
  • git — Repository state queries
  • agent — AI agent detection and spawning
  • config — App configuration read/write
  • workspace — Repo and worktree queries
  • notify — User notifications (toast, confirm)
  • plugin_dev_guide — Plugin authoring reference

Upstream MCP Servers

Proxy external MCP servers through TUICommander. Their tools appear prefixed as {name}__{tool}:

  • Add upstream servers via HTTP (Streamable MCP) or stdio (process) transport
  • API keys for HTTP upstreams are stored in the OS keychain
  • Live status (connecting, ready, circuit open, failed) with tool count and call metrics
  • Reconnect and remove controls per upstream
  • Per-repo scoping: each repo can define an allowlist of active upstream servers via Cmd+Shift+M popup (or repo settings). Empty/null allowlist = all servers active

Remote Access

Enable HTTP/WebSocket access from other devices on your network. See Remote Access for full setup guide.

Voice Dictation

See Voice Dictation for full details.

Keyboard Shortcuts Tab

Browse and rebind all app actions:

  • Every registered action is listed with its current keybinding
  • Click any action row and press a new key combination to rebind it
  • Custom bindings are stored in keybindings.json in the platform config directory
  • Auto-populated from the action registry — new actions appear automatically

See Keyboard Shortcuts for the full reference and customization guide.

Plugins Tab

Install, manage, and browse plugins. See Plugins for the full guide.

  • Installed — List all plugins with enable/disable toggle, logs viewer, uninstall
  • Browse — Discover and install from the community registry

Repository Settings

Per-repository settings accessed via sidebar → “Repo Settings”.

Worktree Tab

  • Display Name — Custom name shown in sidebar
  • Base Branch — Branch to create worktrees from (auto-detect, main, master, develop)
  • Copy ignored files — Copy .gitignored files to new worktrees
  • Copy untracked files — Copy untracked files to new worktrees

Scripts Tab

  • Setup Script — Runs once after worktree creation (e.g., npm install)
  • Run Script — On-demand script launchable from toolbar with Cmd+R
  • Archive Script — Runs before a worktree is archived or deleted; non-zero exit blocks the operation

Repo-Local Config (.tuic.json)

A .tuic.json file in the repository root provides team-shareable settings that override per-repo app settings and global defaults. The file is read-only from TUICommander (edit it in your repo directly).

Precedence: .tuic.json > per-repo app settings > global defaults

Supported fields: base_branch, copy_ignored_files, copy_untracked_files, setup_script, run_script, archive_script, worktree_storage, delete_branch_on_remove, auto_archive_merged, orphan_cleanup, pr_merge_strategy, after_merge, auto_delete_on_pr_close.

User-specific settings (promptOnCreate, autoFetchIntervalMinutes) are intentionally excluded from .tuic.json.

Notification Settings

  • Enable Audio Notifications — Master toggle
  • Volume — 0-100%
  • Per-event toggles:
    • Agent asks question
    • Error occurred
    • Task completed
    • Warning
  • Test buttons — Test each sound individually
  • Reset to Defaults — Restore default notification settings

AI Agents

TUICommander detects, monitors, and manages AI coding agents running in your terminals.

Supported Agents

AgentBinaryResume CommandSession Binding
Claude Codeclaudeclaude --continueclaude --resume $TUIC_SESSION
Codex CLIcodexcodex resume --lastcodex resume $TUIC_SESSION
Aideraideraider --restore-chat-history
Gemini CLIgeminigemini --resumegemini --resume $TUIC_SESSION
OpenCodeopencodeopencode -c
Ampampamp threads continue
Cursor Agentcursor-agentcursor-agent resume
Warp Ozoz
Droid (Factory)droid

Agent Detection

TUICommander auto-detects which agent is running in each terminal by matching output patterns. Detection uses agent-specific status line markers:

  • Claude Code: Middle dot · (U+00B7), dingbat asterisks (U+2720–273F), or ASCII *
  • Copilot CLI: Therefore sign (U+2234), filled circle (U+25CF), empty circle (U+25CB)
  • Aider: Knight Rider scanner blocks ░█
  • Gemini CLI / Amazon Q / Cline: Braille spinners ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏
  • Codex CLI: Bullets

When detected:

  • The status bar shows the agent’s brand logo and name
  • The tab indicator updates to reflect agent state
  • Rate limit and question detection activate for that provider’s patterns

Binary detection uses resolve_cli() — Rust probes well-known directories so agents are found even in release builds where the user’s shell PATH isn’t available.

Rate Limit Detection

When an agent hits a rate limit, TUICommander detects it from terminal output:

  • Status bar warning — Shows a badge with the number of rate-limited sessions and a countdown timer
  • Per-session tracking — Each session’s rate limit is tracked independently with automatic cleanup when expired
  • Provider-specific patterns — Custom regex for Claude (“overloaded”, “rate limit”), Gemini (“429”, “quota exceeded”), OpenAI (“too many requests”), and generic patterns

Question Detection

When an agent asks an interactive question (Y/N, multiple choice, numbered options), TUICommander:

  1. Changes the tab indicator to a ? icon
  2. Shows a prompt overlay with keyboard navigation:
    • ↑/↓ to navigate options
    • Enter to select
    • Number keys 1-9 for numbered options
    • Escape to dismiss
  3. Plays a notification sound (if enabled in Settings → Notifications)

For unrecognized agents, silence-based detection kicks in — if the terminal stops producing output for 10 seconds after a line ending with ?, it’s treated as a potential prompt. User-typed lines ending with ? are suppressed from question detection for 500ms (echo window) to avoid false positives from PTY echo.

Usage Limit Tracking

For Claude Code, TUICommander detects weekly and session usage limit messages from terminal output:

  • Unified agent badge — When Claude is the active agent, the status bar shows a single badge combining the agent icon with usage data. The badge displays rate limit countdowns (when rate-limited), Claude Usage API data (5h/7d utilization percentages), or terminal-detected usage limits, in that priority order.
    • Blue: < 70% utilization
    • Yellow: 70–89%
    • Red (pulsing): >= 90%
  • Clicking the badge opens the Claude Usage Dashboard.

This helps you pace your usage across the week.

Claude Usage Dashboard

A native feature (not a plugin) that provides detailed analytics for your Claude Code usage. Enable it in Settings > Agents > expand Claude Code > Features > Usage Dashboard.

When enabled, TUICommander polls the Claude API every 5 minutes and shows:

  • Rate limits — 5-hour and 7-day utilization bars with reset countdowns. Color-coded: green (OK), yellow (70%+), red (90%+).
  • Usage Over Time — 7-day token usage chart (input vs. output tokens) with hover tooltips.
  • Insights — Session count, message counts, input/output/cache token totals.
  • Activity heatmap — 52-week GitHub-style heatmap of daily message counts with per-project drill-down on hover.
  • Model usage — Breakdown by model (messages, input, output, cache created, cache read).
  • Per-project breakdown — All projects ranked by token usage. Click a project to filter the dashboard to that project.

The dashboard opens as a tab in the Activity Center. You can also reach it by clicking the Claude usage badge in the status bar.

Agent Teams

Agent Teams lets Claude Code spawn teammate agents as TUIC terminal tabs. Enable it in Settings > Agents > Agent Teams.

When enabled, PTY sessions receive the CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 environment variable, which unlocks Claude Code’s TeamCreate, TaskCreate, and SendMessage tools. Agent spawning uses direct MCP tool calls (agent spawn) — the earlier it2 shim approach (iTerm2 CLI emulation) is deprecated.

Spawned sessions automatically emit lifecycle events (session-created, session-closed) so they appear as tabs and clean up on exit.

Session Binding (TUIC_SESSION)

Every terminal tab has a stable UUID that persists across app restarts. This UUID is injected into the PTY shell as the TUIC_SESSION environment variable.

How It Works

  1. When a terminal tab is created, a UUID is generated via crypto.randomUUID()
  2. The UUID is saved with the tab and restored when the app restarts
  3. On PTY creation, the UUID is injected as TUIC_SESSION=<uuid> in the shell environment
  4. Agents can use $TUIC_SESSION for session-specific operations

Use Cases

Start a Claude Code session bound to this tab:

claude --session-id $TUIC_SESSION

Claude Code stores the session locally. When you restart TUICommander and switch to this branch, the session resumes automatically via claude --resume <uuid>.

Resume a specific session (manual):

claude --resume $TUIC_SESSION

Gemini CLI session binding:

gemini --resume $TUIC_SESSION

Custom scripts that persist state per-tab:

# Use TUIC_SESSION as a stable key for any tab-specific state
echo "Last run: $(date)" > "/tmp/tuic-$TUIC_SESSION.log"

Automatic Resume

When TUICommander restores saved terminals after a restart, it checks whether the agent session file exists on disk before deciding the resume strategy:

  1. Verified session — If $TUIC_SESSION maps to an existing session file (e.g. ~/.claude/projects/…/<uuid>.jsonl), the agent resumes with --resume <uuid>
  2. No session file — Falls back to the agent’s default resume behavior (e.g. claude --continue for the last session)
  3. No agent detected — Tab opens a plain shell; $TUIC_SESSION is still available for manual use

UI Agent Spawn

When you spawn an agent via the context menu or command palette, TUICommander automatically uses the tab’s TUIC_SESSION as the --session-id. This ensures the spawned session is bound to the tab and will resume correctly on restart.

Sleep Prevention

When agents are actively working, TUICommander can keep your machine awake:

  • Enable in SettingsGeneralPrevent sleep when busy
  • Uses the keepawake system integration
  • Automatically releases when all agents are idle

Environment Flags

Per-agent environment variables can be injected into every new terminal session. Configure in Settings > Agents > expand an agent > Environment Flags.

This is useful for enabling feature flags (e.g., CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1) without manually running export commands. Flags are organized by category with toggle, enum, and number types.

Tips

  • Multiple agents on the same repo — Use split panes (Cmd+\) to run two agents side by side on the same branch
  • Different agents per branch — Each worktree is independent, so you can run Claude on one branch and Aider on another
  • Monitor all at once — Use the Activity Dashboard (Cmd+Shift+A) to see every terminal’s agent status in one view

Agent Teams

Agent Teams let Claude Code spawn teammate agents that work in parallel, each in its own TUICommander terminal tab. Teammates share a task list, communicate directly with each other, and coordinate autonomously.

How It Works

TUICommander automatically injects CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 into every PTY session. This unlocks Claude Code’s TeamCreate, TaskCreate, and SendMessage tools. When Claude Code spawns a teammate, TUICommander creates a new terminal tab via its MCP agent spawn tool — no external dependencies required.

Setup

No configuration needed. Agent Teams is enabled by default for all Claude Code sessions launched from TUICommander.

To verify it’s active, check the environment inside any terminal:

echo $CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS
# Should print: 1

Usage

Tell Claude Code to create a team using natural language:

Create an agent team with 3 teammates to review this PR:
- One focused on security
- One on performance
- One on test coverage

Claude Code handles team creation, task assignment, and coordination. Each teammate appears as a separate tab in TUICommander’s sidebar.

Claude Code supports two display modes for teammates:

ModeHow it worksRequirement
In-processAll teammates run inside the lead’s terminal. Use Shift+Down to cycle between them.None
Split panesEach teammate gets its own pane.tmux or iTerm2

TUICommander works with both modes. In-process mode is the default and requires no extra setup. With split panes, each teammate appears as a separate TUICommander tab.

Key Controls (In-process Mode)

KeyAction
Shift+DownCycle to next teammate
EnterView a teammate’s session
EscapeInterrupt a teammate’s current turn
Ctrl+TToggle the shared task list

What Teams Can Do

  • Shared task list — All teammates see task status and self-claim available work
  • Direct messaging — Teammates message each other without going through the lead
  • Plan approval — Require teammates to plan before implementing; the lead reviews and approves
  • Parallel work — Each teammate has its own context window and works independently

Good Use Cases

  • Code review — Split review criteria across security, performance, and test coverage reviewers
  • Research — Multiple teammates investigate different aspects of a problem simultaneously
  • Competing hypotheses — Teammates test different debugging theories in parallel and challenge each other
  • New features — Each teammate owns a separate module with no file conflicts

Limitations

Agent Teams is an experimental Claude Code feature. Current limitations:

  • No session resumption/resume does not restore in-process teammates
  • One team per session — Clean up before starting a new team
  • No nested teams — Teammates cannot spawn their own teams
  • Token cost — Each teammate is a separate Claude instance; costs scale linearly with team size
  • File conflicts — Two teammates editing the same file leads to overwrites; assign distinct files to each

Troubleshooting

Teammates not appearing as tabs:

  • Verify the env var is set: echo $CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS should print 1
  • Check that TUICommander’s MCP server is running (status bar shows the MCP icon)

Teammates not spawning at all:

  • Claude Code decides whether to create a team based on task complexity. Be explicit: “Create an agent team with N teammates”
  • Check Claude Code version: Agent Teams requires a recent version

Too many permission prompts:

  • Pre-approve common operations in Claude Code’s permission settings before spawning teammates

Inter-Agent Messaging

TUICommander includes a built-in messaging system that lets agents in different terminal tabs communicate directly. This works alongside (and independently from) Claude Code’s native Agent Teams messaging.

How It Works

Every PTY session gets a stable TUIC_SESSION UUID injected as an environment variable. Agents use this as their identity to register, discover peers, and exchange messages through TUICommander’s MCP messaging tool.

When both sender and recipient are connected via SSE (the default for Claude Code agents), messages are pushed in real-time as MCP channel notifications (notifications/claude/channel). They also land in a buffered inbox as a fallback.

What Gets Injected Automatically

TUICommander injects these into every Claude Code PTY session — no manual configuration needed:

Variable / FlagValuePurpose
TUIC_SESSIONStable UUID per tabAgent identity for messaging
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS1Unlocks TeamCreate/TaskCreate/SendMessage
--dangerously-load-development-channels server:tuicommander(CLI flag, agent spawn only)Enables real-time channel push from TUICommander

Messaging Flow

  1. Register — The agent reads its $TUIC_SESSION and registers as a peer:

    messaging action=register tuic_session="$TUIC_SESSION" name="worker-1" project="/path/to/repo"
    
  2. Discover peers — Find other agents connected to TUICommander:

    messaging action=list_peers
    messaging action=list_peers project="/path/to/repo"   # filter by repo
    
  3. Send a message — Address by the recipient’s tuic_session UUID:

    messaging action=send to="<recipient-tuic-session>" message="PR review done, 3 issues found"
    
  4. Check inbox — Read buffered messages (useful if channel push was missed):

    messaging action=inbox
    messaging action=inbox limit=10 since=1712000000000
    

Channel Push vs Inbox

DeliveryWhenLatencyRequires
Channel pushRecipient has active SSE streamReal-time--dangerously-load-development-channels server:tuicommander on the recipient’s CC process
Inbox bufferAlwaysPoll-basedRegistration only

Messages are always buffered in the inbox regardless of whether channel push succeeds. The inbox holds up to 128 messages per agent (FIFO eviction). Individual messages are capped at 64 KB.

Using Messaging from a Standalone Claude Code Session

If you run Claude Code outside TUICommander but still want to use TUIC messaging, you need to:

  1. Set TUIC_SESSION — export a stable UUID:

    export TUIC_SESSION=$(uuidgen)
    
  2. Connect to TUIC’s MCP server — add to your .mcp.json:

    {
      "mcpServers": {
        "tuicommander": {
          "url": "http://localhost:17463/mcp"
        }
      }
    }
    
  3. Enable channel push (optional, for real-time delivery):

    claude --dangerously-load-development-channels server:tuicommander
    
  4. Register on first turn — the agent must call messaging action=register with its $TUIC_SESSION before sending or receiving.

Messaging vs Claude Code Native SendMessage

FeatureTUIC MessagingCC Native SendMessage
TransportMCP tool call → server-side routingFile append + polling (~/.claude/teams/)
Real-time pushYes (MCP channel notifications)No (polling only)
Cross-appAny MCP client can participateClaude Code processes only
Discoverylist_peers with project filterTeam config file
PersistenceIn-memory ring buffer (lost on TUIC restart)Files on disk (survives restart)

Both systems work simultaneously. Claude Code agents spawned by TUICommander can use either or both.

Deprecated: it2 Shim

Earlier versions of TUICommander used an it2 shell script shim that emulated iTerm2’s CLI to intercept teammate creation. This approach is deprecated — teammate spawning now uses direct MCP tool calls (agent spawn). The shim at ~/.tuicommander/bin/it2 is no longer needed.

Smart Prompts

One-click AI automation for common git and code tasks. Smart Prompts inject context-aware commands into your active agent terminal, run headless one-shot operations, execute shell scripts directly, or call LLM APIs.

What are Smart Prompts?

Smart Prompts are context-aware automation shortcuts that turn repetitive developer workflows into single-click actions. They automatically resolve git context (branch, diff, changed files, PR data) and deliver a well-crafted prompt to your AI agent — no manual typing, no copy-pasting, no context switching.

TUICommander ships with 24 built-in prompts covering commit workflows, code review, PR management, CI fixes, and code investigation. Each prompt includes a description explaining what it does, so you always know what will happen before clicking.

How to Use

Toolbar Dropdown

Press Cmd+Shift+K (Ctrl+Shift+K on Windows/Linux) or click the lightning bolt icon in the toolbar. The dropdown shows all enabled prompts grouped by category with a search bar at the top.

Git Panel — Changes Tab

The SmartButtonStrip appears above the changed files list. Quick access to prompts like Smart Commit, Review Changes, and Write Tests.

PR Detail Popover

Click a PR badge in the sidebar to open the popover. The SmartButtonStrip shows PR-specific prompts: Review PR, Address Review Comments, Fix CI Failures, Update PR Description.

Command Palette

Open with Cmd+P and type “Smart” to see all smart prompts prefixed with “Smart:”.

Branch Context Menu

Right-click a branch in the Branches tab (Cmd+G) for branch-specific prompts like Create PR, Merge Main Into Branch, and Summarize Branch.

Built-in Prompts

CategoryPrompts
Git & CommitSmart Commit, Commit & Push, Amend Commit, Generate Commit Message
Code ReviewReview Changes, Review Staged, Review PR, Address Review Comments
Pull RequestsCreate PR, Update PR Description, Generate PR Description
Merge & ConflictsResolve Conflicts, Merge Main Into Branch, Rebase on Main
CI & QualityFix CI Failures, Fix Lint Issues, Write Tests, Run & Fix Tests
InvestigationInvestigate Issue, What Changed?, Summarize Branch, Explain Changes
Code OperationsSuggest Refactoring, Security Audit

Customizing Smart Prompts

Open Settings > Smart Prompts to manage prompts:

  • Enable/disable individual prompts (disabled prompts are hidden from all UI surfaces)
  • Edit prompt content — built-in prompts show a “Reset to default” button to revert your changes
  • Create your own smart prompts with the same placement options and variable system
  • View each prompt’s placement (toolbar, git-changes, pr-popover, git-branches) and execution mode

Status Feedback

When prompts cannot execute, the dropdown shows a status banner at the top explaining why:

  • “No active terminal” — open a terminal first
  • “No AI agent detected” — the active terminal has no agent running
  • “Agent is busy” — wait for the current operation to finish

Items are visually dimmed but visible, so you can still browse what’s available.

Context Variables

Prompts use {variable_name} syntax. Most variables are auto-resolved at execution time — no manual input needed.

Git Context (from Rust backend)

VariableDescription
{branch}Current branch name
{base_branch}Default branch (main/master/develop)
{repo_name}Repository directory name
{repo_path}Full filesystem path to the repository root
{diff}Working tree diff (truncated to 50KB)
{staged_diff}Staged changes diff (truncated to 50KB)
{changed_files}Short status output
{commit_log}Last 20 commits
{last_commit}Last commit hash + message
{conflict_files}Files with merge conflicts
{stash_list}Stash entries

GitHub/PR Context (from frontend stores)

VariableDescription
{pr_number}PR number for current branch
{pr_title}PR title
{pr_url}GitHub pull request URL
{pr_state}PR state: OPEN, MERGED, or CLOSED
{pr_checks}CI check summary (e.g. “3 passed, 1 failed”)
{merge_status}Merge status: MERGEABLE, CONFLICTING, or BEHIND
{review_decision}Review status: APPROVED, CHANGES_REQUESTED, or REVIEW_REQUIRED

Agent/Terminal Context

VariableDescription
{agent_type}Active agent type (claude, aider, codex, etc.)
{cwd}Active terminal working directory

Manual Input Variables

VariableDescription
{issue_number}GitHub issue number to investigate

Variable Input Dialog

When a prompt contains variables that cannot be auto-resolved, a Variable Input Dialog appears before execution. Each field shows the variable name and a human-readable description, so you know exactly what to fill in. Pre-populated suggestions are shown where available.

Execution Modes

Inject Mode (Default)

The resolved prompt is written directly into the active terminal’s PTY — as if you typed it. The agent processes it like any other input. Before sending, TUICommander checks that the agent is idle (configurable per prompt).

Shell Script Mode

Executes the prompt content as a shell script directly — no AI agent involved. The content runs via the system shell (sh -c on macOS/Linux, cmd /C on Windows) in the active repository’s directory.

Useful for automating repetitive CLI tasks: pruning orphan branches, running linters, collecting metrics, or any command pipeline you’d otherwise type manually. Context variables like {branch} and {repo_path} are resolved before execution.

Output is routed based on the prompt’s output target (clipboard, toast, panel, or returned in result). Timeout: 60 seconds.

Headless Mode

Runs a one-shot subprocess without using the terminal. Useful for quick operations like generating a commit message. Output is routed to the clipboard or shown as a toast notification.

Setup: Go to Settings > Agents and configure the “Headless Command Template” for each agent type. The template uses {prompt} as a placeholder:

  • Claude: claude -p "{prompt}"
  • Gemini: gemini -p "{prompt}"

Without a template, headless prompts automatically fall back to inject mode.

Note: Headless mode is not available in the Mobile Companion (PWA) — prompts fall back to inject mode automatically.

API Mode

Calls LLM providers directly via HTTP API without terminal or agent CLI. Supports an optional system prompt per prompt. Output routed via the same output target options. Requires LLM API configuration in Settings > Agents.

Smart Prompts Library

The Smart Prompts Library stores and manages all your prompt templates — both the 24 built-in AI automation prompts and your custom templates. Prompts are injected directly into the active agent terminal or run headless for quick one-shot operations.

Looking for one-click AI automation? See Smart Prompts for the full guide on built-in automation prompts, context variables, and headless execution.

Opening the Drawer

  • Cmd+Shift+K — Toggle the prompt library drawer
  • Toolbar button — Prompt library icon in the main toolbar

Browsing and Searching

When the drawer opens, the search input is focused automatically. Type to filter prompts by name, description, or content. Matching is case-insensitive and searches all three fields simultaneously.

Use the category tabs to narrow the list:

TabShows
AllEvery saved prompt, sorted by most recently used
CustomUser-created prompts
FavoritesPrompts you have starred
RecentLast 10 prompts you used

Keyboard Navigation

KeyAction
/ Move selection up/down
EnterInsert selected prompt into terminal
Double-clickInsert and immediately execute (adds newline)
Ctrl+N / Cmd+NCreate a new prompt
Ctrl+E / Cmd+EEdit the selected prompt
Ctrl+F / Cmd+FToggle favorite on the selected prompt
EscapeClose the drawer

Creating a Prompt

  1. Open the drawer (Cmd+Shift+K) and click + New Prompt, or press Ctrl+N/Cmd+N
  2. Fill in the fields:
    • Name (required) — shown in the list
    • Description — optional subtitle, also searchable
    • Content (required) — the text to insert; use {{variable}} for dynamic values
    • Keyboard Shortcut — optional global shortcut to trigger this prompt directly
  3. Click Save

Editing and Deleting

  • Click the pencil icon on any prompt row, or select it and press Ctrl+E/Cmd+E
  • Click the trash icon to delete — a confirmation dialog appears before deletion

Variable Substitution

Use {{variable_name}} placeholders in prompt content. When you send a prompt that contains variables, a dialog appears asking you to fill in each value before injection.

cd {{project_dir}} && cargo test -- {{test_filter}}

Built-in Variables

These are resolved automatically by the backend when present:

VariableValue
{{diff}}Current git diff
{{changed_files}}List of changed files
{{repo_name}}Repository name
{{branch}}Current branch name
{{cwd}}Current working directory

Custom Variables

Any {{name}} not in the built-in list becomes a custom input field in the variable dialog. You can optionally add a description and default value per variable when editing the prompt — the description appears as placeholder text in the dialog.

Inserting with Variables

The variable dialog offers two actions:

  • Insert — writes the resolved text to the terminal input line (you can review before pressing Enter)
  • Insert & Run — appends a newline, sending the command immediately

Favorites and Pinning

Click the star icon on any prompt row to toggle its favorite status. Favorited prompts appear at the top of any list view with a prefix and are accessible via the Favorites category tab.

Recently Used

The Recent tab shows the last 10 prompts you sent, in order of use. Recency is also used to sort the All view — most recently used prompts appear first.

Sending to Terminal

Selecting a prompt (click or Enter) writes its content to the currently active terminal. If the prompt has no variables, it is injected immediately. If it does, the variable dialog appears first.

The drawer closes automatically after a successful injection and focus returns to the terminal.


Run Commands

A lighter-weight alternative for per-branch one-off commands:

  • Cmd+R — Run the saved command for the active branch
  • Cmd+Shift+R — Edit the command before running

Configure run commands in Settings → Repository → Scripts tab.

Voice Dictation

TUICommander includes local voice-to-text using Whisper AI. All processing happens on your machine — no cloud services.

Setup

  1. Open Settings → Services → Voice Dictation
  2. Enable dictation
  3. Download a Whisper model (recommended: large-v3-turbo, ~1.6 GB)
  4. Wait for download to complete (progress shown in UI)
  5. Optionally configure language and hotkey

Usage

Push-to-talk workflow:

  1. Hold the dictation hotkey (default: F5) or the mic button in the status bar
  2. Speak your text
  3. Release the key/button
  4. Transcribed text is inserted into the focused input element (textarea, input, or contenteditable). If no text input has focus, the text falls back to the active terminal PTY. The focus target is captured at key-press time.

The hotkey works globally — even when TUICommander is not focused.

Models

ModelSizeQuality
tiny~75 MBLow (fast, inaccurate)
base~140 MBFair
small~460 MBGood
medium~1.5 GBVery good
large-v3-turbo~1.6 GBBest (recommended)

Models are downloaded to <config_dir>/models/ and cached between sessions.

Languages

Auto-detect (default), or set explicitly: English, Spanish, French, German, Italian, Portuguese, Dutch, Japanese, Chinese, Korean, Russian.

Text Corrections

Configure word replacements applied after transcription:

SpokenReplaced with
“new line”\n
“tab”\t
“period”.

Add custom corrections in Settings → Services → Dictation → Corrections.

Audio Device

Select which microphone to use from the dropdown in dictation settings. Lists all available input devices.

Platform Notes

  • macOS: GPU-accelerated transcription via Metal
  • Linux/Windows: CPU-only transcription
  • Microphone permission is requested on first use (not at app startup)

Status Indicators

IndicatorMeaning
Mic button (status bar)Click/hold to start recording
Recording animationAudio is being captured
Processing spinnerWhisper is transcribing
Model downloadingProgress bar with percentage

Hotkey Configuration

Change the push-to-talk hotkey in Settings → Services → Dictation. The hotkey is registered globally via Tauri’s global-shortcut plugin.

Default: F5

Auto-Send

Enable ‘Auto-send’ in Settings > Services > Dictation to automatically press Enter after the transcribed text is inserted into the terminal. Useful when dictating commands.

Branch Management

TUICommander has a built-in Branches tab inside the Git Panel. It lets you view, create, delete, rename, merge, rebase, push, pull, and compare branches — all without leaving the app.

Opening the Branch Panel

Three ways to open it:

  • Cmd+G — Opens the Git Panel directly on the Branches tab
  • Click the “GIT” vertical label in the sidebar — Opens the Git Panel on the Branches tab
  • Cmd+Shift+D, then click the “Branches” tab header — Opens the Git Panel on the last active tab; click Branches to switch

Branch List Overview

The panel shows two collapsible sections:

  • Local — all branches in your local repo
  • Remote — tracking branches from all remotes

Each branch row displays:

  • Branch name
  • Ahead/behind counts relative to its upstream (e.g. ↑2 ↓1)
  • Relative date of the last commit (e.g. “3h ago”, “2d ago”)
  • Merged badge — shown on branches already merged into the default branch
  • Stale dimming — branches with no commit in the last 30 days appear dimmed

A Recent Branches section at the top shows recently checked-out branches from the git reflog, for quick re-access.

Prefix Folding

When you have many branches following a naming convention (feature/, bugfix/, chore/), prefix folding groups them automatically:

  • Branches sharing a common /-delimited prefix collapse into a folder row (e.g. feature/ (5))
  • Click the folder row (or press / ) to expand or collapse it
  • The toggle button in the panel header enables or disables prefix folding globally

Search / Filter

Type in the search bar at the top of the panel to filter branches by name. The filter applies to all sections simultaneously. Press Escape to clear the search.

Keyboard Operations

With the Branches panel focused:

KeyAction
/ Navigate branches
EnterCheckout selected branch
nCreate new branch
dDelete selected branch
RRename selected branch (inline edit)
MMerge selected branch into current
rRebase current branch onto selected
PPush selected branch
pPull current branch
fFetch all remotes
EscapeClose panel

Switching between Git Panel tabs:

KeyTab
Ctrl/Cmd+1Changes
Ctrl/Cmd+2Log
Ctrl/Cmd+3Stashes
Ctrl/Cmd+4Branches

Checkout

Press Enter (or double-click) on any branch to check it out.

If your working tree has uncommitted changes, a dialog appears with three options:

  • Stash — automatically stashes changes, then checks out
  • Force — discards changes and checks out
  • Cancel — aborts the checkout

Create Branch

Press n to open the inline branch creation form:

  1. Type the new branch name
  2. Optionally change the start point (defaults to HEAD)
  3. Toggle “Checkout after create” (on by default)
  4. Press Enter to confirm or Escape to cancel

Delete Branch

Press d to delete the selected branch.

  • Uses safe delete (git branch -d) by default — refuses to delete unmerged branches
  • A confirmation prompt lets you switch to force-delete (git branch -D) if needed
  • Deleting the current branch or the default branch (main, master, develop) is blocked

Rename Branch

Press R to edit the branch name inline. The current name is pre-filled. Press Enter to confirm or Escape to cancel.

Merge

Press M to merge the selected branch into the current branch. The merge runs in the background. Conflicts are reported in the Git Panel header.

Rebase

Press r to rebase the current branch onto the selected branch. Runs in the background; conflicts are reported.

Push

Press P to push the current branch. If no upstream is set, TUICommander automatically configures the tracking relationship (--set-upstream origin <branch>).

Pull

Press p to pull the current branch from its upstream.

Fetch

Press f to fetch all remotes (git fetch --all).

Context Menu

Right-click any branch for the full context menu:

ActionDescription
CheckoutSwitch to this branch
Create Branch from HereCreate a new branch starting from this commit
DeleteDelete branch (safe by default)
RenameRename inline
Merge into CurrentMerge this branch into the current one
Rebase Current onto ThisRebase current branch onto this one
PushPush this branch
PullPull this branch
FetchFetch all remotes
CompareShow git diff --name-status between this branch and current

Stale and Merged Indicators

  • Stale (dimmed): the branch has no commits in the last 30 days — a visual cue that it may be abandoned
  • Merged badge: the branch has been merged into the default branch and is safe to delete

Git Worktrees

TUICommander uses git worktrees to give each branch an isolated working directory.

What Are Worktrees?

Git worktrees let you check out multiple branches simultaneously, each in its own directory. Instead of stashing or committing before switching branches, each branch has its own complete copy of the files.

How TUICommander Uses Them

When you click a non-main branch in the sidebar:

  1. TUICommander creates a git worktree for that branch
  2. A terminal opens in the worktree directory
  3. You work independently without affecting other branches

Main branches (main, master, develop) use the original repository directory — no worktree is created.

Worktree Storage Strategies

Configure where worktrees are stored (Settings → General → Worktree Defaults → Storage):

StrategyLocationUse case
Sibling (default){repo_parent}/{repo_name}__wt/Keeps worktrees near the repo
App directory~/Library/Application Support/tuicommander/worktrees/{repo_name}/Centralised storage
Inside repo{repo_path}/.worktrees/Self-contained, add to .gitignore
Claude Code default{repo_path}/.claude/worktrees/Compatible with Claude Code’s native EnterWorktree

Override per-repo in Settings → Repository → Worktree.

Creating Worktrees

From the + Button (with prompt)

Click + next to a repository name. A dialog opens where you can:

  • Type a new branch name (creates branch + worktree)
  • Select an existing branch from the list
  • Choose a “Start from” base ref (default branch, or any local branch)
  • Generate a random sci-fi name

From the + Button (instant mode)

When “Prompt on create” is off (Settings → Worktree Defaults), clicking + instantly creates a worktree with an auto-generated name based on the default branch.

From Branch Right-Click (quick-clone)

Right-click any non-main branch without a worktree → Create Worktree. This creates a new branch named {source}--{random-name} based on the selected branch, with a worktree directory.

Worktree Settings

Global defaults apply to all repos. Per-repo overrides take precedence when set.

Global Defaults (Settings → General → Worktree Defaults)

SettingOptionsDefault
StorageSibling / App directory / Inside repoSibling
Prompt on createOn / OffOn
Delete branch on removeOn / OffOn
Auto-archive mergedOn / OffOff
Orphan cleanupManual / Prompt / AutoManual
PR merge strategyMerge / Squash / RebaseMerge
After mergeArchive / Delete / AskArchive

Per-Repository Overrides (Settings → Repository → Worktree)

Each setting can use the global default or be overridden for a specific repository.

Merge & Archive

Right-click a worktree branch → Merge & Archive to:

  1. Merge the branch into the main branch
  2. Handle the worktree based on the “After merge” setting:
    • Archive: Moves the worktree directory to __archived/ (accessible but removed from sidebar)
    • Delete: Removes the worktree and branch entirely
    • Ask: Merge succeeds, then you choose what to do

The merge uses --no-edit for a clean fast-forward or merge commit. If conflicts are detected, the merge is aborted and the worktree is left intact.

When using Ask mode, the cleanup dialog detects uncommitted changes and auto-stashes them during the branch switch. An “Unstash after switch” checkbox lets you restore changes on the target branch.

Archive Script

A per-repo lifecycle hook that runs before a worktree is archived or deleted. Configure it in Settings → Repository → Scripts tab, or via .tuic.json (archive_script field).

  • The script runs in the worktree directory that is about to be removed
  • If the script exits with a non-zero code, the archive/delete operation is blocked and an error is shown
  • Use cases: backing up local data, cleaning up resources, notifying external systems
  • The script is invoked via the platform shell (sh -c on macOS/Linux, cmd /C on Windows)

Moving Terminals Between Worktrees

Right-click a terminal tab → Move to Worktree to move it to a different worktree. The terminal will cd into the target worktree path, and the tab automatically reassigns to the new branch in the sidebar.

Also available via Command Palette — type “move to worktree” to see available targets for the active terminal.

Removing Worktrees

  • Sidebar × button on a non-main branch — Removes worktree and branch entry
  • Right-click → Delete Worktree — Context menu option
  • Both prompt for confirmation

Removing a worktree:

  1. Closes all terminals associated with that branch
  2. Runs git worktree remove to clean up
  3. Removes the branch entry from the sidebar

Worktree Manager Panel

Open the Worktree Manager with Cmd+Shift+W (or via the Command Palette → “Worktree Manager”). It shows a unified view of all worktrees across your repositories.

What It Shows

Each worktree row displays:

  • Branch name and repository badge
  • Dirty status — file additions/deletions, or “clean”
  • PR state — open (with PR number), merged, or closed
  • Last commit timestamp — relative time since last activity
  • Main badge — marks the main branch (actions disabled)

Orphan worktrees (detached HEAD or deleted branch) appear at the bottom with a warning badge and a Prune button to clean them up.

Filtering

  • Repo pills — Click a repository name to filter by repo (appears when you have multiple repos)
  • Text search — Type in the search field to filter branches by name
  • Filters compose: selecting a repo and typing text shows only matching branches in that repo

Single-Row Actions

Each worktree row has action buttons (visible on the right):

  • >_ — Open a terminal in the worktree directory
  • — Merge the branch into main and archive (disabled for main branches)
  • — Delete the worktree and branch (disabled for main branches)

Batch Operations

Select multiple worktrees using the checkboxes (shown when more than one selectable worktree exists). A batch bar appears with:

  • Merge & Archive (N) — Merges and archives all selected branches
  • Delete (N) — Deletes all selected worktrees

Use the Select All checkbox in the toolbar to toggle all non-main worktrees.

MCP Worktree Creation (AI Agents)

AI agents connected via MCP can create worktrees using the worktree action=create tool.

Claude Code — Agent Bridge

Claude Code cannot change its working directory mid-session. When CC creates a worktree via MCP, the response includes a cc_agent_hint field with:

  • worktree_path — Absolute path to the worktree directory
  • suggested_prompt — Instructions for spawning a subagent that works in the worktree using absolute paths

CC should spawn a subagent (Agent tool) with the suggested prompt. The subagent uses Read, Edit, Glob, Grep with absolute file paths and cd <path> && ... for shell commands.

Other MCP Clients

Non-Claude Code MCP clients receive the standard {worktree_path, branch} response without the cc_agent_hint field. These clients can change into the worktree directory directly.

External Worktree Detection

TUICommander monitors .git/worktrees/ for changes. Worktrees created outside the app (via CLI or other tools) are detected and appear in the sidebar after the next refresh.

Branch Switching

Switching branches in TUICommander does not change the working directory of existing terminals. Each branch’s terminals stay in their worktree path.

When you switch branches:

  • Previous branch’s terminals are hidden (but remain alive)
  • New branch’s terminals are shown
  • If the new branch has no terminals, a fresh one is created

GitHub Integration

TUICommander monitors your GitHub PRs and CI status automatically.

Authentication

TUICommander needs a GitHub token to access PRs and CI status. You have two options:

  1. Open Settings > GitHub
  2. Click “Login with GitHub”
  3. A code appears — it’s auto-copied to your clipboard
  4. Your browser opens GitHub’s authorization page
  5. Paste the code and authorize
  6. Done — the token is stored securely in your OS keyring

This method automatically requests the correct scopes (repo, read:org) and works with private repositories and organization repos.

Option 2: gh CLI

If you prefer, install the gh CLI and run gh auth login. TUICommander will use the gh token automatically.

Token Priority

When multiple sources are available, TUICommander uses this priority:

  1. GH_TOKEN environment variable
  2. GITHUB_TOKEN environment variable
  3. OAuth token (from Settings login)
  4. gh CLI token

Requirements

  • GitHub authentication (see above)
  • Repository with a GitHub remote

PR Monitoring

When you have a branch with an open pull request, the sidebar shows:

PR Badge

A colored badge next to the branch name showing the PR number:

ColorState
GreenOpen PR
PurpleMerged
RedClosed
Gray/dimDraft

Click the PR badge to open the PR detail popover.

CI Ring

A circular indicator showing CI check status:

SegmentColorMeaning
Green arcPassed checks
Red arcFailed checks
Yellow arcPending checks

The ring segments are proportional to the number of checks in each state. Click to see detailed CI check information.

Diff Stats

Per-branch addition/deletion counts shown as +N / -N next to the branch name.

PR Detail Popover

Click a PR badge or CI ring to open the detail popover. Shows:

  • PR title and number with link to GitHub
  • Author and timestamps (created, last updated)
  • State indicators:
    • Draft/Open/Merged/Closed
    • Merge readiness (Ready to merge, Has conflicts, Behind base, Blocked)
    • Review decision (Approved, Changes requested, Review required)
  • CI check details — Individual check names and status
  • Labels — With GitHub-matching colors
  • Line changes — Total additions and deletions
  • Commit count

PR Notifications (Toolbar Bell)

When any branch has a PR event that needs attention, a bell icon with a count badge appears in the toolbar.

Click the bell to see all active notifications in a popover list. Each notification shows the repo, branch, and event type.

Notification Types

TypeMeaning
MergedPR was merged
ClosedPR was closed without merge
ConflictsMerge conflicts detected
CI FailedOne or more CI checks failed
Changes Req.Reviewer requested changes
ReadyPR is ready to merge (all checks pass, approved)

Interacting with Notifications

  • Click a notification item — Opens the full PR detail popover for that branch
  • Click the dismiss (x) button on an item — Dismiss that single notification
  • Click “Dismiss All” — Clear all notifications at once

PR Badge on Sidebar Branches

Click the colored PR status badge on any branch in the sidebar to open the PR detail popover directly.

Polling

GitHub data is polled automatically:

  • Active window: Every 30 seconds
  • Hidden window: Every 2 minutes (reduced to save API budget)
  • API budget: ~2 calls/min/repo = 1,200/hr for 10 repos (GitHub limit: 5,000/hr)

Polling starts automatically when a repository with a GitHub remote is active.

Merge State Classification

StateLabelMeaning
MERGEABLE + CLEANReady to mergeAll checks pass, no conflicts
MERGEABLE + UNSTABLEChecks failingMergeable but some checks fail
CONFLICTINGHas conflictsMerge conflicts with base branch
BEHINDBehind baseBase branch has newer commits
BLOCKEDBlockedBranch protection prevents merge
DRAFTDraftPR is in draft state

Review State Classification

DecisionLabel
APPROVEDApproved
CHANGES_REQUESTEDChanges requested
REVIEW_REQUIREDReview required

Auto-Delete Branch on PR Close

When a PR is merged or closed on GitHub, TUICommander can automatically clean up the corresponding local branch. Configure per-repo in Settings > Repository Settings or set a global default in Settings > General > Repository Defaults.

ModeBehavior
Off (default)No action taken
AskShows a confirmation dialog before deleting
AutoDeletes silently; falls back to Ask if worktree has uncommitted changes

Safety guarantees:

  • The default/main branch is never deleted
  • If a branch has a linked worktree, the worktree is removed first
  • Uses safe git branch -d — refuses to delete branches with unmerged commits
  • Dirty worktrees (uncommitted changes) always escalate to Ask mode, even when set to Auto

Remote-Only Pull Requests

When a branch exists only on the remote (not checked out locally) but has an open PR, it still appears in the sidebar with a PR badge. These “remote-only” PRs support inline accordion actions:

  • Checkout — Creates a local tracking branch from the remote
  • Create Worktree — Creates a worktree for the branch

PR Detail Popover Actions

Clicking the PR badge on any branch (local or remote-only) opens the detail popover. Available actions:

ButtonWhen ShownWhat It Does
View DiffAlwaysOpens PR diff in a dedicated panel tab
MergePR is open, approved, CI greenMerges via GitHub API (auto-detects allowed merge method)
ApproveRemote-only PRsSubmits an approving review via GitHub API

Post-Merge Cleanup

After merging a PR from the popover, a cleanup dialog appears with checkable steps:

  1. Switch to base branch — if the working directory has uncommitted changes, they are automatically stashed. An inline warning shows with an optional “Unstash after switch” checkbox
  2. Pull base branch — fast-forward only
  3. Delete local branch — closes terminals first, refuses to delete default branch
  4. Delete remote branch — gracefully handles “already deleted”

Steps execute sequentially via the Rust backend (not PTY — your terminal may be occupied by an AI agent). Each step shows live status: pending → running → success/error.

Dismiss & Show Dismissed

Remote-only PRs can be dismissed from the sidebar to reduce clutter. A “Show Dismissed” toggle in the sidebar reveals them again.

GitHub Issues

The GitHub panel shows issues alongside PRs in a unified view.

Issue Filter

Control which issues appear using the filter dropdown in Settings > GitHub or directly in the panel:

FilterShows
Assigned (default)Issues assigned to you
CreatedIssues you opened
MentionedIssues that mention you
AllAll open issues in the repo
DisabledHides the issues section

The filter setting persists across sessions.

Issue Details

Expand an issue to see:

  • Labels with GitHub-matching colors
  • Assignees and milestone
  • Comment count and timestamps (created/updated)

Issue Actions

ActionDescription
Open in GitHubOpens the issue in your browser
Close / ReopenChanges issue state via GitHub API
Copy numberCopies #123 to clipboard

Troubleshooting

No PR data showing:

  1. Check gh auth status — must be authenticated
  2. Check repository has a GitHub remote (git remote -v)
  3. Check that gh pr list works in the repo directory

Stale data:

  • Click the refresh button or switch away and back to the branch
  • Polling updates every 30 seconds automatically

Plugins

TUICommander has an Obsidian-style plugin system. Plugins can watch terminal output, push notifications to the Activity Center, render markdown panels, control PTY sessions, and more.

Installing Plugins

From the Community Registry

  1. Open Settings (Cmd+,) → Plugins tab → Browse
  2. Browse available plugins — each shows name, description, and author
  3. Click Install on any plugin
  4. The plugin is downloaded and activated immediately

An “Update available” badge appears when a newer version exists in the registry.

From a ZIP File

  1. Open SettingsPluginsInstalled
  2. Click Install from file…
  3. Select a .zip archive containing the plugin

Click a link like tuic://install-plugin?url=https://example.com/plugin.zip — TUICommander shows a confirmation dialog, then downloads and installs the plugin. Only HTTPS URLs are accepted.

Manual Installation

Copy the plugin directory to:

  • macOS: ~/Library/Application Support/com.tuic.commander/plugins/my-plugin/
  • Linux: ~/.config/tuicommander/plugins/my-plugin/
  • Windows: %APPDATA%/com.tuic.commander/plugins/my-plugin/

A plugin directory contains at minimum manifest.json and main.js.

Managing Plugins

Settings → Plugins → Installed

The Installed tab lists all plugins (built-in and external):

  • Toggle switch — Enable or disable a plugin. Disabled plugins are not loaded but remain installed.
  • Logs — Click to expand the plugin’s log viewer. Shows recent activity and errors (500-entry ring buffer).
  • Uninstall — Remove the plugin directory (confirmation required). Built-in plugins cannot be uninstalled.

Error count badges appear on plugins that have logged errors.

Built-in Plugins

TUICommander ships with built-in plugins (e.g., Plan Tracker). These show a “Built-in” badge in the list. They can be disabled but not uninstalled.

How Plugins Work

Plugins interact with the app through a PluginHost API organized in 4 capability tiers:

TierAccessExamples
1Always availableWatch terminal output, add Activity Center items, provide markdown content
2Always availableRead repository list, active branch, terminal sessions (read-only)
3Requires capabilitySend input to terminals (pty:write — raw writePty or agent-aware sendAgentInput), open markdown panels (ui:markdown), play sounds (ui:sound), read/list/watch files (fs:read, fs:list, fs:watch)
4Requires capabilityInvoke whitelisted Tauri commands (invoke:read_file, invoke:list_markdown_files)

Capabilities are declared in the plugin’s manifest.json. A plugin without pty:write cannot send input to your terminals.

Activity Center

The toolbar bell icon is the Activity Center. Plugins contribute sections and items here:

  • Sections — Grouped headings (e.g., “ACTIVE PLAN”, “CI STATUS”)
  • Items — Individual notifications with icon, title, subtitle
  • Actions — Click an item to open its detail (usually a markdown panel), or dismiss it

The bell shows a count badge when there are active items.

Hot Reload

When you edit a plugin’s files, TUICommander detects the change and automatically reloads the plugin — no restart needed. Save main.js and see changes in seconds.

Example Plugins

TUICommander ships with example plugins in examples/plugins/:

PluginWhat it does
hello-worldMinimal example — watches terminal output, adds Activity Center items
auto-confirmAuto-responds to Y/N prompts in terminal
ci-notifierSound notifications and markdown panels for CI events
repo-dashboardReads repo state, generates dynamic markdown summaries
report-watcherWatches terminal for generated report files, shows them in Activity Center

Writing Your Own Plugin

See the Plugin Authoring Guide for the full API reference, manifest format, capability details, structured event types, and testing patterns.

Troubleshooting

ProblemFix
Plugin not appearingCheck that manifest.json exists and id matches the directory name
“Requires app version X.Y.Z”Update TUICommander or lower minAppVersion in the manifest
“Requires capability X”Add the capability to the capabilities array in manifest.json
Changes not taking effectSave the file again to trigger hot reload, or restart the app
Plugin errorsCheck Settings → Plugins → Logs for the plugin’s error log

MCP Proxy Hub

TUICommander can act as a universal MCP (Model Context Protocol) proxy. Instead of configuring the same MCP servers in Claude Code, Cursor, and VS Code separately, you configure them once in TUICommander. All your AI clients connect to TUICommander’s single /mcp endpoint and get access to every upstream tool automatically.

How It Works

Claude Code ──┐
Cursor ───────┼──▶  TUICommander /mcp  ──┬──▶ GitHub MCP
VS Code ──────┘                           ├──▶ Filesystem MCP
                                          └──▶ Any MCP server

When a tool call arrives at TUICommander’s MCP endpoint, it routes the request to the correct upstream server and returns the result. Upstream tools appear prefixed with the server name — for example, a tool called search_code from an upstream named github becomes github__search_code.

The MCP server must be enabled (Settings > Services > MCP Server).

Adding an Upstream Server

Open Settings > Services > MCP Upstreams. Click Add Server and fill in:

HTTP Server

Use this for MCP servers that expose a Streamable HTTP endpoint.

FieldExampleNotes
NamegithubLowercase letters, digits, hyphens, underscores only
TypeHTTP
URLhttps://mcp.example.com/mcpMust be http:// or https://
Timeout30Seconds per request. 0 = no timeout
EnabledOnUncheck to disable without removing

Stdio Server

Use this for locally installed MCP servers (npm packages, Python scripts, etc.) that communicate over stdin/stdout.

FieldExampleNotes
NamefilesystemSame naming rules as above
TypeStdio
CommandnpxExecutable name or full path
Args-y @modelcontextprotocol/server-filesystemSpace-separated
EnvALLOWED_PATHS=/home/userOptional extra environment variables
EnabledOn

Click Save. TUICommander connects immediately — no restart required.

Server Names

The server name becomes the namespace prefix for all its tools. Choose names that are:

  • Descriptive and short (github, filesystem, db)
  • Lowercase only
  • No spaces, dots, or capital letters — only [a-z0-9_-]
  • Unique (no two servers can share a name)

Authentication

For HTTP upstream servers that require a Bearer token:

  1. Go to Settings > Services > MCP Upstreams
  2. Find your server in the list
  3. Click the key icon next to it
  4. Enter your token

The token is stored in the OS keyring (Keychain on macOS, Credential Manager on Windows) — never in the config file.

To remove a credential, click the key icon and leave the field empty, then save.

Tool Filtering

You can restrict which tools from an upstream are exposed to downstream clients. Edit a server and set the filter:

Allow list — only these tools are exposed:

Mode: allow
Patterns: read_*, list_*, get_*

Deny list — all tools except these are exposed:

Mode: deny
Patterns: delete_*, rm, drop_*, exec_*

Patterns support a trailing * for prefix matching. Exact names also work. There is no other wildcard syntax.

Upstream Status

Each upstream server has a status indicator:

StatusMeaning
ConnectingHandshake in progress
ReadyConnected, tools available
Circuit OpenToo many failures, retrying with backoff
DisabledDisabled by you in config
FailedPermanently failed — manual reconnect needed

Circuit breaker: If an upstream fails 3 times consecutively, TUICommander stops sending requests to it briefly. Retries use exponential backoff starting at 1 second, capping at 60 seconds. After 10 retry cycles without recovery, the server is marked Failed.

To reconnect a Failed server, click Reconnect next to its name in the settings panel.

Health Checks

TUICommander probes every Ready upstream every 60 seconds to verify it is still responding. If a probe fails, the circuit breaker activates. If a Circuit Open server’s backoff has expired, the health check also attempts recovery.

Hot-Reload

Adding, removing, or changing upstream servers takes effect immediately when you click Save. TUICommander computes a diff and only reconnects servers that actually changed — unchanged servers are never interrupted.

Troubleshooting

The upstream shows “Failed”

  1. Check the server URL or command is correct.
  2. Verify the server process is running (for stdio servers).
  3. Check credentials are set if the server requires authentication.
  4. Click Reconnect to retry.

Tools are not appearing

  • The upstream must be in Ready status for its tools to be included.
  • Check that a tool filter is not hiding the tools you expect.
  • Reconnect and check the error log (Cmd+Shift+E) for initialization errors.

“Circular proxy” error

The HTTP URL you configured points to TUICommander’s own MCP port. This would create an infinite loop. Use a different URL or port.

“Invalid URL scheme” error

Only http:// and https:// URLs are accepted. Other schemes (ftp, file, javascript, etc.) are rejected for security.

Stdio server crashes immediately

  • Confirm the command exists on PATH (or use the full absolute path).
  • Check the Args field for typos.
  • Use the error log (Cmd+Shift+E) to see the stderr output from the child process.
  • Note: the server cannot be respawned more than once every 5 seconds (rate limit).

Credential not found

If the upstream returns 401 errors:

  1. Go to Settings > Services > MCP Upstreams.
  2. Click the key icon for the server.
  3. Re-enter the Bearer token and save.

The credential lookup uses the server name as the keyring key. If you renamed the server, the old credential is no longer found — re-enter it under the new name.

Example: Connecting the MCP Filesystem Server

Install the server:

npm install -g @modelcontextprotocol/server-filesystem

Add it in Settings > Services > MCP Upstreams:

  • Name: filesystem
  • Type: Stdio
  • Command: npx
  • Args: -y @modelcontextprotocol/server-filesystem /path/to/allowed/dir

After saving, the tool filesystem__read_file (and others) will appear in your AI client’s tool list.

Example: Connecting a Remote HTTP MCP Server

  • Name: github
  • Type: HTTP
  • URL: https://api.example.com/mcp
  • Timeout: 30

Set the Bearer token via the key icon in settings. Tools appear as github__search_code, github__create_issue, etc.

Security Notes

  • Config files (mcp-upstreams.json) never contain credentials — only the upstream name is stored. Tokens live in the OS keyring only.
  • Stdio servers run with a sanitized environment. Your shell secrets (ANTHROPIC_API_KEY, AWS_SECRET_ACCESS_KEY, etc.) are not inherited by spawned MCP processes. Only PATH, HOME, USER, LANG, LC_ALL, TMPDIR, TEMP, TMP, SHELL, and TERM are passed through. Add anything else explicitly in the Env field.
  • Self-referential HTTP URLs (pointing to TUIC’s own MCP port) are rejected to prevent circular proxying.
  • Only http:// and https:// URL schemes are accepted.

Remote Access

Access TUICommander from a browser on another device on your network.

Setup

  1. Open Settings (Cmd+,) → ServicesRemote Access
  2. Configure:
    • Port — Default 9876 (range 1024–65535)
    • Username — Basic Auth username
    • Password — Basic Auth password (stored as a bcrypt hash, never in plaintext)
  3. Enable remote access

Once enabled, the settings panel shows the access URL: http://<your-ip>:<port>

Connecting from Another Device

  1. Open a browser on any device on the same network
  2. Navigate to the URL shown in settings (e.g., http://192.168.1.42:9876)
  3. Enter the username and password you configured
  4. TUICommander loads in the browser with full terminal access

QR Code

The settings panel shows a QR code for the access URL — scan it from a phone or tablet to connect quickly. The QR code uses your actual local IP address.

What Works Remotely

The browser client provides the same UI as the desktop app:

  • Terminal sessions (via WebSocket streaming)
  • Sidebar with repositories and branches
  • Diff, Markdown, and File Browser panels
  • Keyboard shortcuts

Security

  • Authentication — Basic Auth with bcrypt-hashed passwords
  • Local network only — The server binds to your machine’s IP; it’s not exposed to the internet unless you configure port forwarding (don’t do this without a VPN)
  • CORS — When remote access is enabled, any origin is allowed (necessary for browser access from different IPs)

MCP HTTP Server

Separate from remote access, TUICommander runs an HTTP API server for AI tool integration:

  • The server always listens on an IPC listener: Unix domain socket at <config_dir>/mcp.sock on macOS/Linux, or named pipe \\.\pipe\tuicommander-mcp on Windows
  • AI agents connect via the tuic-bridge sidecar binary, which translates MCP stdio transport to the IPC listener
  • Bridge configs are auto-installed on first launch for supported agents (Claude Code, Cursor, Windsurf, VS Code, Zed, Amp, Gemini). On every subsequent launch, the bridge path is verified and updated if stale (from reinstalls, updates, or moves)
  • The mcp_server_enabled toggle in SettingsServices controls whether MCP protocol tools are exposed, not the server itself
  • Shows server status and active session count in settings

The Unix socket is accessible only to the current user (filesystem permissions) and requires no authentication — it’s designed for local tool integration, not remote access.

Mobile Companion

TUICommander includes a phone-optimized interface for monitoring agents from your phone.

Accessing the Mobile UI

  1. Enable remote access (see Setup above)
  2. Navigate to http://<your-ip>:<port>/mobile from your phone
  3. Log in with your credentials

Add to Home Screen

The mobile UI supports PWA (Progressive Web App) installation:

  • iOS Safari: Tap Share → “Add to Home Screen”
  • Android Chrome: Tap the three-dot menu → “Add to Home screen”

The app launches in standalone mode (no browser chrome) for a native-like experience.

Mobile Features

  • Sessions list — See all running agents with status (idle, busy, question, rate-limited, error)
  • Session detail — Live output streaming, quick-reply chips (Yes/No/Enter/Ctrl-C), text input
  • Question banner — Instant notification when any agent needs input, with quick-reply buttons
  • Activity feed — Chronological event feed grouped by time
  • Notification sounds — Audio alerts for questions, errors, completions, and rate limits

Tips

  • Pull down on the sessions list to refresh
  • The question banner appears on all screens — you don’t need to be on the sessions tab to respond
  • Sound notifications can be toggled in the mobile Settings tab

Troubleshooting

ProblemFix
Can’t connect from another deviceCheck that both devices are on the same network. Try pinging the host IP.
Connection refusedVerify the port isn’t blocked by a firewall. The settings panel includes a reachability check.
Authentication failsRe-enter the password in settings — the stored bcrypt hash may be from a different password.
Terminals not respondingWebSocket connection may have dropped. Refresh the browser page.

Architecture Overview

Tech Stack

LayerTechnologyPurpose
FrontendSolidJS + TypeScriptReactive UI with fine-grained updates
BuildVite + LightningCSSFast dev server, optimized CSS
BackendTauri (Rust)Native APIs, PTY, git, system integration
Terminalxterm.js + WebGLGPU-accelerated terminal rendering
StateSolidJS reactive storesFrontend state management
PersistenceJSON files via RustPlatform-specific config directory
TestingVitest + SolidJS Testing LibraryUnit/integration tests (~830 tests)

Hexagonal Architecture

The project follows hexagonal architecture with clear separation between layers:

┌──────────────────────────────────────────────┐
│                  UI Layer                     │
│  SolidJS Components (render + user input)     │
│  ┌──────┐ ┌───────┐ ┌─────────┐ ┌────────┐  │
│  │Sidebar│ │TabBar │ │Terminal │ │Settings│  │
│  └──┬───┘ └──┬────┘ └──┬──────┘ └──┬─────┘  │
├─────┼────────┼─────────┼───────────┼─────────┤
│     │    Application Layer (Hooks)  │         │
│  ┌──┴────────┴─────────┴───────────┴──┐      │
│  │ useGitOps · usePty · useTerminals  │      │
│  │ useGitHub · useDictation · etc.    │      │
│  └──┬────────┬─────────┬─────────────┘      │
├─────┼────────┼─────────┼─────────────────────┤
│     │   State Layer (Stores)   │              │
│  ┌──┴────────┴─────────┴──────┐              │
│  │ terminals · repositories   │              │
│  │ settings · github · ui     │              │
│  └──┬─────────────────────────┘              │
├─────┼────────────────────────────────────────┤
│     │     IPC / Transport Layer              │
│  ┌──┴─────────────────────────────────┐      │
│  │ invoke.ts / transport.ts           │      │
│  │ Tauri IPC (native) | HTTP (browser)│      │
│  └──┬─────────────────────────────────┘      │
├─────┼────────────────────────────────────────┤
│     │     Backend (Rust/Tauri)               │
│  ┌──┴─────────────────────────────────┐      │
│  │ pty · git · github · config        │      │
│  │ agent · worktree · dictation       │      │
│  │ output_parser · error_classification│      │
│  └────────────────────────────────────┘      │
└──────────────────────────────────────────────┘

Design Principles

  • Logic in Rust: All business logic, data transformation, and parsing implemented in the Rust backend. The frontend handles rendering and user interaction only.
  • Cross-Platform: Targets macOS, Windows, and Linux. Uses Tauri cross-platform primitives.
  • KISS/YAGNI: Minimal complexity, no premature abstractions.
  • Dual Transport: Same app works as native Tauri desktop app or browser app via HTTP/WebSocket.

Directory Structure

src/
├── components/           # SolidJS UI components
│   ├── Terminal/         # xterm.js wrapper with PTY integration
│   ├── Sidebar/          # Repository tree, branch list, CI rings
│   ├── TabBar/           # Terminal tabs with drag-to-reorder
│   ├── Toolbar/          # Window drag region, repo/branch display
│   ├── StatusBar/        # Status messages, zoom, dictation
│   ├── SettingsPanel/    # Tabbed settings (General, Agents, Services, etc.)
│   ├── GitPanel/         # Git panel (Changes, Log, Stashes)
│   ├── MarkdownPanel/    # Markdown file browser and renderer
│   ├── HelpPanel/        # Keyboard shortcuts documentation
│   ├── TaskQueuePanel/   # Agent task queue visualization
│   ├── PromptOverlay/    # Agent prompt interception UI
│   ├── PromptDrawer/     # Prompt library management
│   └── ui/               # Reusable UI primitives (CiRing, DiffViewer, etc.)
├── stores/               # Reactive state management
├── hooks/                # Business logic and side effects
├── utils/                # Pure utility functions
├── types/                # TypeScript type definitions
├── transport.ts          # IPC abstraction (Tauri vs HTTP)
└── invoke.ts             # Smart invoke wrapper

src-tauri/src/
├── lib.rs                # App setup, plugin init, command registration
├── main.rs               # Entry point
├── pty.rs                # PTY session lifecycle
├── git.rs                # Git operations
├── github.rs             # GitHub API integration
├── config.rs             # Configuration management
├── state.rs              # Global state (sessions, buffers, metrics)
├── agent.rs              # Agent binary detection and spawning
├── worktree.rs           # Git worktree management
├── output_parser.rs      # Terminal output parsing
├── prompt.rs             # Prompt template processing
├── error_classification.rs # Error classification and backoff
├── menu.rs               # Native menu bar
├── mcp_http.rs           # HTTP/WebSocket server
└── dictation/            # Voice dictation (Whisper)
    ├── mod.rs            # State management
    ├── audio.rs          # Audio capture (CPAL)
    ├── commands.rs       # Tauri commands
    ├── model.rs          # Whisper model management
    ├── transcribe.rs     # Whisper transcription
    └── corrections.rs    # Post-processing corrections

Application Startup Flow

  1. Rust (main.rs): Calls tui_commander_lib::run()
  2. Library (lib.rs): Creates AppState, loads config, spawns HTTP server if enabled, builds Tauri app with plugins, registers 73+ commands, sets up native menu
  3. Frontend (index.tsx): Mounts <App /> component
  4. App (App.tsx): Initializes all hooks, calls initApp() which hydrates stores from backend, detects binaries, sets up keyboard shortcuts, starts GitHub polling
  5. Render: Full UI hierarchy with terminals, panels, overlays, and dialogs

Module Dependencies

App.tsx
├── useAppInit       → hydrates all stores from Rust config
├── usePty           → PTY session management via invoke()
├── useGitOperations → branch switching, worktree creation
├── useTerminalLifecycle → tab management, zoom, copy/paste
├── useKeyboardShortcuts → global keyboard handler
├── useGitHub        → GitHub polling (uses githubStore)
├── useDictation     → push-to-talk (uses dictationStore)
├── useQuickSwitcher → branch quick-switch UI
└── useSplitPanes    → split terminal panes

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

State Management

Overview

State is split between the Rust backend (source of truth for persistence) and SolidJS frontend stores (reactive UI state).

Backend State (src-tauri/src/state.rs)

AppState

The central backend state, shared across all Tauri commands via State<'_, Arc<AppState>>:

#![allow(unused)]
fn main() {
pub struct AppState {
    pub sessions: DashMap<String, Mutex<PtySession>>,       // Active PTY sessions
    pub worktrees_dir: PathBuf,                              // Worktree storage path
    pub metrics: SessionMetrics,                             // Atomic counters
    pub output_buffers: DashMap<String, Mutex<OutputRingBuffer>>, // MCP output access
    pub mcp_sse_sessions: DashMap<String, UnboundedSender<String>>, // SSE clients
    pub ws_clients: DashMap<String, Vec<UnboundedSender<String>>>,  // WebSocket clients
}
}

Concurrency model:

  • DashMap for lock-free concurrent read/write of session maps
  • Mutex for interior mutability of individual PTY writers and buffers
  • Arc<AtomicBool> for pause/resume signaling per session
  • AtomicUsize for zero-overhead metrics counters

PtySession

#![allow(unused)]
fn main() {
pub struct PtySession {
    pub writer: Box<dyn Write + Send>,          // Write to PTY
    pub master: Box<dyn MasterPty + Send>,      // PTY master handle
    pub(crate) _child: Box<dyn Child + Send>,   // Child process
    pub(crate) paused: Arc<AtomicBool>,         // Pause flag
    pub worktree: Option<WorktreeInfo>,         // Associated worktree
    pub cwd: Option<String>,                    // Working directory
}
}

SessionMetrics

Zero-overhead atomic counters:

#![allow(unused)]
fn main() {
pub(crate) struct SessionMetrics {
    pub(crate) total_spawned: AtomicUsize,
    pub(crate) failed_spawns: AtomicUsize,
    pub(crate) active_sessions: AtomicUsize,
    pub(crate) bytes_emitted: AtomicUsize,
    pub(crate) pauses_triggered: AtomicUsize,
}
}

Buffer Types

BufferPurposeCapacity
Utf8ReadBufferAccumulates bytes until valid UTF-8 boundaryVariable
EscapeAwareBufferHolds incomplete ANSI escape sequencesVariable
OutputRingBufferCircular buffer for MCP output access64 KB

Constants

#![allow(unused)]
fn main() {
pub(crate) const MAX_CONCURRENT_SESSIONS: usize = 50;
pub(crate) const OUTPUT_RING_BUFFER_CAPACITY: usize = 64 * 1024;
}

Frontend Stores

Store Pattern

All stores follow a consistent pattern:

// Internal reactive state
const [state, setState] = createStore<Type>(defaults);

// Exported as a module object
export const myStore = {
  get state() { return state; },  // Read-only access
  hydrate() { ... },              // Load from Rust
  action() { ... },               // Mutate + persist
};

Store Registry

StoreFilePurposePersisted
terminalsStoreterminals.tsTerminal instances, active tab, split layoutPartial (IDs in repos)
repositoriesStorerepositories.tsSaved repos, branches, terminal associations, repo groupsrepositories.json
settingsStoresettings.tsApp settings (font, shell, IDE, theme, update channel)config.json
repoSettingsStorerepoSettings.tsPer-repository settings (scripts, worktree)repo-settings.json
repoDefaultsStorerepoDefaults.tsDefault settings for new repositoriesrepo-defaults.json
uiStoreui.tsPanel visibility, sidebar widthui-prefs.json
githubStoregithub.tsPR/CI data per branch, remote tracking (ahead/behind), PR state transitionsNot persisted
promptLibraryStorepromptLibrary.tsPrompt templatesprompt-library.json
notificationsStorenotifications.tsNotification preferencesnotification-config.json
dictationStoredictation.tsDictation config and statedictation-config.json
errorHandlingStoreerrorHandling.tsError retry configui-prefs.json
rateLimitStoreratelimit.tsActive rate limitsNot persisted
tasksStoretasks.tsAgent task queueNot persisted
promptStoreprompt.tsActive prompt overlay stateNot persisted
diffTabsStorediffTabs.tsOpen diff tabsNot persisted
mdTabsStoremdTabs.tsOpen markdown tabs and plugin panelsNot persisted
notesStorenotes.tsIdeas/notes with repo tagging and used-at trackingnotes.json
statusBarTickerstatusBarTicker.tsPriority-based rotating status bar messagesNot persisted
userActivityStoreuserActivity.tsTracks last user click/keydown for activity-based timeoutsNot persisted
updaterStoreupdater.tsApp update state (check, download, install)Not persisted
keybindingsStorekeybindings.tsCustom keyboard shortcut bindingskeybindings.json
agentConfigsStoreagentConfigs.tsPer-agent run configs and togglesagents.json

Key Store Relationships

repositoriesStore
    │
    ├── BranchState.terminals: string[]  ──references──> terminalsStore IDs
    ├── BranchState.worktreePath         ──managed by──> worktree.rs
    └── BranchState.additions/deletions  ──from──> git.rs (get_diff_stats)

terminalsStore
    │
    ├── TerminalData.sessionId           ──maps to──> AppState.sessions key
    ├── TerminalData.agentType           ──read by──> StatusBar (agent badge)
    ├── TerminalData.usageLimit          ──read by──> StatusBar (usage display)
    └── TabLayout.panes                  ──indexes into──> TerminalData[]

githubStore
    │
    ├── Per-branch PR status             ──from──> github.rs (get_repo_pr_statuses)
    ├── Per-repo remote status           ──from──> github.rs (get_github_status)
    ├── CheckSummary                     ──drives──> CiRing component
    └── PR state transitions             ──emits to──> prNotificationsStore

statusBarTicker
    │
    ├── TickerMessage[]                  ──rendered by──> StatusBar
    ├── Claude Usage messages            ──from──> features/claudeUsage.ts (native)
    └── Plugin messages                  ──from──> pluginRegistry (ui:ticker capability)

notesStore
    │
    ├── Note.repoPath                    ──filters by──> active repo
    ├── Note.usedAt                      ──marks when──> sent to terminal
    └── filteredCount()                  ──drives──> StatusBar badge

settingsStore
    │
    ├── font/theme                       ──configures──> Terminal component
    ├── shell                            ──passed to──> create_pty
    ├── ide                              ──used by──> open_in_app
    └── updateChannel                    ──used by──> updaterStore

Debug Registry (MCP Introspection)

Stores self-register snapshot functions via src/stores/debugRegistry.ts. Snapshots are exposed on window.__TUIC__ and accessible through MCP debug(action=invoke_js).

APIReturns
__TUIC__.stores()List of registered store names
__TUIC__.store(name)Snapshot of the named store

Registered: github, globalWorkspace, keybindings, notes, paneLayout, repositories, settings, tasks, ui.

To register a new store, append at the bottom of the store file:

import { registerDebugSnapshot } from "./debugRegistry";
registerDebugSnapshot("name", () => ({ /* safe subset */ }));

Configuration Files

All config files are JSON, stored in the platform config directory:

PlatformPath
macOS~/Library/Application Support/tuicommander/
Linux~/.config/tuicommander/
Windows%APPDATA%/tuicommander/

Legacy path ~/.tuicommander/ is auto-migrated on first launch.

Config File Map

FileContentsRust Type
config.jsonShell, font, theme, MCP, remote access, update channelAppConfig
notification-config.jsonSound preferences, volumeNotificationConfig
ui-prefs.jsonSidebar, error handling settingsUIPrefsConfig
repo-settings.jsonPer-repo scripts, worktree optionsRepoSettingsMap
repo-defaults.jsonDefault settings for new repos (base branch, scripts)RepoDefaultsConfig
repositories.jsonSaved repos, branches, groupsserde_json::Value
prompt-library.jsonPrompt templatesPromptLibraryConfig
dictation-config.jsonDictation on/off, hotkey, language, modelDictationConfig
notes.jsonIdeas/notes with repo tags and used-at timestampsserde_json::Value
keybindings.jsonCustom keyboard shortcut overridesserde_json::Value
agents.jsonPer-agent run configs and togglesAgentsConfig
claude-usage-cache.jsonIncremental JSONL parse offsets for session statsSessionStatsCache

Terminal State Machine

Definitive reference for terminal activity states, notifications, and question detection.

State Variables

Each terminal has these reactive fields in terminalsStore:

FieldTypeDefaultSource of truth
shellState"busy" | "idle" | nullnullRust (emitted as parsed event)
awaitingInput"question" | "error" | nullnullFrontend (from parsed events)
awaitingInputConfidentbooleanfalseFrontend (from Question event)
activeSubTasksnumber0Rust (parsed + stored per session)
debouncedBusybooleanfalseFrontend (derived from shellState with 2s hold)
unseenbooleanfalseFrontend (set by fireCompletion, cleared on tab focus)
agentTypeAgentType | nullnullFrontend (from agent detection)

Rust-side per-session state:

FieldLocationPurpose
SilenceState.last_output_atpty.rsTimestamp of last real output (not mode-line ticks)
SilenceState.last_chunk_atpty.rsTimestamp of last chunk of any kind (real or chrome-only). Used by backup idle timer to detect reader thread activity.
SilenceState.last_status_line_atpty.rsTimestamp of last spinner/status-line
SilenceState.pending_question_linepty.rsCandidate ?-ending line for silence detection
SilenceState.output_chunks_after_questionpty.rsStaleness counter: real-output chunks since last ? candidate
SilenceState.question_already_emittedpty.rsPrevents re-emission of the same question
SilenceState.suppress_echo_untilpty.rsDeadline to ignore PTY echo of user-typed ? lines
active_sub_tasksAppState.session_statesSub-agent count per session
shell_statesAppState.shell_statesDashMap<String, AtomicU8>: 0=null, 1=busy, 2=idle. Transitions use compare_exchange to prevent duplicate events when reader thread and silence timer race.
last_output_msAppState.last_output_msEpoch ms of last real output (not chrome-only). Stamped only when !chrome_only.

1. Tab Indicator — Visual Priority

The tab dot reflects the terminal’s highest-priority active state:

Priority    State       Color       CSS var         Condition
────────    ─────       ─────       ───────         ─────────
   1        Error       red         --error         awaitingInput == "error"
   2        Question    orange      --attention     awaitingInput == "question"
   3        Busy        blue ●̣      --activity      debouncedBusy && !awaitingInput
   4        Unseen      purple      --unseen        unseen && !debouncedBusy && !awaitingInput
   5        Done        green       --success       shellState=="idle" && !unseen && !debouncedBusy && !awaitingInput
   6        Idle        gray        (default)       shellState==null or none of above

Error and Question have pulse animation. Busy has pulse animation. Unseen and Done are static.

Complete state combination matrix

Every valid combination of the 4 key fields and the resulting indicator:

awaitingInput  debouncedBusy  unseen  shellState   → Indicator
─────────────  ─────────────  ──────  ──────────   ──────────
"error"        true           any     any          → Error (red)
"error"        false          any     any          → Error (red)
"question"     true           any     any          → Question (orange)
"question"     false          any     any          → Question (orange)
null           true           any     any          → Busy (blue pulse)
null           false          true    "idle"       → Unseen (purple)
null           false          true    null         → Unseen (purple)
null           false          false   "idle"       → Done (green)
null           false          false   "busy"       → (transient: cooldown pending)
null           false          false   null         → Idle (gray)

Lifecycle of each indicator

                    ┌──────────────────────── Error (red) ◄─── API error / agent crash
                    │                              │
                    │   ┌──────────────────── Question (orange) ◄─── agent asks ?
                    │   │                          │
                    │   │    ┌─────────────── Busy (blue) ◄─── real output detected
                    │   │    │                     │
                    │   │    │    ┌────────── Unseen (purple) ◄─── completion fired,
                    │   │    │    │                │                user not watching
                    │   │    │    │    ┌───── Done (green) ◄─── user viewed unseen tab,
                    │   │    │    │    │            │              or short idle session
                    │   │    │    │    │    ┌─ Idle (gray) ◄─── no session / fresh
                    │   │    │    │    │    │
                    ▼   ▼    ▼    ▼    ▼    ▼
                 [ Higher priority wins when multiple states active ]

2. shellState — Derived in Rust

Rust is the single source of truth. The reader thread classifies every PTY chunk:

PTY chunk arrives in reader thread
         │
         ▼
    ┌─────────────────────────────┐
    │ Compute chrome_only:        │
    │  = no regex question found  │
    │  AND no ?-ending line       │
    │  AND changed_rows non-empty │
    │  AND ALL changed rows pass  │
    │    is_chrome_row() (contain │
    │    ⏵/›/✻/• markers)        │
    └─────────┬───────────────────┘
              │
         chrome_only?
        ╱            ╲
      YES             NO
       │               │
       ▼               ▼
  Mode-line tick    Real output
  (timer, spinner)  (agent working)
       │               │
       │               ├── last_output_at = now
       │               │
       │               └── if shell_state ≠ busy:
       │                      emit ShellState { "busy" }
       │                      shell_state = busy
       │
       └── if shell_state == busy
           AND last_output_at > threshold ago
               (500ms for shell, 5s for agent sessions)
           AND active_sub_tasks == 0
           AND not in resize grace:
              emit ShellState { "idle" }
              shell_state = idle

A backup timer (the existing silence timer, 1s interval) also checks:

Silence timer (every 1s)
         │
         ▼
    reader thread active?  ─── YES ──► skip
    (last_chunk_at < 2s)       (reader handles idle via !has_status_line guard)
         │ NO
         ▼
    shell_state == busy?  ─── NO ──► skip
         │ YES
         ▼
    last_output_at > threshold ago?  ─── NO ──► skip
    (500ms shell / 2.5s agent)
         │ YES
         ▼
    active_sub_tasks == 0?  ─── NO ──► skip
         │ YES
         ▼
    emit ShellState { "idle" }
    shell_state = idle

This catches the case where NO chunks arrive at all (agent truly silent — reader thread blocked on read()). When chrome-only chunks are arriving (mode-line timer ticks), the reader thread is active and handles idle transitions correctly via its own !has_status_line guard.

Session end

When the reader thread loop breaks (Ok(0)), Rust emits ShellState { "idle" } before stopping, ensuring the frontend sees the final transition. The frontend then receives the exit callback and sets sessionId = null.

Frontend consumption

pty-parsed event: ShellState { state }
         │
         ▼
terminalsStore.update(id, { shellState: state })
         │
         ▼
handleShellStateChange(prev, next)  ← existing debounced busy logic

The frontend does NOT derive shellState from raw PTY data. handlePtyData writes to xterm and updates lastDataAt — but never touches shellState.

Transition table

FromToTriggerCondition
nullbusyFirst real output chunk
busyidleChrome-only chunk or silence timerlast_output_at > threshold (500ms shell / 2.5s agent) AND active_sub_tasks == 0 AND not resize grace
idlebusyReal output chunk
busyidleSession ends (reader thread exit)Always (cleanup)
anynullTerminal removed from storecleanup

What does NOT cause transitions

EventWhy it’s ignored
Mode-line timer tick (✻ Cogitated 3m 47s)Classified as chrome_only
Status-line update (▶▶ ... 1 local agent)Classified as chrome_only
ActiveSubtasks eventUpdates counter, doesn’t produce real output
Resize redrawSuppressed by resize grace (1s)

3. debouncedBusy — Derived from shellState

Smoothed version with a 2-second hold to prevent flicker:

shellState events from Rust:

  busy ─────────── idle ──── busy ─────── idle ──────────────────
                     │         │            │
debouncedBusy:       │         │            │
  true ──────────────┼─────────┼── true ────┼── true ──┐ false ──
                     │         │            │          │
                     └── 2s ───┘            └── 2s ───┘
                     cooldown               cooldown
                     cancelled              expires
EventdebouncedBusy effect
shellState → busyImmediately true. Cancel any running cooldown. Record busySince (first time only).
shellState → idleStart 2s cooldown. If cooldown expires: set false, fire onBusyToIdle(id, duration).
shellState → busy during cooldownCancel cooldown. Stay true. Keep original busySince.

onBusyToIdle fires exactly once per busy→idle cycle, after the 2s cooldown fully expires.

4. awaitingInput — Question and Error Detection

State diagram

                          Question event
                          (passes all guards)
    ┌──────┐             ┌──────────┐
    │ null │────────────►│ question │
    └──┬───┘             └─────┬────┘
       │                       │
       │  ◄── clear triggers ──┘
       │      (see table below)
       │
       │  Error event
       │  (API error, agent crash)
       │                 ┌────────┐
       └────────────────►│ error  │
                         └───┬────┘
                             │
                    ◄── clear triggers ──┘
                        (see table below)

Clear triggers

TriggerClears “question”?Clears “error”?Why
StatusLine parsed eventYesYesAgent is working again (showing a task)
Progress parsed eventYesYesAgent is making progress
User keystroke (terminal.onData)YesYesUser typed something — prompt answered
shellState idle → busyYesNoAgent resumed real output (reliable post-refactor since mode-line ticks no longer cause idle→busy)
Process exitYesYesSession over

What does NOT clear awaitingInput

EventWhy it doesn’t clear
shellState idle → busyClears "question" but not "error". API errors are persistent and need explicit agent activity (status-line) or process exit to clear.
Mode-line tickChrome-only output, not agent activity
activeSubTasks changeSub-agent count changing doesn’t mean the main question was answered

Notification sounds

Sounds play on transitions into a state, never on repeated sets or clearing:

getAwaitingInputSound(prev, current):

  prev      current     sound
  ────      ───────     ─────
  null   →  question →  play "question"
  null   →  error    →  play "error"
  *      →  same     →  null (no sound)
  *      →  null     →  null (clearing, no sound)
  question→ error    →  play "error" (state changed)
  error  →  question →  play "question" (state changed)

5. unseen — Completion Visibility Tracking

unseen tracks whether the user has seen a completed task.

Lifecycle

                                          ┌─────────┐
  fireCompletion() ──────────────────────►│ unseen  │
  (background tab, agent done)            │ = true  │
                                          └────┬────┘
                                               │
  User clicks/switches to this tab ───────────►│
  (setActive clears unseen)                    │
                                               ▼
                                          ┌─────────┐
                                          │ unseen  │
                                          │ = false │
                                          └─────────┘

What sets unseen

Only ONE place: App.tsx fireCompletion() sets unseen = true (along with activity = true).

What clears unseen

Only ONE place: terminalsStore.setActive(id) sets unseen = false.

Tab color transitions for unseen

Agent working    Agent done     User switches    User switches
(background)     (background)   to other tab     to THIS tab
    │                │               │                │
    ▼                ▼               ▼                ▼
  Blue ●̣  ───►  Purple ● ────►  Purple ● ────►  Green ●
  (busy)       (unseen)        (stays unseen)   (done/idle)

6. activeSubTasks — Sub-agent Tracking

Parsed from the agent mode line by Rust OutputParser:

Mode line text                               Parsed count
──────────────────────────────────────────   ────────────
"▶▶ bypass permissions on · 1 local agent"  → 1
"▶▶ Reading files · 3 local agents"         → 3
"▶▶ bypass permissions on"                  → 0
(no mode line)                               → unchanged

Stored in both Rust (AppState.active_sub_tasks) and frontend (terminalsStore).

Effects on other states

                    activeSubTasks
                    ┌─────────────────────────────────────────────┐
                    │                                             │
                    ▼                                             ▼
              > 0 (agents running)                    == 0 (no agents)
              ┌────────────────────┐                  ┌──────────────────┐
              │ shellState:        │                  │ shellState:      │
              │   stays busy       │                  │   normal rules   │
              │   (idle blocked)   │                  │   (500ms timer)  │
              │                    │                  │                  │
              │ Question guard:    │                  │ Question guard:  │
              │   low-confidence   │                  │   passes through │
              │   IGNORED          │                  │                  │
              │                    │                  │                  │
              │ Completion:        │                  │ Completion:      │
              │   SUPPRESSED       │                  │   normal rules   │
              └────────────────────┘                  └──────────────────┘

Reset

EventEffect
ActiveSubtasks { count: N } parsed eventSet to N
UserInput parsed eventReset to 0 (new agent cycle)
Process exitReset to 0

7. Completion Notification

Fires when an agent was busy for ≥5s then truly goes idle.

Two independent paths can trigger completion:

Path 1: Session exit (Terminal.tsx)

Process exits → reader thread ends → exit callback fires
         │
         ├── terminal is active tab? → SKIP
         │
         └── play("completion")
             (does NOT set unseen — user may switch soon)

Path 2: Busy-to-idle (App.tsx) — sets unseen

onBusyToIdle(id, durationMs)
         │
         ├── durationMs < 5s? ────────────────────── SKIP
         ├── terminal is active tab? ─────────────── SKIP
         │
         ├── agentType set? ── YES ─► defer 10s ──► fireCompletion()
         │                 NO ──────► fireCompletion()
         │
         ▼
    fireCompletion()
         │
         ├── terminal is active tab? ──── SKIP (user switched to it)
         ├── debouncedBusy still true? ── SKIP (went busy again)
         ├── terminal removed? ────────── SKIP
         ├── activeSubTasks > 0? ──────── SKIP (agents still running)
         ├── awaitingInput set? ────────── SKIP (question/error active)
         │
         ▼
    play("completion")
    set unseen = true
    → tab turns purple (Unseen)
    → when user views: tab turns green (Done)

Sound deduplication

Both paths can fire for the same session. Path 1 fires immediately on exit. Path 2 fires after cooldown + deferral. The notification manager handles dedup (cooldown between identical sounds).

Timing under the new architecture

t=0       Agent starts working (real output) → shellState: busy
t=0..T    Agent works. Mode-line ticks arrive but don't affect shellState.
t=T       Agent stops real output. Mode-line may continue.
t=T+0.5   Shell session: Rust idle threshold (500ms) reached → shellState: idle
          Agent session: still within 2.5s threshold → stays busy
           (If sub_tasks > 0: stays busy regardless of threshold)
t=T+2.5   Shell: Cooldown expires → debouncedBusy: false → onBusyToIdle fires
          Agent: Rust idle threshold (2.5s) reached → shellState: idle
t=T+4.5   Agent: Cooldown expires → debouncedBusy: false → onBusyToIdle fires
t=*+0     duration = T seconds. If T ≥ 5s and agentType:
             → defer 10s → fireCompletion
t=*+10    fireCompletion checks all guards → play("completion"), unseen=true

8. Question Detection Pipeline

Two layers: Rust detectionFrontend notification.

Rust: Two parallel detection strategies

┌─────────────────────────────────────────────────────────────────┐
│                    READER THREAD (per chunk)                     │
│                                                                 │
│  PTY data → parse_clean_lines(changed_rows) → events[]          │
│                                                                 │
│  ┌─ Strategy A: Regex (instant) ──────────────────────────────┐ │
│  │ parse_question() matches "Enter to select"                 │ │
│  │ → Question { confident: true }                             │ │
│  │ → emitted immediately in the events list                   │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                 │
│  ┌─ Strategy B: Silence (delayed) ────────────────────────────┐ │
│  │ extract_question_line(changed_rows)                        │ │
│  │ → finds last line ending with '?' that passes              │ │
│  │   is_plausible_question() filter                           │ │
│  │ → stored as pending_question_line in SilenceState          │ │
│  │ → NOT emitted yet — waits for silence timer                │ │
│  └────────────────────────────────────────────────────────────┘ │
│                                                                 │
│  on_chunk() updates SilenceState:                               │
│    - regex fired? → clear pending, mark emitted                 │
│    - echo suppress window? → ignore '?' line                    │
│    - same line already emitted? → ignore (repaint)              │
│    - new '?' line? → set as pending candidate                   │
│    - real output after '?'? → increment staleness               │
│    - mode-line tick? → do nothing                               │
│    - staleness > 10? → clear pending (agent kept working)       │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    SILENCE TIMER (every 1s)                      │
│                                                                 │
│  is_silent()?                                                   │
│    ├── question_already_emitted? → skip                         │
│    ├── is_spinner_active()? → skip (status-line < 10s ago)      │
│    └── last_output_at < 10s? → skip                             │
│                                                                 │
│  If silent (all three pass):                                    │
│                                                                 │
│  ┌─ Strategy 1: Screen-based ────────────────────────────────┐  │
│  │ Read VT screen → extract_last_chat_line()                 │  │
│  │ → find line above prompt (❯, ›, >)                        │  │
│  │ → ends with '?' AND is_plausible_question()?              │  │
│  │ → emit Question { confident: false }                      │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ┌─ Strategy 2: Chunk-based fallback ────────────────────────┐  │
│  │ check_silence() → pending_question_line exists?           │  │
│  │ AND not stale (≤ 10 real-output chunks after)?            │  │
│  │ → verify_question_on_screen() (bottom 5 rows)            │  │
│  │ → emit Question { confident: false }                      │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  If neither strategy finds a question: continue sleeping.       │
└─────────────────────────────────────────────────────────────────┘

SilenceState update rules

Chunk typelast_chunk_atlast_output_atlast_status_line_atstaleness counterpending_question_line
Real output, no ‘?’Reset to nowReset to now+1 (if pending exists)Cleared if >10
Real output with ‘?’Reset to nowReset to nowReset to 0Set to new line
Real output + status-lineReset to nowReset to nowReset to now(per above rules)(per above rules)
Mode-line tick onlyReset to nowNot resetNot resetNot incrementedNot affected
Regex question firedReset to nowReset to nowReset to 0Cleared (handled)

Frontend: event handler + notification

pty-parsed: Question { prompt_text, confident }
         │
         ▼
    ┌──────────────────────────────────────────────────┐
    │ Guard: low-confidence question while agent busy  │
    │                                                  │
    │ NOT confident                                    │
    │ AND (shellState == "busy"                        │
    │      OR activeSubTasks > 0)?                     │
    │                                                  │
    │ YES → IGNORE (likely false positive)             │
    │ NO  → continue                                   │
    └──────────────────┬───────────────────────────────┘
                       │
                       ▼
    setAwaitingInput(id, "question", confident)
                       │
                       ▼
    createEffect detects transition (null → "question")
                       │
                       ▼
    play("question")

9. Timing Constants

ConstantValueLocationPurpose
Shell idle threshold500mspty.rs (Rust)Real output silence before idle (plain shell)
Agent idle threshold2.5spty.rs (Rust)Real output silence before idle (agent sessions)
Debounce hold2sterminals.tsdebouncedBusy hold after idle
Silence question threshold10spty.rsSilence before ‘?’ line → question
Silence check interval1spty.rsTimer thread wake frequency
Backup idle chunk threshold2spty.rsSkip backup idle if any chunk arrived within this window
Stale question chunks10pty.rsReal-output chunks before discarding ‘?’ candidate
Resize grace1spty.rsSuppress all events after resize
Echo suppress window500mspty.rsIgnore PTY echo of user-typed ‘?’ lines
Screen verify rows5pty.rsBottom N rows checked for screen verification
Completion threshold5sApp.tsxMinimum busy duration for completion notification
Completion deferral10sApp.tsxExtra wait for agent processes (sub-agents may still run)

10. Scenarios

A: Agent asks “Procedo?” — no sub-agents

t=0       Agent outputs "Procedo?" (real output)
          → shellState: busy
          → pending_question_line = "Procedo?"
          → last_output_at = now

t=2.5     No more real output. Rust idle check:
          last_output_at > 2.5s (agent threshold), active_sub_tasks=0
          → shellState: idle  │  Tab: blue→(cooldown)
          → debouncedBusy cooldown starts (2s)

t=1-9     Mode-line ticks arrive (chrome_only=true)
          → shellState stays busy (agent threshold not reached)
          → pending_question_line preserved

t=4.5     Cooldown expires → debouncedBusy: false
          → onBusyToIdle fires (duration ~2.5s < 5s → no completion)
          │  Tab: blue→green (Done)

t=10      Silence timer: is_silent()? YES
          → Strategy 1 or 2 finds "Procedo?"
          → emit Question { confident: false }
          → Frontend: guard passes (idle, subTasks=0)
          → awaitingInput = "question"
          → play("question") ✓
          │  Tab: green→orange (Question)

t=???     User types response → UserInput event
          → clearAwaitingInput
          │  Tab: orange→green (Done)
          → agent resumes → status-line → clearAwaitingInput (redundant, safe)
          │  Tab: green→blue (Busy)

B: Agent asks “Procedo?” — sub-agents running

t=0       Agent outputs "Procedo?" while 2 sub-agents run
          → shellState: busy  │  Tab: blue
          → pending_question_line = "Procedo?"
          → active_sub_tasks = 2

t=0.5+    No more real output but active_sub_tasks > 0
          → shellState stays busy (Rust: idle blocked)

t=10      Silence timer: is_silent()? YES
          → emit Question { confident: false }
          → Frontend: activeSubTasks=2 > 0, NOT confident → IGNORED

t=60      Last sub-agent finishes → ActiveSubtasks { count: 0 }

t=62.5    Rust: last_output_at > 2.5s (agent threshold), sub_tasks=0
          → shellState: idle  │  Tab: blue→(cooldown)

t=64.5    Cooldown expires → onBusyToIdle(duration=60s)
          → ≥ 5s, agentType set → defer 10s

t=74.5    fireCompletion()
          → activeSubTasks=0, awaitingInput=null
          → play("completion") ✓, unseen=true
          │  Tab: purple (Unseen)

          User switches to tab → unseen cleared
          │  Tab: purple→green (Done)

C: Ink menu — “Enter to select”

t=0       Agent renders Ink menu with "Enter to select" footer
          → parse_question() regex match (INK_FOOTER_RE)
          → emit Question { confident: true } immediately
          → SilenceState: pending cleared, question_already_emitted = true

t=0       Frontend: confident=true → guard skipped (always passes)
          → awaitingInput = "question"
          → play("question") ✓
          │  Tab: orange (Question)

          No 10s wait needed — instant detection.

D: False positive — agent discusses code with ‘?’

t=0       Agent outputs "// Should we use HashMap?"
          → is_plausible_question → false (starts with //)
          → NO candidate set

t=0       Agent outputs "Does this look right?"
          → is_plausible_question → true
          → pending_question_line = "Does this look right?"

t=0.1+    Agent continues with more real output (non-'?')
          → staleness +1, +2, ... +11 (> STALE_QUESTION_CHUNKS=10)
          → pending_question_line cleared

t=10+     Silence timer: pending is None → nothing emitted ✓
          │  No false notification

E: Agent completes long task — no question

t=0       Agent starts working (real output)
          → shellState: busy  │  Tab: blue

t=120     Agent finishes, goes to prompt. No more real output.

t=122.5   Rust: last_output_at > 2.5s (agent threshold), sub_tasks=0
          → shellState: idle  │  Tab: blue→(cooldown)

t=124.5   Cooldown expires → onBusyToIdle(duration=120s)
          → ≥ 5s, agentType set → defer 10s

t=134.5   fireCompletion()
          → all guards pass
          → play("completion") ✓, unseen=true
          │  Tab: purple (Unseen)

          User switches to tab → unseen cleared
          │  Tab: purple→green (Done)

F: User watches terminal — active tab

t=0       Agent working in active tab (user watching)
          → shellState: busy  │  Tab: blue

t=60      Agent finishes → idle

t=62      onBusyToIdle fires
          → terminal IS active tab → SKIP
          → no sound, no unseen
          │  Tab: blue→green (Done) — user was watching

G: Short command — under 5s

t=0       User runs `ls` → shellState: busy  │  Tab: blue

t=0.1     Output finishes → idle

t=2.1     Cooldown expires → onBusyToIdle(duration=0.1s)
          → duration < 5s → SKIP
          → no sound, no unseen
          │  Tab: blue→green (Done)

H: Process exits in background tab

t=0       Agent working in background tab
          → shellState: busy  │  Tab: blue

t=60      Process exits → reader thread ends
          → Rust emits ShellState { "idle" }
          → Frontend exit callback:
            sessionId = null, clearAwaitingInput
            play("completion") [Path 1] ✓

t=62      Cooldown expires → onBusyToIdle(duration=60s)
          → fireCompletion [Path 2] → play("completion"), unseen=true
          │  Tab: purple (Unseen)

          User switches to tab → unseen cleared
          │  Tab: purple→green (Done)

I: Rate-limit detected

t=0       Agent output matches rate-limit pattern
          → RateLimit parsed event emitted

t=0       Frontend handler:
          shellState == "busy"?
            YES → IGNORE (false positive from streaming code)
            NO  → agentType set, not recently detected?
              YES → play("warning") ✓, rateLimitStore updated
              NO  → SKIP (dedup)

J: Resize during question display

t=0       "Procedo?" visible on screen, awaitingInput = "question"
          │  Tab: orange (Question)

t=X       User resizes terminal pane
          → resize_pty called → SilenceState.on_resize()
          → Shell redraws visible output (real PTY output)
          → Rust: shellState → busy (real output)

t=X       Resize grace active (1s):
          → All notification events SUPPRESSED (Question, RateLimit, ApiError)
          → "Procedo?" in redraw doesn't re-trigger question

t=X+1     Grace expires. awaitingInput still "question" (never cleared).
          │  Tab stays orange ✓

K: Agent error (API error, stuck)

t=0       Agent output matches API error pattern
          → ApiError parsed event emitted

t=0       Frontend handler:
          → awaitingInput = "error"
          → play("error") ✓
          │  Tab: red (Error)

          User answers / agent retries → StatusLine event
          → clearAwaitingInput
          │  Tab: red→blue (Busy)

L: Question then error (priority override)

t=0       Agent asks question → awaitingInput = "question"
          │  Tab: orange (Question)

t=5       API error while question is pending
          → awaitingInput = "error" (overrides question)
          → play("error") ✓
          │  Tab: orange→red (Error)

          Agent recovers → StatusLine event
          → clearAwaitingInput
          │  Tab: red→blue (Busy)

11. File Reference

FileResponsibility
src-tauri/src/pty.rsSilenceState, spawn_silence_timer, shellState derivation, extract_question_line, verify_question_on_screen, extract_last_chat_line, spawn_reader_thread
src-tauri/src/output_parser.rsparse_question (INK_FOOTER_RE), parse_active_subtasks, ParsedEvent enum
src-tauri/src/state.rsAppState (includes shell_state, active_sub_tasks maps)
src/stores/terminals.tsshellState, awaitingInput, debouncedBusy, handleShellStateChange, onBusyToIdle
src/components/Terminal/Terminal.tsxhandlePtyData (xterm write), pty-parsed event handler, notification effect
src/components/Terminal/awaitingInputSound.tsgetAwaitingInputSound edge detection
src/App.tsxonBusyToIdle → completion notification with deferral + guards
src/stores/notifications.tsplay(), playQuestion(), playCompletion() etc.
src/components/TabBar/TabBar.tsxTab indicator class priority logic
src/components/TabBar/TabBar.module.cssIndicator colors and animations

Agent UI Analysis — General Reference

Cross-agent reference for parsing AI agent terminal UIs in TUICommander. Agent-specific layouts are documented in agents/<name>.md.

Scope

TUICommander supports multiple AI coding agents. Each has a unique terminal UI with different rendering approaches, chrome patterns, and interaction models. This document covers:

  1. Shared concepts and detection strategies
  2. Code architecture and known gaps
  3. Research methodology for ongoing verification

Agent-specific documents:

  • Claude Code — Ink-based, ANSI cursor positioning
  • Codex CLI — Ink-based, absolute positioning + scroll regions
  • Gemini CLI — Ink-like, relative positioning + prompt box
  • Aider — Sequential CLI, no TUI framework
  • OpenCode — Bubble Tea full-screen TUI

Detection Strategy Per Agent

Each agent class requires a different parsing strategy:

AgentUI TypeParsing Strategychrome.rs applies?
Claude CodeCLI inline (Ink)Changed-rows delta analysisYes
Codex CLICLI inline (Ink)Changed-rows delta analysisYes
OpenCodeFull-screen TUI (Bubble Tea)Screen snapshot analysisNo (all rows are “chrome”)
Gemini CLICLI inlineChanged-rows delta analysisYes
AiderCLI sequentialChanged-rows delta analysisYes

CLI inline agents (CC, Codex, Gemini, Aider) render output into the terminal sequentially, with chrome at specific positions. chrome.rs functions work for these — is_separator_line, is_prompt_line, is_chrome_row classify individual rows.

Full-screen TUI agents (OpenCode) take over the entire screen. Every row changes on every update, making delta analysis useless. These need screen-snapshot-based parsing: identify panels by position, extract text from known regions, detect state changes by content comparison.


Shared Concepts

Chrome Detection

“Chrome” = UI decoration rows that are NOT real agent output (separators, mode lines, status bars, spinners, menus). Correctly classifying chrome is critical for:

  • Silence-based question detection: chrome-only chunks should not reset the silence timer or invalidate pending questions
  • Shell state transitions: chrome-only output should not prevent BUSY → IDLE transitions
  • Log trimming: chrome should be stripped from mobile logs and REST API responses

Prompt Line

The row where the user types input. Each agent uses a different character:

AgentPrompt charUnicode
Claude CodeU+276F
Codex CLIU+203A
Gemini CLI>ASCII

Separator Lines

Horizontal rules that delineate sections. Detected by a run of 4+ box-drawing characters (─ ━ ═ — ╌ ╍). Not all agents use separators.

AgentUses separatorsStyle
Claude CodeYes──── around prompt box
Codex CLIPartially──── between tool output and summary only
Gemini CLINo
AiderNo

Interactive Menu Detection

All observed agent menus share the pattern Esc to in their footer:

Footer variantAgent / Context
Esc to cancel · Tab to amendCC permission prompt
Enter to select · Tab/Arrow keys to navigate · Esc to cancelCC custom Ink menu
↑↓ to navigate · Enter to confirm · Esc to cancelCC built-in (/mcp)
Esc to cancel · r to cycle dates · ctrl+s to copyCC built-in (/stats)
←/→ tab to switch · ↓ to return · Esc to closeCC built-in (/status)
Enter to select · ↑/↓ to navigate · Esc to cancelCC Ink select
esc again to edit previous messageCodex (after interrupt)

Esc to is the most reliable cross-agent signal for “interactive menu active.”

OSC Sequences

Terminal escape sequences that carry structured metadata:

SequencePurposeAgent
\033]777;notify;Claude Code;...\007User attention notificationCC
\033]0;...\007Window title (task name + spinner)CC, Codex
\033]8;;url\007HyperlinkCC
\033]9;4;N;\007Progress notificationCC
\033]10;?\033\\Query foreground colorCodex
\033]11;?\033\\Query background colorCodex

Code Architecture

Unified chrome.rs module

All chrome detection is centralized in src-tauri/src/chrome.rs. The three pipelines (pty.rs, session.rs, state.rs) all import from this single module:

src-tauri/src/chrome.rs
├── is_separator_line()    — run-of-4 box-drawing chars (─ ━ ═ — ╌ ╍)
├── is_prompt_line()       — all agent prompt chars: ❯ › >
├── is_chrome_row()        — 10 marker chars + dingbat range + Codex • disambiguation
├── CHROME_SCAN_ROWS       — single constant (15)
└── find_chrome_cutoff()   — unified trim logic for REST and mobile pipelines
PipelineFileWhat it uses from chrome.rs
Changed-rows parserpty.rsis_chrome_row (for chrome_only), is_separator_line, is_prompt_line
Screen trim (REST)session.rsfind_chrome_cutoff (replaces local trim_screen_chrome body)
Log trim (mobile)state.rsfind_chrome_cutoff (replaces local find_prompt_cutoff body)

Parsing Functions

Chrome Detection (is_chrome_row) — chrome.rs

Classifies changed terminal rows as “UI decoration” vs “real agent output”.

Detected markers:

  • (U+23F5) — CC mode-line prefix
  • (U+23F8) — CC plan mode prefix
  • (U+203A) — CC/Codex mode-line prefix
  • · (U+00B7) — CC middle-dot spinner prefix
  • (U+2580) — Gemini prompt box top border
  • (U+2584) — Gemini prompt box bottom border
  • (U+2591) — Aider Knight Rider spinner
  • (U+2588) — Aider Knight Rider spinner / CC context bar
  • (U+25A0) — Codex interrupt marker
  • (U+2022) — Codex spinner (disambiguated: • Working = chrome, • Created = output)
  • U+2720–U+273F — CC spinner dingbats (✶✻✳✢ etc.)

Used by chrome_only calculation (pty.rs) which also considers has_status_line from parse_status_line events (for Gemini braille/Aider spinners). Gates:

  • last_output_ms timestamp updates
  • SHELL_BUSYSHELL_IDLE transitions
  • SilenceState::on_chunk()

Gaps:

  • Missing (U+23F8) — plan mode chunks not classified as chrome
  • 1 shell (no ⏵⏵) not detected — new CC format without mode-line markers
  • No positional awareness — cannot use “last row = always chrome” heuristic
  • CC status lines (below separator) transit PTY but have no chrome markers

Subprocess Count (parse_active_subtasks) — output_parser.rs:706

Extracts subprocess count from the mode line. Must handle:

  1. Old format: ⏵⏵ <mode> · N <type> — markers first, count last
  2. New format: N <type> · ⏵⏵ <mode> — count first, markers last
  3. Count only: N <type> — no markers at all (e.g., 1 shell)
  4. Bare mode: ⏵⏵ <mode> — markers only, no count (count = 0)

Gap: Only format 1 and 4 are currently implemented.

Question Detection (extract_last_chat_line) — pty.rs:207

Finds the last agent chat line above the prompt box:

  1. Scan from bottom, find prompt line (, , >)
  2. Walk up past separators and empty lines
  3. First non-empty, non-separator line = last chat line

Robust: Does not depend on mode line format.


Test Expectations

Tests that hardcode specific bottom-zone layouts. Update these when adding new format support.

output_parser.rs — parse_active_subtasks tests

All use format: ⏵⏵|›› <mode> · N <type> (old format only)

  • test_active_subtasks_local_agents›› bypass permissions on · 2 local agents
  • test_active_subtasks_single_bash›› reading config files · 1 bash
  • test_active_subtasks_background_tasks›› fixing tests · 3 background tasks
  • test_active_subtasks_single_local_agent›› writing code · 1 local agent
  • test_active_subtasks_bare_mode_line_resets_to_zero›› bypass permissions on
  • test_active_subtasks_explicit_zero_count›› finishing · 0 bash
  • test_active_subtasks_triangle_* — same patterns with ⏵⏵ prefix

pty.rs — extract_last_chat_line tests

  • test_extract_last_chat_line_standard_claude_code⏵⏵ bypass permissions on (shift+tab to cycle)
  • test_extract_last_chat_line_with_wiz_hud — 3 HUD lines + mode line
  • test_extract_last_chat_line_plan_mode⏸ plan mode on (shift+tab to cycle)

pty.rs — is_chrome_row / chrome_only tests

  • test_chrome_only_single_statusline_row_is_chrome⏵⏵ auto mode
  • test_chrome_only_wrapped_statusline_is_chrome⏵⏵ bypass permissions on + ✻ timer
  • test_chrome_only_subtasks_row_is_chrome›› bypass permissions on · 1 local agent

Verification Methodology

These documents must be re-verified weekly (or after agent version updates) to catch layout changes before they break parsing.

Procedure 1: Capture current layout from live sessions

Use session action=list to find active sessions, then for each:

session action=output session_id=<id> limit=4000               → clean text
session action=output session_id=<id> limit=8000 format=raw     → raw ANSI

Compare against documented layouts. Look for:

  • Changed cursor-up distances (\033[NA) → bottom zone height changed
  • New Unicode characters in mode/status lines → update is_chrome_row
  • Changed OSC sequences → update notification detection
  • New footer text → update question detection

Procedure 2: Trigger interactive menus

  1. Create a fresh session: session action=create
  2. Start agent in restricted mode (CC: --permission-mode default, Codex: -a untrusted)
  3. Request operations that trigger approval prompts
  4. Capture raw output — compare separators, colors, footers
  5. Cancel and clean up

Procedure 3: Audit parser compatibility

cd src-tauri && cargo test -- --test-threads=1 \
  chrome_only \
  active_subtasks \
  extract_last_chat_line \
  separator \
  prompt_line \
  question \
  2>&1 | head -100

Research Techniques

  1. Raw ANSI capture via MCP: session action=output format=raw reveals cursor positioning, colors, and OSC sequences invisible in clean output
  2. Cursor-up distance as height probe: \033[NA reveals bottom zone height
  3. OSC sequence interception: \033]777;notify;... and \033]0;... carry metadata
  4. Color as semantic signal: RGB colors distinguish interactive vs chrome elements
  5. Forced state transitions: specific CLI flags surface all UI variants
  6. Screen clear detection: \033[2J\033[3J\033[H distinguishes full-screen menus

Agent Detection Matrix

Standardized checklist for analyzing and onboarding new AI coding agent CLIs. Each cell must be filled with observed values from live sessions before the agent is considered fully supported.

Detection Matrix

1. Identity & Rendering

PropertyClaude CodeCodex CLIGemini CLIAiderOpenCode
Version testedv2.1.81v0.116.0v0.34.0v0.86.2v1.2.20
Date tested2026-03-212026-03-212026-03-222026-03-222026-03-22
Rendering engineInk (React)Ink (React)Ink-like (Node.js)Python rich + readlineBubble Tea (Go)
Cursor positioningRelative (\033[NA])Absolute (\033[r;cH)Relative (\033[1A])Sequential (no cursor)Absolute (\033[r;cH)
Scroll mechanism\r\n paddingScroll regions (\033[n;mr])\r\n paddingNormal scrollFull-screen redraw
Screen clear on menusSometimes (\033[2J)NoNoN/AFull-screen TUI
Parsing strategyChanged-rows deltaChanged-rows deltaChanged-rows deltaChanged-rows deltaScreen snapshot

2. Prompt Line

PropertyClaude CodeCodex CLIGemini CLIAiderOpenCode
Prompt char (U+276F) (U+203A, bold)> (purple, rgb 215,175,255)> (green, ANSI #40)None (framed box)
Prompt backgroundNoneDark gray (rgb 57,57,57)Dark gray (rgb 65,65,65)NoneDark (rgb 30,30,30)
Prompt box border──── separatorsBackground color only▀▀▀ top / ▄▄▄ bottomNone┃╹▀ vertical frame
Ghost text styledim cell attribute\033[2m dimGray (rgb 175,175,175)N/AGray placeholder
Multiline inputEnter = submitEnter = newlineEnter = submitEnter = submitUnknown

3. Separator Lines

PropertyClaude CodeCodex CLIGemini CLIAiderOpenCode
Uses separatorsYesPartiallyYesYes (green ─────)No (uses ┃╹▀)
Separator chars (U+2500) (U+2500) (U+2500) (U+2500) (vertical frame)
Separator colorGray (rgb 136,136,136)StandardDark gray (rgb 88,88,88)Green (rgb 0,204,0)N/A
Separator purposeFrame prompt boxBetween tool output & summaryAbove prompt areaBetween conversation turnsPrompt box border
Decorated separatorsYes (──── label ──)NoNoNoN/A
Min run length4+ charsFull widthFull widthFull widthN/A

4. Status / Chrome Lines

PropertyClaude CodeCodex CLIGemini CLIAiderOpenCode
Mode line⏵⏵ <mode> (last row)NoneNoneNoneMode in prompt box (Build/Plan)
Status line(s)0-N below separator1 line below prompt2-row status bar (4 columns)Token report after responseRight panel (context, cost, LSP)
Status indent2 spaces (\033[2C)2 spaces1 spaceNoneN/A (panel layout)
Info lineNoneNoneShift+Tab to accept edits + MCP/skills countNonetab agents · ctrl+p commands
Subprocess countIn mode lineNoneNoneNoneNone (progress bar instead)

5. Spinner / Working Indicators

PropertyClaude CodeCodex CLIGemini CLIAiderOpenCode
Spinner chars✶✻✳✢· (U+2720-273F) (U+2022)⠋⠙⠹⠸⠴⠦⠧⠇ (braille)░█ / █░ (Knight Rider)■⬝ (progress bar)
Spinner colorWhiteStandardBlue/green (varies)StandardStandard
Spinner positionAbove separatorInline with outputBelow output, above separatorInline (backspace overwrite)Footer row
Time display(1m 32s)(10s • esc to interrupt)(esc to cancel, Ns)NoneNone
Token display↓ 2.2k tokensNoneNoneTokens: Nk sent, N received. Cost: $X.XXNone
Tip textSpinner verb namesNoneItalic tips during spinnerNoneNone
Detected byis_chrome_rowis_chrome_rowparse_status_lineparse_status_lineN/A (full TUI)

6. Interactive Menus

PropertyClaude CodeCodex CLIGemini CLIAiderOpenCode
Permission promptMultiselect (❯ 1. Yes)Not observed (sandbox)None (model-level refusal)File add: Y/N/A/S/D△ Permission required inline
Selection char (blue)Not observedN/AN/A⇆ select
Footer patternEsc to cancel/closeesc to interruptesc to cancel (in spinner)Noneenter confirm
OSC 777 notifyYesNoNoNoNo
OSC 0 window titleYes (task + spinner)YesYes (◇ Ready (workspace))NoNo
Slash commands/mcp, /stats, /status/model, /mcp, /fast/help, /settings, /model, /stats/helpNone observed

7. System Messages

PropertyClaude CodeCodex CLIGemini CLIAiderOpenCode
Output prefix (white/green/red) (U+2022) (U+2726, purple)None (blue text)None (inline in panel)
Tool call display + verb + description╭───╮ ✓ ToolName ╰───╯ boxNone read / write
Warning prefixN/A (U+26A0)N/AOrange textN/A
Error indicator (red) + error textRed text┃ Error: inline
Interrupt markerN/ANot observed^Cesc interrupt hint
Tool result (U+23BF) or inlineInside ╭───╮ boxInline completion marker

Trigger Procedures

How to force each UI state for analysis and testing.

Procedure A: Start agent in each permission mode

AgentRestricted modePermissive mode
Claude Codeclaude --permission-mode defaultclaude --permission-mode bypassPermissions
Codex CLIcodex -a untrustedcodex (suggest mode, default)
Gemini CLIgemini (default, workspace-restricted)gemini --sandbox=false (unconfirmed)
AiderN/A (no sandbox)N/A
OpenCodeUnknownUnknown

Procedure B: Trigger permission/approval prompt

AgentActionExpected result
Claude Code (default mode)“create a file /tmp/test.txt with hello”Multiselect: Yes/Yes+allow/No
Codex CLI (untrusted)SameNot observed — auto-approves in sandbox
Gemini CLI“create a file /tmp/test.txt with hello”Text refusal (workspace restriction)
AiderOpen file not in chatAdd file to the chat? (Y)es/(N)o/(A)ll/(S)kip all/(D)on't ask again
OpenCodeAccess external directory△ Permission required with Allow once / Allow always / Reject

Procedure C: Trigger interactive menus

AgentCommandExpected result
Claude Code/mcpServer list with selection
Claude Code/statsUsage heatmap with date cycling
Claude Code/statusSettings panel with search box
Codex CLI/modelModel selector
Codex CLI/mcpMCP server list
Gemini CLI/settingsSettings panel (unconfirmed)
Gemini CLI/statsUsage stats

Procedure D: Observe working state

AgentActionWhat to capture
AnySend a complex multi-tool taskSpinner animation, cursor-up distance
AnySend task during active subprocessSubprocess count display
AnyPress Escape during workInterrupt marker

Procedure E: Capture raw ANSI

For each state above:

session action=output session_id=<id> limit=8000 format=raw

Look for:

  • Cursor positioning: \033[NA] (relative up), \033[r;cH (absolute)
  • Colors: \033[38;2;R;G;Bm (RGB foreground)
  • Background: \033[48;2;R;G;Bm
  • Screen clear: \033[2J
  • Scroll regions: \033[n;mr
  • OSC sequences: \033]777;..., \033]0;..., \033]8;...

Onboarding a New Agent

  1. Fill the detection matrix columns by running procedures A-E
  2. Create docs/architecture/agents/<name>.md with observed layouts
  3. Update chrome.rs if new markers/chars are needed
  4. Add test cases from real captured text
  5. Run /agent-ui-audit skill to verify parser compatibility

Claude Code — UI Layout Reference

Agent-specific layout reference for Claude Code (Anthropic). See agent-ui-analysis.md for shared concepts.

Observed version: v2.1.81 (2026-03-21) Rendering engine: Ink (React for terminals) Rendering approach: ANSI relative cursor positioning (\033[NA, \033[1B)


Layout Anatomy (bottom → top)

[agent output / response text]
[empty line(s)]
✶ Undulating… (1m 32s · ↓ 2.2k tokens)     (spinner — above separator, while working)
[empty line]
──────────────────────────────────── (upper separator, may contain label)
❯ [user input]                       (prompt line)
──────────────────────────────────── (lower separator)
  [status line(s)]                   (0-N lines, indented 2 spaces)
  [mode line]                        (last row, indented 2 spaces)

Real-world Examples (live sessions, 2026-03-21)

Standard idle — no subprocess

❯
──────────────────────────────────────────────────────────────────────────
  [Opus 4.6 (1M context) | Max] │ tuicommander git:(main*)
  Context █░░░░░░░░░ 8% $0 (~$2.97) │ Usage ⚠ (429)
  ⏵⏵ bypass permissions on (shift+tab to cycle)

With subprocess — new format (count left)

───────────────────────────────────────────────────────── extractor ──
❯
──────────────────────────────────────────────────────────────────────────
  [Opus 4.6 (1M context) | Max] │ mdkb git:(feat/document-organizer*)
  Context ░░░░░░░░░░ 4% $0 (~$79.88) │ Usage ⚠ (429)
  1 shell · ⏵⏵ bypass permissions on

Subprocess only — no mode indicator

───────────────────────────────────────────────────────── extractor ──
❯
──────────────────────────────────────────────────────────────────────────
  [Opus 4.6 (1M context) | Max] │ mdkb git:(feat/document-organizer*)
  Context ░░░░░░░░░░ 4% $0 (~$79.81) │ Usage ⚠ (429)
  1 shell

Default mode (no bypass) — single status line, no mode line

❯
───────────────────────────────────────────────────────────────────────────────
  [Opus 4.6 (1M context) | Max] ░░░░░░░░░░ 3% | tuicommander git:(main*) |… ○ low · /ef…

Agent working — spinner above, no prompt box

  ⎿  $ ls -la /Users/stefano.straus/Documents/.mdkb/index.sqlite

✶ Undulating…


Separator Line

A row containing a run of 4+ box-drawing characters (─ ━ ═ — ╌ ╍). May contain embedded labels:

────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────── extractor ──
──────── ■■■ Medium /model ────────

Spinner / Timer Lines (above upper separator)

✶ Undulating…
✻ Sautéed for 1m 19s
✳ Ideating… (1m 32s · ↓ 2.2k tokens)
✻ Sautéed for 2m 9s · 1 local agent still running
· Proofing… (1m 14s · ↓ 1.6k tokens)

Markers: (U+2736), (U+273B), (U+2733), (U+2722), · (U+00B7). Detected by is_chrome_row (✻ check) and parse_status_line (dingbat range U+2720–U+273F).


Status Lines (between lower separator and mode line)

Zero or more lines. Content is arbitrary and agent-customizable. Indented with 2 spaces (via \033[2C).

  [Opus 4.6 (1M context) | Max] │ tuicommander git:(main*)
  Context █░░░░░░░░░ 5% $0 (~$0.64) │ Usage ⚠ (429)

Or Wiz HUD:

  [Opus 4.6 | Team] 54% | wiz-agents git:(main)
  5h: 42% (3h) | 7d: 27% (2d)
  ✓ Edit ×7 | ✓ Bash ×5

Configured via ~/.claude/settings.jsonstatusLine, but rendered through the PTY using ANSI cursor positioning. They appear as changed_rows and DO pass through is_chrome_row.

Bug: These lines contain none of the 4 chrome markers (⏵, ›, ✻, •), so they are not classified as chrome.


Mode Line (last row)

The final visible row. Always chrome. Indented with 2 spaces.

Observed variants

FormatExampleSource
Mode only ⏵⏵ bypass permissions ontest
Mode + hint ⏵⏵ bypass permissions on (shift+tab to cycle)live
Mode + subprocess (old) ⏵⏵ bypass permissions on · 1 shelltest
Mode + subprocess (old, plural) ⏵⏵ bypass permissions on · 2 local agentstest
Subprocess + mode (new) 1 shell · ⏵⏵ bypass permissions onlive
Subprocess only 1 shellscreenshot
Plan mode ⏸ plan mode on (shift+tab to cycle)test
Accept edits ⏵⏵ accept edits on (shift+tab to cycle)test
Auto mode ⏵⏵ auto modetest
Empty``test
Absent (default mode)N/A — no mode line at alllive

Key markers

  • ⏵⏵ (U+23F5 x2) — current mode-line prefix
  • ›› (U+203A x2) — older mode-line prefix
  • (U+23F8) — plan mode prefix
  • · (U+00B7) — separator between mode and subprocess count

Subprocess types observed

shell / shells, local agent / local agents, bash, background tasks


Interactive Menus

Permission Prompt

Replaces the entire bottom zone when CC requests tool approval.

⏺ Write(/tmp/test-permission-prompt.txt)
────────────────────────────────────────── (BLUE separator, rgb 177,185,249)
 Create file
 ../../../../../tmp/test-permission-prompt.txt
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ (dotted ╌ U+254C, rgb 80,80,80)
  1 hello
╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
 Do you want to create test-permission-prompt.txt?
 ❯ 1. Yes                                (❯ colored BLUE, rgb 177,185,249)
   2. Yes, allow all edits in tmp/ during this session (shift+tab)
   3. No

 Esc to cancel · Tab to amend
FeatureNormalPermission prompt
Top separatorGray (rgb 136,136,136)Blue (rgb 177,185,249)
Content separatorNoneDotted (U+254C)
charGray promptBlue selection (rgb 177,185,249)
Mode line⏵⏵/⏸ on last rowNone
Last lineMode indicatorEsc to cancel · Tab to amend

Custom Ink Menu (e.g., /wiz:setup)

────────────────────────────────────── (dim gray separator, \033[2m)
← ☐ Essential  ☐ Workflow  ☐ Tools  ✔ Submit  →
Select essential components to install:
❯ 1. [ ] notifications              (checkbox, ❯ blue)
  ...
────────────────────────────────────── (dim gray separator)
  6. Chat about this
Enter to select · Tab/Arrow keys to navigate · Esc to cancel

May clear the entire screen (\033[2J\033[3J\033[H).

Built-in Menu (/mcp)

───────────────────────────────────── (blue separator, rgb 177,185,249)
  Manage MCP servers
  ❯ tuicommander · ✔ connected
    context7 · ✔ connected
    mac · ✘ failed
  ※ Run claude --debug to see error logs
  https://code.claude.com/docs/en/mcp for help   (OSC 8 hyperlink)
 ↑↓ to navigate · Enter to confirm · Esc to cancel   (italic, \033[3m)

Built-in (/stats)

───────────────────────────────────────────────────────────────────────
    Overview   Models
      Mar Apr May Jun ... Mar
      ··········░·▓██▓█·
  ...
  You've used ~29x more tokens than The Count of Monte Cristo
    Esc to cancel · r to cycle dates · ctrl+s to copy

Built-in (/status)

───────────────────────────────────────────────────────────────────────
   Status   Config   Usage
  ╭───────────────────────────────────────────────────────────────────╮
  │ ⌕ Search settings...                                             │
  ╰───────────────────────────────────────────────────────────────────╯
    Auto-compact                              true
    ...
  ↓ 3 more below
  ←/→ tab to switch · ↓ to return · Esc to close

Uses box-drawing ╭╮╰╯│ for search box (not matched by is_separator_line).


OSC 777 Notifications

CC emits terminal notifications for user attention events:

\033]777;notify;Claude Code;Claude needs your permission to use Write\007
\033]777;notify;Claude Code;Claude Code needs your attention\007

All share the prefix \033]777;notify;Claude Code;.


Ink Rendering Mechanics

Spinner animation (above separator)

\033[8A          ← cursor UP 8 rows (to spinner position)
✶               ← overwrite spinner character
\r\n × 7        ← 7 empty newlines back down to bottom

The cursor-up count equals the bottom zone height:

  • 8 = bypass mode (spinner + empty + sep + prompt + sep + 2 status + mode)
  • 6 = default mode (spinner + empty + sep + prompt + sep + 1 status, no mode)
  • 10 = with subprocess in bypass mode

Full bottom zone redraw

\r\033[1B  separator ────
\r\033[1B  ❯ [input]
\r\033[1B  separator ────
\r\n       \033[2C [status line 1]    ← 2C = cursor forward 2 = indent
\r\n       \033[2C [status line 2]
\r\n       \033[2C [mode line]

Key implications

  • All bottom-zone rows transit the PTY as ANSI sequences
  • The vt100 parser processes them into screen buffer rows
  • Spinner updates touch the spinner row AND all rows below it, causing the entire bottom zone to appear as changed rows
  • The 2-space indent on status/mode lines comes from \033[2C

Codex CLI — UI Layout Reference

Agent-specific layout reference for Codex CLI (OpenAI). See agent-ui-analysis.md for shared concepts.

Observed version: v0.116.0 (2026-03-21) Rendering engine: Ink (React for terminals) Rendering approach: ANSI absolute positioning (\033[row;colH) + scroll regions


Layout Anatomy

Codex uses a fundamentally different approach from Claude Code:

  • No separator-framed prompt box — uses background color instead
  • Absolute cursor positioning\033[12;2H (row 12, col 2)
  • Terminal scroll regions\033[12;41r to define scrollable content area
  • Reverse index\033M to scroll content upward
[agent output with • bullet prefix]
[empty line]
 ← dark background (rgb 57,57,57) starts here
› [user input]                       (prompt, bold ›, dark bg)
                                     (dark bg continues)
                                     (dark bg continues)
  gpt-5.4 high · 100% left · ~/project   (status line, dim, normal bg)

Real-world Examples (live session, 2026-03-21)

Startup banner

╭────────────────────────────────────────────╮
│ >_ OpenAI Codex (v0.116.0)                 │
│                                            │
│ model:     gpt-5.4 high   /model to change │
│ directory: ~/Gits/personal/tuicommander    │
╰────────────────────────────────────────────╯
  Tip: Use /mcp to list configured MCP tools.

Idle (waiting for input)

› Summarize recent commits           (ghost text, dim)
  gpt-5.4 high · 100% left · ~/Gits/personal/tuicommander

After tool execution

› create a file called /tmp/codex-test.txt with "hello"
• Creating /tmp/codex-test.txt with the requested contents.
• Added /tmp/codex-test.txt (+1 -0)
    1 +hello
───────────────────────────────────────────────────────────────────────────────
• Created /tmp/codex-test.txt with hello.
› Summarize recent commits
  gpt-5.4 high · 98% left · ~/Gits/personal/tuicommander

After interrupt (Escape)

■ Conversation interrupted - tell the model what to do differently.
› Summarize recent commits
  esc again to edit previous message

Key Differences from Claude Code

FeatureClaude CodeCodex CLI
Prompt char (U+276F) (U+203A, bold)
Prompt boxSeparator-framed (────)Background color (rgb 57,57,57)
Cursor positioningRelative (\033[8A)Absolute (\033[12;2H)
Scrolling\r\n paddingScroll regions (\033[12;41r) + reverse index (\033M)
Status lineMulti-line, indented 2spSingle line, indented 2sp, dim
Mode line⏵⏵ bypass permissions on etc.None observed
Separator usageAround prompt boxBetween tool output and summary
System messages prefix (white/green/red) prefix (bullet)
WarningsN/A prefix (yellow)
Interrupt markerN/A prefix
Ghost textVia dim cell attributeVia \033[2m dim
Submit keyEnterEnter (but multiline: Enter = newline in prompt)

Prompt Line

  • Character: (U+203A) — bold \033[1m
  • Background: dark gray rgb(57,57,57)\033[48;2;57;57;57m
  • The prompt area spans multiple rows with dark background
  • Ghost text (placeholder) shown in dim: \033[2m

Multiline input: Codex supports multiline prompts where Enter adds a newline. Submit is also Enter (single line). This makes programmatic input tricky — sending text + Enter may add a newline instead of submitting.


Status Line

Single line below the prompt area, always present:

  gpt-5.4 high · 100% left · ~/Gits/personal/tuicommander

Format: <model> <effort> · <quota>% left · <directory>

Rendered in dim (\033[2m) with normal background (not dark bg).


Separator Usage

Codex uses ──── separators differently from CC — they appear between tool output and the agent’s summary response, not as a prompt box frame:

• Added /tmp/codex-test.txt (+1 -0)
    1 +hello
───────────────────────────────────────────────────────────────────────────
• Created /tmp/codex-test.txt with hello.

Spinner

Codex uses (U+2022) as a spinner/working indicator:

• Working (10s • esc to interrupt)

The is already in is_chrome_row’s marker set.


OSC Sequences

\033]10;?\033\\    — query terminal foreground color
\033]11;?\033\\    — query terminal background color
\033]0;...\007     — window title updates

No \033]777;notify; observed — Codex does not emit terminal notifications for approval prompts.


Approval Modes

CLI flag: -a or --ask-for-approval <POLICY>

PolicyBehavior
untrustedSandbox commands (does NOT prompt for approval)
on-failureDEPRECATED — auto-run, ask only on failure
on-requestModel decides when to ask
neverNever ask

Note: In untrusted mode, Codex auto-approves tool use within the sandbox. No interactive approval prompt was observed. The approval UI may only appear in specific edge cases or with on-request mode.


Slash Commands Observed

• Unrecognized command '/mode'. Type "/" for a list of supported commands.

Available: /model, /mcp, /fast, /feedback, /help, and others. Not observed: /stats, /status (CC-specific).


Known Issues

Enter key handling

Codex likely uses the kitty keyboard protocol to distinguish Enter (submit) from Enter (newline in multiline prompt). The TUICommander session action=input special_key=enter sends \r which Codex may interpret as newline. Workaround: send text and Enter in separate calls, but this is unreliable for multiline content.

ask_user_question tool (proposed)

GitHub issue openai/codex#9926 proposes a tabbed questionnaire UI similar to Claude Code’s skill menus. Currently available via request_user_input tool with collaboration_modes = true in config, but only in plan mode (Shift+Tab).


Rendering Mechanics (raw ANSI)

Absolute positioning

\033[12;2H     — cursor to row 12, col 2 (absolute)
\033[K         — erase to end of line

Scroll regions

\033[12;41r    — set scroll region rows 12-41
\033M          — reverse index (scroll content up within region)
\033[r         — reset scroll region

Prompt area rendering

\033[48;2;57;57;57m  — dark background starts
\033[1m›\033[22m     — bold › then unbold
\033[2m...          — dim ghost text
\033[49m             — background reset for status line

Unlike CC which uses relative cursor movement (\033[8A), Codex uses absolute positioning. This means changed_rows detection works differently — Codex updates specific rows by address rather than painting top-down.

Aider — UI Layout Reference

Agent-specific layout reference for Aider. See agent-ui-analysis.md for shared concepts.

Observed version: v0.86.2 (2026-03-22) Rendering engine: Python readline + rich (no TUI framework) Rendering approach: Sequential CLI output with ANSI colors


Key Characteristics

Aider is the simplest of all supported agents — a sequential CLI tool with no TUI framework, no screen management, no cursor positioning. Output flows linearly top-to-bottom like a normal shell command.

  • No Ink, no Bubble Tea — just Python with rich text formatting
  • Prompt is simple > (green, ANSI 256 color 40)
  • Spinner uses ░█/█░ Knight Rider pattern with backspace overwrite
  • No mode line, no status bar, no panels
  • File approval uses inline Y/N/A/S/D prompts

Observed States

Startup Banner

─────────────────────────────────────────────────────────────────────
Aider v0.86.2
Main model: openrouter/anthropic/claude-sonnet-4.5 with diff edit format, infinite output
Weak model: openrouter/anthropic/claude-haiku-4-5
Git repo: .git with 855 files
Repo-map: using 4096 tokens, auto refresh
─────────────────────────────────────────────────────────────────────
>
  • Green separators ───── (rgb 0,204,0)
  • Model info, git repo stats, repo-map config
  • Bare > prompt (green)

Working State (spinner)

░█  Updating repo map: examples/plugins/repo-dashboard/main.js
█░  Waiting for openrouter/anthropic/claude-sonnet-4.5
  • Knight Rider scanner: ░█ and █░ alternate using backspace (\b) to overwrite
  • (U+2591, light shade) and (U+2588, full block)
  • Task description after the scanner chars
  • Already detected by parse_status_line via AIDER_SPINNER_RE

Agent Response

To find the version of this project, I need to check the version files.
 • package.json (for the frontend/Node.js part)
 • src-tauri/Cargo.toml (for the Rust/Tauri part)
Please add these files to the chat so I can tell you the version.
Tokens: 11k sent, 75 received. Cost: $0.03 message, $0.03 session.
  • Blue text (rgb 0,136,255) for agent output
  • Bold bullets for lists
  • File names with inverted background (rgb 0,0,0 on rgb 248,248,248)
  • Token report after every response: Tokens: Nk sent, N received. Cost: $X.XX
  • Already detected by parse_status_line via AIDER_TOKENS_RE

File Approval Prompt

package.json
Add file to the chat? (Y)es/(N)o/(A)ll/(S)kip all/(D)on't ask again [Yes]:
  • File name shown in reverse video (\033[7m)
  • Inline prompt with 5 options: Y/N/A/S/D
  • Green text (same as main prompt)
  • Default answer in brackets: [Yes]
  • This is a readline prompt — Enter submits, no special handling needed

After Response (idle)

Tokens: 8.0k sent, 106 received. Cost: $0.03 message, $0.06 session.
─────────────────────────────────────────────────────────────────────
package.json src-tauri/Cargo.toml
>
  • Green separator between conversation turns
  • Active file list shown before prompt (files in chat context)
  • Bare > prompt

Error State

litellm.AuthenticationError: AuthenticationError: OpenrouterException - {"error":{"message":"User not found.","code":401}}
The API provider is not able to authenticate you. Check your API key.
  • Orange warning text (rgb 255,165,0) for non-fatal warnings
  • Red error text (rgb 255,34,34) for fatal errors

Detection Signals

Agent Identification

  • Aider v in startup banner
  • ░█ / █░ Knight Rider spinner
  • Tokens: + Cost: report after responses
  • Add file to the chat? approval prompt

Chrome Detection (is_chrome_row)

  • ░█ / █░ — not in current marker set but detected by parse_status_line
  • Separator ───── — detected by is_separator_line
  • Prompt > — detected by is_prompt_line
  • Token report lines — not chrome markers, but not agent output either

Subtask / Subprocess Count

None. Aider does not have subprocess/subtask concepts.

Permission / Approval

  • No tool approval system — Aider auto-applies edits (or asks about file adds)
  • File add approval: Add file to the chat? (Y)es/(N)o/...
  • Edit confirmation: only with --auto-commits disabled, shows diff for review

Rendering Mechanics (raw ANSI)

Sequential output (no cursor positioning)

\r\n    — standard newlines, no cursor movement
\b      — backspace for spinner animation only

Color scheme

\033[0;38;5;40m        — green (ANSI 256 #40) for prompt and UI elements
\033[38;2;0;136;255m   — blue (rgb 0,136,255) for agent response text
\033[38;2;0;204;0m     — green (rgb 0,204,0) for separators
\033[38;2;255;165;0m   — orange for warnings
\033[38;2;255;34;34m   — red for errors
\033[7m                — reverse video for file names
\033[1m                — bold for list bullets

Spinner (Knight Rider)

░█\b\b       — write 2 chars, backspace 2
█░\b\b       — overwrite with swapped chars
  \b\b       — clear with spaces

Uses backspace (\b) to overwrite in place. No cursor positioning.

Readline integration

\033[?2004h    — enable bracketed paste
\033[6n        — request cursor position (readline)
\033[?25l/h    — hide/show cursor during drawing

Implications for TUICommander

Parsing Strategy

Aider is the ideal case for chrome.rs changed-rows detection:

  • Sequential output → each new line is a new changed row
  • No full-screen redraws → delta analysis works perfectly
  • Spinner overwrites in place → appears as single changed row

Already Supported

  • AIDER_SPINNER_RE in parse_status_line detects the Knight Rider scanner
  • AIDER_TOKENS_RE detects token reports
  • is_separator_line matches the green ───── separators
  • is_prompt_line matches the bare > prompt
  • (U+2591) and (U+2588) detected by is_chrome_row — Knight Rider spinner classified as chrome
  • has_status_line in chrome_only calculation — spinner-only chunks don’t reset silence timer
  • find_chrome_cutoff correctly trims Aider bottom zone (separator + file list + prompt)

Not Yet Supported

  • File approval prompt detection (Add file to the chat?)
  • Token/cost extraction from the report line
  • File context list (shown before prompt) — not classified as chrome

Gemini CLI — UI Layout Reference

Agent-specific layout reference for Gemini CLI (Google). See agent-ui-analysis.md for shared concepts.

Observed version: v0.34.0 (2026-03-22) Rendering engine: Ink-like (Node.js, ANSI relative positioning) Rendering approach: ANSI relative cursor positioning (\033[1A, \033[2K, \033[G)


Layout Anatomy (bottom → top)

[agent output with ✦ prefix]
[suggest line]
                                                          ? for shortcuts
───────────────────────────────── (separator, gray rgb 88,88,88)
 Shift+Tab to accept edits                    1 MCP server | 3 skills
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ (prompt box top, dark rgb 30,30,30 on bg 65,65,65)
 > [user input]                  (prompt, purple >, ghost text gray)
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ (prompt box bottom)
 workspace (/directory)          branch          sandbox              /model
 ~/path                          main            no sandbox           Auto (Gemini 3)

Bottom zone = 8 rows: shortcuts hint, separator, info line, prompt box (3 rows), status labels, status values.


Real-world Examples (live session, 2026-03-22)

Startup banner

  ▝▜▄     Gemini CLI v0.34.0
    ▝▜▄
   ▗▟▀    Signed in with Google: user@example.com /auth
  ▝▀      Plan: Gemini Code Assist for individuals /upgrade
╭───────────────────────────────────────────────────────────────────────╮
│ We're making changes to Gemini CLI that may impact your workflow.     │
│ What's Changing: ...                                                  │
│ Read more: https://goo.gle/geminicli-updates                          │
╰───────────────────────────────────────────────────────────────────────╯
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
  • Geometric ASCII art logo: ▝▜▄ / ▗▟▀ / ▝▀
  • Auth + plan info inline
  • Notification box with ╭╮╰╯│ border (like CC /status search box)
  • Numbered tips list

Idle (waiting for input)

                                                                  ? for shortcuts
─────────────────────────────────────────────────────────────────────────────────
 Shift+Tab to accept edits                              1 MCP server | 3 skills
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
 >   Type your message or @path/to/file
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▀▀
 workspace (/directory)          branch          sandbox              /model
 ~/Gits/personal/tuicommander   main            no sandbox           Auto (Gemini 3)

Working state (spinner)

✦ intent: read package.json version (package.json)
  I will read the package.json file to find the project version.
 ⠴ Check tool-specific usage stats with /stats tools… (esc to cancel, 14s)
─────────────────────────────────────────────────────────────────────────────────
 Shift+Tab to accept edits
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
 >   Type your message or @path/to/file
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▀▀
 workspace (/directory)          branch          sandbox              /model
 ~/Gits/personal/tuicommander   main            no sandbox           Auto (Gemini 3)
  • Spinner line appears between agent output and separator
  • Braille spinner + italic tip text + (esc to cancel, Ns) timer
  • During work, ? for shortcuts disappears, info line loses right-side text

Tool call (in response)

╭─────────────────────────────────────────────────────────────────────────╮
│ ✓  ReadFile package.json                                                │
│                                                                         │
╰─────────────────────────────────────────────────────────────────────────╯
  • Bordered box with ╭╮╰╯│ (same chars as startup notification)
  • prefix for completed tool calls
  • Tool name + arguments inline

Agent response (completed)

✦ The version of this project is 0.9.5, as specified in the package.json file.
  suggest: View CHANGELOG.md | Check README.md | List active sessions | Search codebase
  • (U+2726, purple rgb 215,175,255) prefix for agent output
  • suggest: line follows response (TUICommander protocol)
  • No token/cost report

Out-of-workspace write rejection

✦ I am unable to create the file at /tmp/gemini-test.txt because it is outside
  the allowed workspace directories. I can, however, create it within the
  project directory or the project's temporary directory.
  Would you like me to create it at ~/.gemini/tmp/tuicommander/gemini-test.txt?
  • No interactive permission prompt — Gemini refuses with text explanation
  • Workspace restriction enforced at model level, not via UI prompt

Prompt Line

  • Character: > (ASCII, colored purple rgb 215,175,255)
  • Background: dark gray rgb(65,65,65)\033[48;2;65;65;65m
  • Prompt box bordered by ▀▀▀ (U+2580, upper half block) top and ▄▄▄ (U+2584, lower half block) bottom
  • Border colors: dark rgb(30,30,30) foreground on rgb(65,65,65) background
  • Ghost text: gray rgb(175,175,175)Type your message or @path/to/file
  • Cursor shown with reverse video \033[7m
  • Enter = submit (single line prompt)

Separator Line

Single horizontal rule above the prompt area:

─────────────────────────────────────────────────── (gray, rgb 88,88,88)
  • Always present in idle and working states
  • No decorated separators (no embedded labels)
  • Uses (U+2500) — same char as CC and Codex

Spinner / Working Indicator

 ⠴ Check tool-specific usage stats with /stats tools… (esc to cancel, 14s)
 ⠋ Exclude specific tools from being used (settings.json)… (esc to cancel, 33s)
  • Braille spinner: ⠋⠙⠹⠸⠴⠦⠧⠇ (U+2800 range)
  • Color varies: blue rgb(135,189,241), green rgb(224,255,206) — colors shift during animation
  • Format: ⠋ <italic tip text>… (esc to cancel, Ns)
  • Tip text: italic (\033[3m), shows contextual tips/suggestions during work
  • Timer: (esc to cancel, Ns) in gray rgb(175,175,175)
  • Position: between agent output and separator (above bottom zone)

Already detected by parse_status_line via GEMINI_SPINNER_RE (braille range check).


Status Bar (bottom 2 rows)

Always visible. 4 columns with label/value pairs:

 workspace (/directory)          branch          sandbox              /model
 ~/Gits/personal/tuicommander   main            no sandbox           Auto (Gemini 3)
  • Labels: gray rgb(175,175,175)workspace (/directory), branch, sandbox, /model
  • Values: white rgb(255,255,255) — path, branch name, sandbox status, model name
  • Sandbox warning: pink rgb(255,135,175) when no sandbox

Info Line (between separator and prompt box)

 Shift+Tab to accept edits                              1 MCP server | 3 skills
  • Left: Shift+Tab to accept edits (gray)
  • Right: N MCP server | N skills (gray) — MCP and skill counts
  • Above the prompt box, below the separator

A second info hint ? for shortcuts appears right-aligned above the separator when idle.


Window Title (OSC 0)

\033]0;◇  Ready (tuicommander)\007
  • (U+25C7, white diamond) — state indicator
  • Ready — current state
  • (tuicommander) — workspace name
  • Updates on state changes

Detection Signals

Agent Identification

  • Gemini CLI v in startup banner
  • Geometric ASCII logo ▝▜▄
  • (U+2726) output prefix
  • Braille spinner ⠋⠙⠹⠸⠴⠦⠧⠇
  • ? for shortcuts hint line
  • OSC 0 with diamond

Chrome Detection (is_chrome_row)

  • Braille spinner chars — detected by parse_status_line via GEMINI_SPINNER_RE
  • Separator ───── — detected by is_separator_line
  • Prompt > — detected by is_prompt_line
  • ▀▀▀ / ▄▄▄ prompt box borders — NOT in chrome marker set
  • Status bar labels/values — NOT chrome markers
  • (U+2726) — NOT in is_chrome_row marker set

Subtask / Subprocess Count

None. Gemini CLI does not expose subprocess/subtask counts. Tool calls shown inline in bordered boxes (╭───╮ ✓ ReadFile ╰───╯).

Permission / Approval

  • No interactive permission UI — workspace restriction enforced at model level
  • Out-of-scope writes rejected with text explanation
  • No Esc to cancel permission footer
  • No OSC 777 notifications observed

Rendering Mechanics (raw ANSI)

Relative cursor positioning

\033[1A     — cursor UP 1 row (repeated for multi-row updates)
\033[2K     — erase entire line
\033[G      — cursor to column 1
\033[4A     — cursor UP 4 (bottom zone jump)
\033[4G     — cursor to column 4
\033[4B     — cursor DOWN 4 (back to bottom)

Uses \033[1A] repeated (like CC) rather than absolute \033[r;cH (like Codex/OpenCode). Bottom zone updates use \033[4A...\033[4B pattern to jump up, redraw, jump back.

Prompt box rendering

\033[48;2;65;65;65m              — dark gray background
\033[38;2;30;30;30m▀▀▀▀...      — dark top border on gray bg
\033[38;2;215;175;255m>           — purple prompt char
\033[7m \033[27m                 — cursor (reverse video block)
\033[38;2;175;175;175m...        — gray ghost text
\033[38;2;30;30;30m▄▄▄▄...      — dark bottom border
\033[49m                         — reset background

Color scheme

\033[38;2;215;175;255m   — purple (prompt char >, output prefix ✦, file names)
\033[38;2;255;255;255m   — white (agent text, status values)
\033[38;2;175;175;175m   — gray (labels, hints, timer)
\033[38;2;88;88;88m      — dark gray (separator ─────)
\033[38;2;255;135;175m   — pink (sandbox warning)
\033[38;2;135;189;241m   — blue (spinner, varies)
\033[38;2;224;255;206m   — green (spinner, varies)

Spinner animation

\033[3m     — italic on (for tip text)
\033[23m    — italic off

Spinner updates use the same \033[1A]\033[2K] erase-and-redraw pattern as the bottom zone.


Implications for TUICommander

Parsing Strategy

Gemini CLI is a CLI inline agent — changed-rows delta analysis works. Similar to CC in rendering mechanics (relative cursor positioning).

Already Supported

  • Braille spinner detected by parse_status_line via GEMINI_SPINNER_RE
  • Separator ───── detected by is_separator_line
  • Prompt > detected by is_prompt_line

Not Yet Supported

  • (U+2726) is the Gemini agent output prefix (NOT chrome — do not add to is_chrome_row)
  • Tool call boxes (╭╮╰╯│) not classified as chrome
  • ? for shortcuts hint line not classified as chrome
  • Status bar labels (bottom 2 rows) have no chrome markers (but find_chrome_cutoff trims them via separator anchor)
  • No sandbox/permission prompt detection needed (Gemini handles this at model level)

Now Supported (as of chrome.rs multi-agent update)

  • ▀▀▀ / ▄▄▄ prompt box borders detected as chrome via is_chrome_row
  • Braille spinner classified as chrome via has_status_line in chrome_only calculation
  • find_chrome_cutoff correctly trims the full Gemini 8-row bottom zone
  • Separator ───── detected by is_separator_line
  • Prompt > detected by is_prompt_line

OpenCode — UI Layout Reference

Agent-specific layout reference for OpenCode. See agent-ui-analysis.md for shared concepts.

Observed version: v1.2.20 (2026-03-22) Rendering engine: Bubble Tea (Go TUI framework) Rendering approach: Full-screen TUI, ANSI absolute positioning, mouse tracking


Key Difference from Other Agents

OpenCode is a full-screen TUI application, not a CLI that renders inline in the terminal like Claude Code or Codex. It takes over the entire terminal screen with its own layout, panels, and navigation. This means:

  • The “bottom zone” concept from CC/Codex does not directly apply
  • OpenCode manages its own screen regions (panels, status bar, prompt)
  • Mouse tracking is enabled (\033[?1000h, \033[?1003h, \033[?1006h)
  • It uses bracketed paste (\033[?2004h) and focus events (\033[?1004h)

Observed States

Welcome Screen

Only shown on fresh start, before first message:

                                                     █▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█
                                                     █  █ █  █ █▀▀▀ █  █ █    █  █ █  █ █▀▀▀
                                                     ▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀
                                   ┃
                                   ┃  Ask anything... "What is the tech stack of this project?"
                                   ┃
                                   ┃  Build  Claude Sonnet 4.5 lansweeper.ai
                                   ╹▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
                                                                          tab agents  ctrl+p commands
                                         ● Tip Set agent temperature from 0.0 (focused) to 1.0 (creative)
  ~/Gits/personal/tuicommander:main                                                          1.2.20

Conversation (after first message) — two-panel layout

  ┃  what version is this project                                           █  Project version inquiry
  ┃                                                                         █
                                                                            █  Context
     Leggo la versione del progetto...                                      █  20,192 tokens
                                                                            █  0% used
     → Read SPEC.md [limit=50]                                              █  $0.00 spent
     → Read package.json [limit=20]                                         █
     → Read src-tauri/tauri.conf.json [limit=30]                            █  LSP
                                                                            █  LSPs will activate...
     Questo progetto è alla versione 0.9.5...                               █
                                                                            █
     ▣  Build · anthropic/claude-sonnet-4.5 · 10.2s                         █
                                                                            █
  ┃
  ┃  Build  Claude Sonnet 4.5 lansweeper.ai          ~/path:main
  ╹▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
                                          tab agents  ctrl+p commands    • OpenCode 1.2.20

Key elements:

  • Left panel: conversation (messages, tool calls, results)
  • Right panel: sidebar with border — title, context tokens, cost, LSP info
  • Prompt box: bottom, framed with ┃╹▀
  • Model info: inside prompt box Build Claude Sonnet 4.5 lansweeper.ai

Permission Prompt

  ┃  △ Permission required
  ┃    ← Access external directory /tmp
  ┃
  ┃  Patterns
  ┃
  ┃  - /tmp/*
  ┃
  ┃                                                              ~/path:main
  ┃   Allow once   Allow always   Reject   ctrl+f fullscreen  ⇆ select  enter confirm
  ┃                                                              • OpenCode 1.2.20

Working State (during tool execution)

Footer changes to show progress bar and interrupt hint:

   ■■⬝⬝⬝⬝⬝⬝  esc interrupt                            tab agents  ctrl+p commands    • OpenCode 1.2.20
  • (U+25A0) — completed steps
  • (U+2B1D) — remaining steps
  • Mode label may switch: BuildPlan in prompt box

Error State

  ┃  Error: Unable to connect. Is the computer able to access the url?

Errors displayed inline in the frame, same area as conversation.


Key elements:

  • (U+25B3): permission required marker
  • : tool call prefix (Write direction)
  • 3 inline options: Allow once Allow always Reject — not numbered, not multiselect
  • Footer: ctrl+f fullscreen ⇆ select enter confirm
  • (U+21C6): select/navigate hint
  • Pattern display: shows glob pattern (/tmp/*)
  • Entire dialog inside frame — prompt box expands to contain it

UI Element Reference

Prompt Frame

  • Left border: (U+2503, heavy vertical)
  • Bottom border: ╹▀▀▀▀... (U+2579 corner + U+2580 upper half blocks)
  • No , , or > prompt char
  • Model info inline: Build Claude Sonnet 4.5 lansweeper.ai

Right Panel Border

  • (U+2588, full block) — vertical border for sidebar
  • (U+2584, lower half block) — top corner of sidebar

Tool Call Prefixes

  • — Read operations (files read by the agent)
  • — Write operations (files written/modified by the agent)

Completion Marker

  • (U+25A3, white square with rounded corners) — marks completed tool calls
  • Format: ▣ Build · anthropic/claude-sonnet-4.5 · 10.2s

Tips

  • (U+25CF) — orange marker (rgb 245,167,66)
  • Format: ● Tip <highlighted_word> <gray description>

Status Bar

  • Left: ~/Gits/personal/tuicommander:main (path + branch)
  • Right: 1.2.20 or • OpenCode 1.2.20
  • tab agents — switch to agents panel
  • ctrl+p commands — command palette
  • ctrl+f fullscreen — toggle fullscreen (in permission dialog)
  • ⇆ select — select between options
  • enter confirm — confirm selection

Rendering Mechanics

Full-screen with background

\033[48;2;10;10;10m    — near-black background fills entire screen
\033[48;2;30;30;30m    — slightly lighter for input box

Absolute cursor positioning

\033[29;42H            — cursor to row 29, col 42

Mouse tracking (enabled on startup)

\033[?1000h   — normal mouse tracking
\033[?1002h   — button-event tracking
\033[?1003h   — all-motion tracking
\033[?1006h   — SGR mouse mode
\033[?1004h   — focus events

Kitty keyboard protocol

\033[?2026h / \033[?2026l   — toggled very frequently (polling pattern)

Cursor

\033[1 q      — blinking block cursor
\033[?25h     — show cursor
\033[?25l     — hide cursor (during redraws)

Color palette queries (on startup)

\033]4;0;?\007 through \033]4;15;?\007    — all 16 ANSI palette colors
\033]10;?\007 through \033]19;?\007       — foreground, background, etc.

Implications for TUICommander

Chrome Detection

OpenCode is a full-screen TUI — every row changes on every update. The changed_rows / is_chrome_row approach does not work. Needs:

  • Full-screen TUI detection mode (mouse tracking + full background = TUI)
  • Screen-snapshot-based parsing instead of changed-row delta analysis

Prompt Detection

No standard prompt char (, , >). Would need to detect frame or input box background color change.

Permission Detection

  • △ Permission required is a unique text signal
  • Allow once Allow always Reject footer is unique to OpenCode
  • No OSC 777 notifications observed

Subtask / Subprocess Count

None. OpenCode does not expose subprocess/subtask counts. Instead:

  • Tool calls shown inline in conversation panel (→ Read, ← Write)
  • Progress bar in footer: ⬝■■■■■■⬝ (filled/empty squares)
  • Completion marker: ▣ Build · model · time

Working State

  • Progress bar: ■■⬝⬝⬝⬝⬝⬝ in footer row — graphical, not numeric
  • Mode label: changes from Build to Plan in prompt box
  • Interrupt hint: esc interrupt in footer during work
  • No spinner chars — uses progress bar instead

Error Display

Errors shown inline in the frame:

  ┃  Error: Unable to connect. Is the computer able to access the url?

Agent Identification

OpenCode can be detected by:

  • ASCII art banner with OPENCODE text on first screen
  • ┃╹▀ vertical frame chars
  • Mouse tracking enabled on startup
  • • OpenCode X.Y.Z in status bar

HTTP API Reference

REST API served by the Axum HTTP server when MCP server is enabled. All Tauri commands are accessible as HTTP endpoints.

Base URL

  • Local (Unix socket): <config_dir>/mcp.sock — always started on macOS/Linux. No auth, MCP always enabled. Used by the local MCP bridge binary.
  • Remote (TCP): http://<host>:{remote_access_port} — only started when remote access is enabled in settings. HTTP Basic Auth required.

Authentication

  • MCP mode (localhost): No authentication
  • Remote access mode: HTTP Basic Auth with configured username/password

Session Endpoints

List Sessions

GET /sessions

Returns array of active session info (ID, cwd, worktree path, branch).

Create Session

POST /sessions
Content-Type: application/json

{
  "rows": 24,
  "cols": 80,
  "shell": "/bin/zsh",    // optional
  "cwd": "/path/to/dir"   // optional
}

Returns { "session_id": "..." }.

Create Session with Worktree

POST /sessions/worktree
Content-Type: application/json

{ "pty_config": { ... }, "worktree_config": { ... } }

Creates a git worktree and a PTY session in one call.

Spawn Agent Session

POST /sessions/agent
Content-Type: application/json

{ "pty_config": { ... }, "agent_config": { ... } }

Spawns an AI agent (Claude, etc.) in a PTY session.

Write to Session

POST /sessions/:id/write
Content-Type: application/json

{ "data": "ls -la\n" }

Resize Session

POST /sessions/:id/resize
Content-Type: application/json

{ "rows": 30, "cols": 120 }

Read Output

GET /sessions/:id/output?limit=4096&format=text

Returns recent output. Format controls what is returned:

formatResponse shapeDescription
(omit){ "data": "<bytes>" }Raw PTY bytes, base64-encoded
text{ "data": "<string>" }ANSI-stripped plain text from ring buffer
log{ "lines": [...], "total_lines": N }VT100-extracted clean lines (no ANSI, no TUI garbage)
ParamDefaultDescription
limit(all)raw/text: max bytes; log: max lines to return
offset(tail)log only: absolute start offset. When omitted, returns the newest limit lines (tail). When provided, returns lines starting from that offset
format(raw)See table above

format=log reads from VtLogBuffer — a VT100-aware buffer that extracts only scrolled-off lines, suppressing alternate-screen TUI apps (vim, htop, claude). Ideal for mobile clients.

total_lines in the response is a monotonically increasing counter — it never decreases when old lines are evicted from the buffer. Use it as a stable cursor for paginated reads. The offset parameter operates in the same coordinate space.

Kitty Protocol Flags

GET /sessions/:id/kitty-flags

Returns the current Kitty keyboard protocol flags (integer) for a session.

Foreground Process

GET /sessions/:id/foreground

Returns the foreground process info for a session.

Pause/Resume

POST /sessions/:id/pause
POST /sessions/:id/resume

Rename Session

PUT /sessions/:id/name
Content-Type: application/json

{ "name": "my-session" }

Sets a custom display name for a session.

Close Session

DELETE /sessions/:id?cleanup_worktree=false

Streaming Endpoints

WebSocket PTY Stream

WS /sessions/:id/stream

Receives real-time PTY output as text frames. One WebSocket per session.

WebSocket JSON Framing (Mobile/Browser)

WebSocket connections to /sessions/:id/stream receive JSON-framed messages:

{"type": "output", "data": "raw terminal output text"}
{"type": "parsed", "event": {"type": "question", "text": "Allow?"}}
{"type": "exit"}
{"type": "closed"}

Frame types:

  • output — Raw PTY output (ANSI-stripped when ?format=text)
  • log — VT100-extracted clean lines batch (when ?format=log): {"type":"log","lines":[...],"offset":N}
  • parsed — Structured events (questions, rate limits, errors) from the output parser
  • exit — Session process exited
  • closed — Session was closed

WebSocket format=log

WS /sessions/:id/stream?format=log

When ?format=log is specified, the connection streams VT100-extracted log lines instead of raw PTY chunks:

  • On connect: sends all accumulated lines as a single catch-up frame
  • While running: polls every 200ms and sends new lines batched by offset
  • PTY input passthrough is still available (write text/binary frames to send to PTY)

Server-Sent Events (SSE)

GET /events?types=repo-changed,pty-parsed

Broadcasts server-side events to all browser/mobile clients. Supports optional ?types= query parameter for comma-separated event name filtering. Uses monotonic event IDs and 15-second keep-alive pings.

EventPayloadDescription
session-created{session_id, cwd}New session started
session-closed{session_id}Session ended
repo-changed{repo_path}Git repository state changed
head-changed{repo_path, branch}Git HEAD changed (branch switch)
pty-parsed{session_id, parsed}Structured output event from PTY parser
pty-exit{session_id}PTY process exited
plugin-changed{plugin_ids}Plugin(s) installed/removed/updated
upstream-status-changed{name, status}MCP upstream server status change
mcp-toast{title, message, level, sound}Toast notification from MCP layer
lagged{missed}Client fell behind; N events were dropped

MCP Streamable HTTP

POST /mcp
Content-Type: application/json

{ JSON-RPC message }

Single endpoint for all MCP JSON-RPC requests (initialize, tools/list, tools/call). Returns JSON-RPC responses directly in the HTTP response body. Session ID returned via Mcp-Session-Id header on initialize.

GET /mcp          → 405 Method Not Allowed
DELETE /mcp       → Ends MCP session (pass Mcp-Session-Id header)

Git Endpoints

Repository Info

GET /repo/info?path=/path/to/repo

Returns RepoInfo (name, branch, status, initials).

Git Diff

GET /repo/diff?path=/path/to/repo

Returns unified diff string.

Diff Stats

GET /repo/diff-stats?path=/path/to/repo

Returns { "additions": N, "deletions": N }.

Changed Files

GET /repo/files?path=/path/to/repo

Returns array of ChangedFile (path, status, additions, deletions).

Single File Diff

GET /repo/file-diff?path=/path/to/repo&file=src/main.rs

Returns diff for a single file.

Read File

GET /repo/file?path=/path/to/repo&file=src/main.rs

Returns file contents as text.

Branches

GET /repo/branches?path=/path/to/repo

Returns sorted branch list.

Repo Summary

GET /repo/summary?path=/path/to/repo

Aggregate snapshot: worktree paths, merged branches, and per-path diff stats in one round-trip. Replaces 3+ separate IPC calls.

Repo Structure (Progressive Phase 1)

GET /repo/structure?path=/path/to/repo

Returns { "worktree_paths": { "branch": "/path", ... }, "merged_branches": ["branch", ...] }. Fast path — no diff stats computation.

Repo Diff Stats (Progressive Phase 2)

GET /repo/diff-stats/batch?path=/path/to/repo

Returns { "diff_stats": { "/path": { "additions": N, "deletions": N }, ... }, "last_commit_ts": { "branch": N, ... } }. Slow path — computes per-worktree diff stats and last commit timestamps.

Local Branches

GET /repo/local-branches?path=/path/to/repo

Returns local branch list.

Checkout Remote Branch

POST /repo/checkout-remote
Content-Type: application/json

{ "repoPath": "/path/to/repo", "branchName": "feat-remote" }

Creates a local tracking branch from origin/<branchName>.

Rename Branch

POST /repo/branch/rename
Content-Type: application/json

{ "path": "/path/to/repo", "old_name": "old", "new_name": "new" }

Check Main Branch

GET /repo/is-main-branch?branch=main

Returns true if the branch is main/master/develop.

Initials

GET /repo/initials?name=my-repo

Returns 2-char repo initials.

Markdown Files

GET /repo/markdown-files?path=/path/to/repo

Returns list of .md files in a directory.

Recent Commits

GET /repo/recent-commits?path=/path/to/repo

Returns recent git commits.

GitHub Status

GET /repo/github?path=/path/to/repo

Returns PR status, CI status, ahead/behind for current branch.

PR Statuses (Batch)

GET /repo/prs?path=/path/to/repo

Returns BranchPrStatus[] for all branches with open PRs.

PR Statuses (Multi-Repo Batch)

POST /repo/prs/batch
Content-Type: application/json

{ "paths": ["/repo1", "/repo2"], "include_merged": false }

Returns aggregated PR statuses across multiple repositories.

Issues

GET /repo/issues?path=/path/to/repo

Returns GitHubIssue[] for the repo, filtered by the user’s configured issue filter.

Close Issue

POST /repo/issues/close
Content-Type: application/json

{ "repo_path": "/path/to/repo", "issue_number": 42 }

Closes the specified issue via GitHub GraphQL API.

Reopen Issue

POST /repo/issues/reopen
Content-Type: application/json

{ "repo_path": "/path/to/repo", "issue_number": 42 }

Reopens a closed issue via GitHub GraphQL API.

Merged Branches

GET /repo/branches/merged?path=/path/to/repo

Returns list of branch names merged into the default branch.

Orphan Worktrees

GET /repo/orphan-worktrees?repoPath=/path/to/repo

Returns list of worktree directory paths that are in detached HEAD state (their branch was deleted).

Remove Orphan Worktree

POST /repo/remove-orphan
Content-Type: application/json

{ "repoPath": "/path/to/repo", "worktreePath": "/path/to/worktree" }

Removes an orphan worktree by filesystem path. The worktree path is validated against the repo’s actual worktree list.

Merge PR via GitHub

POST /repo/merge-pr
Content-Type: application/json

{ "repoPath": "/path/to/repo", "prNumber": 42, "mergeMethod": "squash" }

Merges a PR via the GitHub API. mergeMethod must be "merge", "squash", or "rebase". Returns {"sha": "..."} on success.

Approve PR

POST /repo/approve-pr
Content-Type: application/json

{ "repoPath": "/path/to/repo", "prNumber": 42 }

Submits an approving review on a PR via the GitHub API.

CI Checks

GET /repo/ci?path=/path/to/repo

Returns detailed CI check list.

PR Diff

GET /repo/pr-diff?path=/path/to/repo

Returns diff for the current branch’s open PR.

Remote URL

GET /repo/remote-url?path=/path/to/repo

Returns the remote origin URL.

Git Panel Endpoints

Working Tree Status

GET /repo/working-tree-status?path=/path/to/repo

Returns porcelain v2 working tree status.

Panel Context

GET /repo/panel-context?path=/path/to/repo

Returns aggregated context for the Git Panel (status, branch, merge state).

Stage Files

POST /repo/stage
Content-Type: application/json

{ "repoPath": "/path/to/repo", "files": ["src/main.rs"] }

Unstage Files

POST /repo/unstage
Content-Type: application/json

{ "repoPath": "/path/to/repo", "files": ["src/main.rs"] }

Discard Files

POST /repo/discard
Content-Type: application/json

{ "repoPath": "/path/to/repo", "files": ["src/main.rs"] }

Commit

POST /repo/commit
Content-Type: application/json

{ "repoPath": "/path/to/repo", "message": "feat: add feature" }

Run Git Command

POST /repo/run-git
Content-Type: application/json

{ "repoPath": "/path/to/repo", "args": ["log", "--oneline", "-5"] }

Runs an arbitrary git command in the repo directory.

Commit Log

GET /repo/commit-log?path=/path/to/repo

Returns commit log entries.

File History

GET /repo/file-history?path=/path/to/repo&file=src/main.rs

Returns git log for a specific file.

File Blame

GET /repo/file-blame?path=/path/to/repo&file=src/main.rs

Returns line-by-line blame annotations.

Stash Endpoints

List Stashes

GET /repo/stash?path=/path/to/repo

Returns stash list.

Apply Stash

POST /repo/stash/apply
Content-Type: application/json

{ "repoPath": "/path/to/repo", "index": 0 }

Pop Stash

POST /repo/stash/pop
Content-Type: application/json

{ "repoPath": "/path/to/repo", "index": 0 }

Drop Stash

POST /repo/stash/drop
Content-Type: application/json

{ "repoPath": "/path/to/repo", "index": 0 }

Show Stash

GET /repo/stash/show?path=/path/to/repo&index=0

Returns diff of a stash entry.

Log Endpoints

Get Logs

GET /logs?limit=50&level=error&source=terminal

Retrieve log entries from the ring buffer (1000 entries max). All query params optional:

  • limit — max entries to return (0 = all, default: 0)
  • level — filter by level: debug, info, warn, error
  • source — filter by source: app, plugin, git, network, terminal, github, dictation, store, config

Push Log

POST /logs
{ "level": "warn", "source": "git", "message": "...", "data_json": "{...}" }

Clear Logs

DELETE /logs

Configuration Endpoints

App Config

GET /config
PUT /config

Load/save AppConfig.

Hash Password

POST /config/hash-password
Content-Type: application/json

{ "password": "..." }

Returns bcrypt hash string.

Notification Config

GET /config/notifications
PUT /config/notifications

Load/save NotificationConfig.

UI Preferences

GET /config/ui-prefs
PUT /config/ui-prefs

Load/save UIPrefsConfig.

Repository Settings

GET /config/repo-settings
PUT /config/repo-settings

Load/save per-repository settings.

Repository Defaults

GET /config/repo-defaults
PUT /config/repo-defaults

Load/save default settings applied to new repositories.

Check Custom Settings

GET /config/repo-settings/has-custom?path=/path/to/repo

Returns true if the repo has non-default settings.

Repositories

GET /config/repositories
PUT /config/repositories

Load/save the repositories list.

Prompt Library

GET /config/prompt-library
PUT /config/prompt-library

Load/save prompt entries.

Notes

GET /config/notes
PUT /config/notes

Load/save notes (opaque JSON, shape defined by frontend).

MCP Status

GET /mcp/status

Returns MCP server status (enabled, port, connected clients).

MCP Upstream Status

GET /mcp/upstream-status

Returns status and metrics for all upstream MCP servers (connecting, ready, circuit_open, disabled, failed).

MCP Instructions

GET /mcp/instructions

Returns dynamic server instructions for the MCP bridge binary as {"instructions": "..."}.

Filesystem Endpoints

GET  /fs/list?repoPath=/path/to/repo&subdir=src
GET  /fs/search?repoPath=/path/to/repo&query=main&limit=50
GET  /fs/search-content?repoPath=/path/to/repo&query=foo&caseSensitive=false&useRegex=false&wholeWord=false&limit=200
GET  /fs/read?repoPath=/path/to/repo&file=src/main.rs
GET  /fs/read-external?path=/absolute/path/to/file
POST /fs/write         { "repoPath": "...", "file": "...", "content": "..." }
POST /fs/mkdir         { "repoPath": "...", "dir": "..." }
POST /fs/delete        { "repoPath": "...", "path": "..." }
POST /fs/rename        { "repoPath": "...", "from": "...", "to": "..." }
POST /fs/copy          { "repoPath": "...", "from": "...", "to": "..." }
POST /fs/gitignore     { "repoPath": "...", "pattern": "..." }

Sandboxed filesystem operations for the file manager panel. /fs/read-external reads an arbitrary absolute path (not sandboxed to a repo).

Monitoring Endpoints

Health Check

GET /health

Returns { "status": "ok" }.

Orchestrator Stats

GET /stats

Returns { "active_sessions": N, "max_sessions": 50, "available_slots": N }.

Session Metrics

GET /metrics

Returns { "total_spawned": N, "failed_spawns": N, "bytes_emitted": N, "pauses_triggered": N }.

Local IPs

GET /system/local-ips

Returns list of local network interfaces and addresses.

Local IP (Primary)

GET /system/local-ip

Returns the preferred local IP address (single value).

Watcher Endpoints

Head Watcher

POST   /watchers/head?path=/path/to/repo
DELETE /watchers/head?path=/path/to/repo

Start/stop watching .git/HEAD for branch changes. Browser-only mode.

Repo Watcher

POST   /watchers/repo?path=/path/to/repo
DELETE /watchers/repo?path=/path/to/repo

Start/stop watching .git/ for repository state changes. Browser-only mode.

Directory Watcher

POST   /watchers/dir?path=/path/to/directory
DELETE /watchers/dir?path=/path/to/directory

Start/stop watching a directory (non-recursive) for file changes (create/delete/rename). Emits dir-changed SSE event. Used by File Browser panel for auto-refresh.

Agent Endpoints

Detect All Agents

GET /agents

Returns detected agent binaries and installed IDEs.

Detect Specific Agent

GET /agents/detect?binary=claude

Returns detection result for a specific agent binary.

Detect Installed IDEs

GET /agents/ides

Returns list of installed IDEs.

Prompt Endpoints

Process Prompt

POST /prompt/process
Content-Type: application/json

{ "content": "...", "variables": { ... } }

Substitutes {{var}} placeholders in prompt text.

Extract Variables

POST /prompt/extract-variables
Content-Type: application/json

{ "content": "..." }

Returns list of {{var}} placeholder names found in content.

Plugin Endpoints

List Plugins

GET /plugins/list

Returns array of valid plugin manifests.

Plugin Development Guide

GET /plugins/docs

Returns the complete plugin development reference as {"content": "..."}. AI-optimized documentation covering manifest format, PluginHost API, structured event types, and example plugins.

Plugin Data

GET /api/plugins/:plugin_id/data/*path

Reads a plugin’s stored data file. Returns application/json if content starts with { or [, otherwise text/plain. Returns 404 if the file doesn’t exist. Goes through the same auth middleware as all other routes.

Note: Write and delete operations are only available via Tauri commands (write_plugin_data, delete_plugin_data), not as HTTP endpoints. Data is sandboxed to ~/.config/tuicommander/plugins/{plugin_id}/data/.

Worktree Endpoints

List Worktrees

GET /worktrees

Returns list of managed worktrees.

Create Worktree

POST /worktrees
Content-Type: application/json

{ "base_repo": "/path", "branch_name": "feature-x" }

Worktrees Base Directory

GET /worktrees/dir

Returns the base directory where worktrees are created.

Get Worktree Paths

GET /worktrees/paths?path=/path/to/repo

Returns { "branch-name": "/worktree/path", ... }.

Generate Worktree Name

POST /worktrees/generate-name
Content-Type: application/json

{ "existing_names": ["name1", "name2"] }

Returns a unique worktree name.

Finalize Merged Worktree

POST /worktrees/finalize
Content-Type: application/json

{ "repoPath": "/path/to/repo", "branchName": "feature-x", "action": "archive" }

Finalizes a merged worktree branch. action must be "archive" (moves to archive directory) or "delete" (removes worktree and branch).

Remove Worktree

DELETE /worktrees/:branch?repoPath=/path&deleteBranch=true

Query parameters:

  • repoPath (required) – base repository path
  • deleteBranch (optional, default true) – when true, also deletes the local git branch

Push Notification Endpoints

Get VAPID Public Key

GET /api/push/vapid-key

Returns the VAPID public key for PushManager.subscribe(). No authentication required.

Response: { "publicKey": "<base64url>" }

Returns 404 if push is not enabled.

Subscribe

POST /api/push/subscribe
Content-Type: application/json

{ "endpoint": "https://...", "keys": { "p256dh": "...", "auth": "..." } }

Register a push subscription. Idempotent (same endpoint updates keys).

Push delivery is gated by desktop window focus: notifications for question and session completion events are sent whenever the desktop window is not focused (including when the app is minimized or the user is on another workspace). This avoids duplicate alerts while the user is actively at the desktop, and still wakes the PWA service worker when the phone is locked.

Unsubscribe

DELETE /api/push/subscribe
Content-Type: application/json

{ "endpoint": "https://..." }

Remove a push subscription by endpoint.

Tauri-Only Commands (No HTTP Route)

The following commands are accessible only via the Tauri invoke() bridge in the desktop app. They have no HTTP endpoint.

CommandModuleDescription
get_claude_usage_apiclaude_usage.rsFetch rate-limit usage from Anthropic OAuth API
get_claude_usage_timelineclaude_usage.rsGet hourly token usage timeline from session transcripts
get_claude_session_statsclaude_usage.rsScan session transcripts for aggregated token/session stats
get_claude_project_listclaude_usage.rsList Claude project slugs with session counts
plugin_read_fileplugin_fs.rsRead file as UTF-8 (within $HOME, 10 MB limit)
plugin_read_file_tailplugin_fs.rsRead last N bytes of file, skip partial first line
plugin_list_directoryplugin_fs.rsList filenames in directory (optional glob filter)
plugin_watch_pathplugin_fs.rsStart watching path for changes
plugin_unwatchplugin_fs.rsStop watching a path
plugin_http_fetchplugin_http.rsMake HTTP request (validated against allowed_urls)
plugin_read_credentialplugin_credentials.rsRead credential from system store
fetch_plugin_registryregistry.rsFetch remote plugin registry index
install_plugin_from_zipplugins.rsInstall plugin from local ZIP file
install_plugin_from_urlplugins.rsInstall plugin from HTTPS URL
uninstall_pluginplugins.rsRemove a plugin and all its files
get_agent_mcp_statusagent_mcp.rsCheck MCP config status for an agent
install_agent_mcpagent_mcp.rsInstall TUICommander MCP entry in agent config
remove_agent_mcpagent_mcp.rsRemove TUICommander MCP entry from agent config

Tauri Commands Reference

All commands are invoked from the frontend via invoke(command, args). In browser mode, these map to HTTP endpoints (see HTTP API).

PTY Session Management (pty.rs)

CommandArgsReturnsDescription
create_ptyconfig: PtyConfigString (session ID)Create PTY session
create_pty_with_worktreepty_config, worktree_configWorktreeResultCreate worktree + PTY
write_ptysession_id, data()Write to PTY
resize_ptysession_id, rows, cols()Resize PTY
pause_ptysession_id()Pause reader thread
resume_ptysession_id()Resume reader thread
close_ptysession_id, cleanup_worktree()Close PTY session
can_spawn_sessionboolCheck session limit
get_orchestrator_statsOrchestratorStatsActive/max/available
get_session_metricsJSONSpawn/fail/byte counts
list_active_sessionsVec<ActiveSessionInfo>List all sessions
list_worktreesVec<JSON>List managed worktrees
update_session_cwdsession_id, cwd()Update session working directory (from OSC 7)
get_session_foreground_processsession_idJSONGet foreground process info
get_kitty_flagssession_idu32Get Kitty keyboard protocol flags for session
get_last_promptsession_idOption<String>Get last user-typed prompt from input line buffer
get_shell_statesession_idOption<String>Get current shell state (“busy”, “idle”, or null)
has_foreground_processsession_id: StringboolChecks if a non-shell foreground process is running
debug_agent_detectionsession_id: StringAgentDiagnosticsReturns diagnostic breakdown of agent detection pipeline
set_session_namesession_id, name()Set custom display name for a session

Git Operations (git.rs)

CommandArgsReturnsDescription
get_repo_infopathRepoInfoRepo name, branch, status
get_git_diffpathStringFull git diff
get_diff_statspathDiffStatsAddition/deletion counts
get_changed_filespathVec<ChangedFile>Changed files with stats
get_file_diffpath, fileStringSingle file diff
get_git_branchespathVec<JSON>All branches (sorted)
get_recent_commitspathVec<JSON>Recent git commits
rename_branchpath, old_name, new_name()Rename branch
check_is_main_branchbranchboolIs main/master/develop
get_initialsnameString2-char repo initials
get_merged_branchesrepo_pathVec<String>Branches merged into default branch
get_repo_summaryrepo_pathRepoSummaryAggregate snapshot: worktree paths + merged branches + per-path diff stats in one IPC
get_repo_structurerepo_pathRepoStructureFast phase: worktree paths + merged branches only (Phase 1 of progressive loading)
get_repo_diff_statsrepo_pathRepoDiffStatsSlow phase: per-worktree diff stats + last commit timestamps (Phase 2 of progressive loading)
run_git_commandpath, argsGitCommandResultRun arbitrary git command (success, stdout, stderr, exit_code)
get_git_panel_contextpathGitPanelContextRich context for Git Panel (branch, ahead/behind, staged/changed/stash counts, last commit, rebase/cherry-pick state). Cached 5s TTL.
get_working_tree_statuspathWorkingTreeStatusFull porcelain v2 status: branch, upstream, ahead/behind, stash count, staged/unstaged entries, untracked files
git_stage_filespath, files()Stage files (git add). Path-traversal validated
git_unstage_filespath, files()Unstage files (git restore --staged). Path-traversal validated
git_discard_filespath, files()Discard working tree changes (git restore). Destructive. Path-traversal validated
git_commitpath, message, amend?String (commit hash)Commit staged changes; optional --amend. Returns new HEAD hash
get_commit_logpath, count?, after?Vec<CommitLogEntry>Paginated commit log (default 50, max 500). after is a commit hash for cursor-based pagination
get_stash_listpathVec<StashEntry>List stash entries (index, ref_name, message, hash)
git_stash_applypath, index()Apply stash entry by index
git_stash_poppath, index()Pop stash entry by index
git_stash_droppath, index()Drop stash entry by index
git_stash_showpath, indexStringShow diff of stash entry
git_apply_reverse_patchpath, patch, scope?()Apply a unified diff patch in reverse (git apply --reverse). Used for hunk/line restore. scope="staged" adds --cached. Patch passed via stdin (no temp files). Path-traversal validated
get_file_historypath, file, count?, after?Vec<CommitLogEntry>Per-file commit log following renames (default 50, max 500)
get_file_blamepath, fileVec<BlameLine>Per-line blame: hash, author, author_time (unix), line_number, content
get_branches_detailpathVec<BranchDetail>Rich branch listing: name, ahead/behind, last commit date, tracking upstream, merged status
delete_branchpath, name, force()Delete a local branch. force=false uses safe -d; force=true uses -D. Refuses to delete the current branch or default branch
create_branchpath, name, start_point, checkout()Create a new branch from start_point (defaults to HEAD). checkout=true switches to it immediately
get_recent_branchespath, limitVec<String>Recently checked-out branches from reflog, ordered by recency

Commit Graph (git_graph.rs)

CommandArgsReturnsDescription
get_commit_graphpath, count?Vec<GraphNode>Lane-assigned commit graph for visual rendering. Default 200, max 1000. Returns hash, column, row, color_index (0–7), parents, refs, and connection metadata (from/to col/row) for Bezier curve drawing

GitHub Authentication (github_auth.rs)

CommandArgsReturnsDescription
github_start_loginDeviceCodeResponseStart OAuth Device Flow, returns user/device code
github_poll_logindevice_codePollResultPoll for token; saves to keyring on success
github_logout()Delete OAuth token from keyring, fall back to env/CLI
github_auth_statusAuthStatusCurrent auth: login, avatar, source, scopes
github_disconnect()Disconnect GitHub (clear all tokens from keyring and env cache)
github_diagnosticsJSONDiagnostics: token sources, scopes, API connectivity

GitHub Integration (github.rs)

CommandArgsReturnsDescription
get_github_statuspathGitHubStatusPR + CI for current branch
get_ci_checkspathVec<JSON>CI check details
get_repo_pr_statusespath, include_mergedVec<BranchPrStatus>Batch PR status (all branches)
approve_prrepo_path, pr_numberStringSubmit approving review via GitHub API
merge_pr_via_githubrepo_path, pr_number, merge_methodStringMerge PR via GitHub API
get_all_pr_statusespathVec<BranchPrStatus>Batch PR status for all branches (includes merged)
get_pr_diffrepo_path, pr_numberStringGet PR diff content
fetch_ci_failure_logsrepo_path, run_idStringFetch failure logs from a GitHub Actions run for CI auto-heal
check_github_circuitpathCircuitStateCheck GitHub API circuit breaker state

Worktree Management (worktree.rs)

CommandArgsReturnsDescription
create_worktreebase_repo, branch_nameJSONCreate git worktree
remove_worktreerepo_path, branch_name, delete_branch?()Remove worktree; delete_branch (default true) controls whether the local branch is also deleted. Archive script resolved from config (not IPC).
delete_local_branchrepo_path, branch_name()Delete a local branch (and its worktree if linked). Refuses to delete the default branch. Uses safe git branch -d
check_worktree_dirtyrepo_path, branch_nameboolCheck if a branch’s worktree has uncommitted changes. Returns false if no worktree exists
get_worktree_pathsrepo_pathHashMap<String,String>Worktree paths for repo
get_worktrees_dirStringWorktrees base directory
generate_worktree_name_cmdexisting_namesStringGenerate unique name
list_local_branchespathVec<String>List local branches
checkout_remote_branchrepo_path, branch_name()Check out a remote-only branch as a new local tracking branch
detect_orphan_worktreesrepo_pathVec<String>Detect worktrees in detached HEAD state (branch deleted)
remove_orphan_worktreerepo_path, worktree_path()Remove an orphan worktree by filesystem path (validated against repo)
switch_branchrepo_path, branch_name()Switch main worktree to a different branch (with dirty-state and process checks)
merge_and_archive_worktreerepo_path, branch_nameMergeResultMerge worktree branch into base and archive
finalize_merged_worktreerepo_path, branch_name()Clean up worktree after merge (delete branch + worktree)
list_base_ref_optionsrepo_pathVec<String>List valid base refs for worktree creation
run_setup_scriptrepo_path, worktree_path()Run post-creation setup script in new worktree
generate_clone_branch_name_cmdbase_name, existing_namesStringGenerate hybrid branch name for clone worktree

Configuration (config.rs)

CommandArgsReturnsDescription
load_app_configAppConfigLoad app settings
save_app_configconfig()Save app settings
load_notification_configNotificationConfigLoad notifications
save_notification_configconfig()Save notifications
load_ui_prefsUIPrefsConfigLoad UI preferences
save_ui_prefsconfig()Save UI preferences
load_repo_settingsRepoSettingsMapLoad per-repo settings
save_repo_settingsconfig()Save per-repo settings
check_has_custom_settingspathboolHas non-default settings
load_repo_defaultsRepoDefaultsConfigLoad repo defaults
save_repo_defaultsconfig()Save repo defaults
load_repositoriesJSONLoad saved repositories
save_repositoriesconfig()Save repositories
load_prompt_libraryPromptLibraryConfigLoad prompts
save_prompt_libraryconfig()Save prompts
load_notesJSONLoad notes
save_notesconfig()Save notes
save_note_imagenote_id, data_base64, extensionString (absolute path)Decode base64 image, validate ≤10 MB, write to config_dir()/note-images/<note_id>/<timestamp>.<ext>
delete_note_assetsnote_id()Remove note-images/<note_id>/ directory recursively (no-op if missing)
get_note_images_dirStringReturn config_dir()/note-images/ absolute path
load_keybindingsJSONLoad keybinding overrides
save_keybindingsconfig()Save keybinding overrides
load_agents_configAgentsConfigLoad per-agent run configs
save_agents_configconfig()Save per-agent run configs
load_activityActivityConfigLoad activity dashboard state
save_activityconfig()Save activity dashboard state
load_repo_local_configrepo_pathRepoLocalConfig?Read .tuic.json from repo root; returns null if absent or malformed

Agent Detection (agent.rs)

CommandArgsReturnsDescription
detect_agent_binarybinaryAgentBinaryDetectionCheck binary in PATH
detect_all_agent_binariesVec<AgentBinaryDetection>Detect all known agents
detect_claude_binaryStringDetect Claude binary
detect_installed_idesVec<String>Detect installed IDEs
open_in_apppath, app()Open path in application
spawn_agentpty_config, agent_configString (session ID)Spawn agent in PTY
discover_agent_sessionsession_id, agent_type, cwdOption<String>Discover agent session UUID from filesystem for session-aware resume
verify_agent_sessionagent_type, session_id, cwdboolVerify if a specific agent session file exists on disk (for TUIC_SESSION resume)

AI Chat (ai_chat.rs)

Conversational AI companion with terminal context injection. See docs/user-guide/ai-chat.md for the feature overview.

CommandArgsReturnsDescription
load_ai_chat_configAiChatConfigLoad provider / model / base URL / temperature / context_lines from ai-chat-config.json
save_ai_chat_configconfig()Persist chat config
has_ai_chat_api_keyboolWhether an API key is stored in the OS keyring for the current provider
save_ai_chat_api_keykey: String()Store API key in OS keyring (service tuicommander-ai-chat, user api-key)
delete_ai_chat_api_key()Remove stored API key
check_ollama_statusOllamaStatusProbe GET /api/tags on the configured base URL (default http://localhost:11434/v1/); returns reachable + model list
test_ai_chat_connectionStringValidate API key + base URL with a minimal completion request
list_conversationsVec<ConversationMeta>List persisted conversations (id, title, updated_at, message count)
load_conversationid: StringConversationLoad a saved conversation body
save_conversationconversation: Conversation()Persist a conversation to ai-chat-conversations/<id>.json
delete_conversationid: String()Remove a saved conversation (idempotent)
new_conversation_idStringMint a fresh conversation UUID
stream_ai_chatsession_id, messages, chat_id, on_event: Channel<ChatStreamEvent>()Stream a turn. Events: chunk { text }, end, error { message }, tool_call / tool_result (agent mode). Context assembly pulls VtLogBuffer (capped at context_lines), SessionState, recent ParsedEvents, git context
cancel_ai_chatchat_id: String()Cancel an in-flight stream (idempotent)

AI Agent Loop (ai_agent/commands.rs)

ReAct-style agent loop driving a terminal session with ai_terminal_* tools, plus a Tauri-side query for the per-session knowledge store.

CommandArgsReturnsDescription
start_agent_loopsession_id, goalString (status message)Start a ReAct loop on the given terminal session with the given goal. Errors if an agent is already active for the session.
cancel_agent_loopsession_idStringCancel the active agent loop. Errors if no loop is active.
pause_agent_loopsession_idStringPause the active agent loop between iterations.
resume_agent_loopsession_idStringResume a paused agent loop.
agent_loop_statussession_id{ active: bool, state: AgentState?, session_id }Query whether an agent is active and its current state (running/paused/pending_approval).
approve_agent_actionsession_id, approvedStringApprove or reject the pending destructive command the agent wants to run. Errors if no agent is active.
get_session_knowledgesession_idSessionKnowledgeSummaryLightweight summary for the SessionKnowledgeBar UI: commands count, last 5 outcomes with kind badges, recent errors with error_type, TUI mode indicator, TUI apps seen. Returns an empty summary when the session has no recorded knowledge yet.
list_knowledge_sessionsfilter?: { text?, hasErrors?, since? }, limit?SessionListEntry[]Scan persisted ai-sessions/ and list sessions sorted by most recent activity. Filter by text (matches command/output/intent/error_type), errors-only, or UNIX-seconds since lower bound. limit clamps at 500 (default 100).
get_knowledge_session_detailsession_idSessionDetail?Full command history for one session — reads the in-memory store when active, falls back to disk otherwise. HistoryCommand rows include pre-extracted kind/error_type and the opt-in semantic_intent.

Agent Tools (ai_agent/tools.rs)

12 tools available to the ReAct agent loop and exposed via MCP as ai_terminal_*:

Terminal tools (require session_id):

ToolArgsDescription
read_screensession_id, lines?Read visible terminal text (default 50 lines). Secrets redacted.
send_inputsession_id, commandSend a text command to the PTY (Ctrl-U prefix + \r).
send_keysession_id, keySend a special key (enter, tab, ctrl+c, escape, arrows).
wait_forsession_id, pattern?, timeout_ms?, stability_ms?Wait for regex match or screen stability.
get_statesession_idStructured session metadata (shell_state, cwd, terminal_mode).
get_contextsession_idCompact ~500-char context summary.

Filesystem tools (sandboxed per session via FileSandbox):

ToolArgsDescription
read_filefile_path, offset?, limit?Paginated file read (default 200, max 2000 lines). Binary/10MB rejected. Secrets redacted.
write_filefile_path, contentAtomic create/overwrite (tmp+rename). Sensitive paths flagged.
edit_filefile_path, old_string, new_string, replace_all?Search-and-replace. Must be unique unless replace_all=true.
list_filespattern, path?Glob match (e.g. src/**/*.rs). Max 500 entries.
search_filespattern, path?, glob?, context_lines?Regex search, .gitignore-aware. Max 50 matches with context.
run_commandcommand, timeout_ms?, cwd?Shell command with captured stdout/stderr. Safety-checked. Env sanitized.

MCP OAuth 2.1 (mcp_oauth/commands.rs)

OAuth 2.1 authorization for upstream MCP servers. Full RFC 9728 (Protected Resource Metadata) + RFC 8414 (Authorization Server Discovery) flow with PKCE S256. Completion via the tuic://oauth-callback deep link.

CommandArgsReturnsDescription
start_mcp_upstream_oauthname: StringStartOAuthResponseBegin an OAuth flow for the named upstream. Transitions status to authenticating, returns the authorization URL + AS origin for the consent dialog. PKCE challenge is generated and stored per pending flow
mcp_oauth_callbackcode: String, oauth_state: String()Consume the tuic://oauth-callback?code=…&state=… deep link. Exchanges the code for tokens, persists OAuthTokenSet to the OS keyring, transitions upstream to connecting
cancel_mcp_upstream_oauthname: String()Abort an in-flight OAuth flow. Drops the pending entry and resets upstream status

MCP Upstream Proxy (mcp_upstream_config.rs, mcp_upstream_credentials.rs)

Commands for managing upstream MCP servers proxied through TUICommander’s /mcp endpoint.

CommandArgsReturnsDescription
load_mcp_upstreamsUpstreamMcpConfigLoad upstream config from mcp-upstreams.json
save_mcp_upstreamsconfig: UpstreamMcpConfig()Validate, persist, and hot-reload upstream config. Errors if validation fails
reconnect_mcp_upstreamname: String()Disconnect and reconnect a single upstream by name. Useful after credential changes or transient failures
get_mcp_upstream_statusVec<UpstreamStatus>Get live status of all upstream MCP servers. Status values: connecting, ready, circuit_open, disabled, failed, authenticating, needs_auth
save_mcp_upstream_credentialname: String, token: String()Store a Bearer token for an upstream in the OS keyring
delete_mcp_upstream_credentialname: String()Remove a Bearer token from the OS keyring (idempotent)

UpstreamMcpConfig schema

interface UpstreamMcpConfig {
  servers: UpstreamMcpServer[];
}

interface UpstreamMcpServer {
  id: string;              // Unique UUID, used for config diff tracking
  name: string;            // Namespace prefix — must match [a-z0-9_-]+
  transport: UpstreamTransport;
  enabled: boolean;        // Default: true
  timeout_secs: number;    // Default: 30 (0 = no timeout, HTTP only)
  tool_filter?: ToolFilter; // Optional allow/deny filter
}

type UpstreamTransport =
  | { type: "http"; url: string }
  | { type: "stdio"; command: string; args: string[]; env: Record<string, string> };

interface ToolFilter {
  mode: "allow" | "deny";
  patterns: string[];  // Exact names or trailing-* glob prefix patterns
}

Upstream status values

The live registry exposes status via SSE events (upstream_status_changed). Valid status strings:

ValueMeaning
connectingHandshake in progress
readyTools available
circuit_openCircuit breaker open, backoff active
disabledDisabled in config
failedPermanently failed, manual reconnect required

Agent MCP Configuration (agent_mcp.rs)

CommandArgsReturnsDescription
get_agent_mcp_statusagentAgentMcpStatusCheck MCP config for an agent
install_agent_mcpagentStringInstall TUICommander MCP entry
remove_agent_mcpagentStringRemove TUICommander MCP entry
get_agent_config_pathagentStringGet agent’s MCP config file path

Prompt Processing (prompt.rs)

CommandArgsReturnsDescription
extract_prompt_variablescontentVec<String>Parse {var} placeholders
process_prompt_contentcontent, variablesStringSubstitute variables
resolve_context_variablesrepo_path: StringHashMap<String, String>Resolve git context variables (branch, diff, changed_files, commit_log, etc.) for smart prompt substitution. Best-effort: variables that fail are omitted

Smart Prompt Execution (smart_prompt.rs)

CommandArgsReturnsDescription
execute_headless_promptcommand: String, args: Vec<String>, stdin_content: Option<String>, timeout_ms: u64, repo_path: String, env: Option<HashMap<String,String>>Result<String, String>Spawn a one-shot agent process in argv form (no shell — metacharacters in args are literal). Prompt content piped via stdin. Timeout capped at 5 minutes
execute_shell_scriptscript_content: String, timeout_ms: u64, repo_path: StringResult<String, String>Execute shell script content directly via platform shell (sh/cmd). No agent involved — runs the content as-is. Captures stdout. Timeout capped at 60 seconds

Claude Usage (claude_usage.rs)

CommandArgsReturnsDescription
get_claude_usage_apiUsageApiResponseFetch rate-limit usage from Anthropic OAuth API
get_claude_usage_timelinescope, days?Vec<TimelinePoint>Hourly token usage from session transcripts
get_claude_session_statsscopeSessionStatsAggregated token/session stats from JSONL transcripts
get_claude_project_listVec<ProjectEntry>List project slugs with session counts

scope values: "all" (all projects) or a specific project slug. days defaults to 7.

Uses incremental parsing with a file-size-based cache (claude-usage-cache.json) so only newly appended JSONL data is processed on each call. The cache is persisted across app restarts.

Voice Dictation (dictation/)

CommandArgsReturnsDescription
start_dictation()Start recording
stop_dictation_and_transcribeTranscribeResponseStop + transcribe. Returns {text, skip_reason?, duration_s}
inject_texttextStringApply corrections
get_dictation_statusDictationStatusModel/recording status
get_model_infoVec<ModelInfo>Available models
download_whisper_modelmodel_nameStringDownload model
delete_whisper_modelmodel_nameStringDelete model
get_correction_mapHashMap<String,String>Load corrections
set_correction_mapmap()Save corrections
list_audio_devicesVec<AudioDevice>List input devices
get_dictation_configDictationConfigLoad config
set_dictation_configconfig()Save config
check_microphone_permissionStringCheck macOS microphone TCC permission status
open_microphone_settings()Open macOS System Settings > Privacy > Microphone

Filesystem (fs.rs)

CommandArgsReturnsDescription
resolve_terminal_pathpathStringResolve terminal path
list_directorypathVec<DirEntry>List directory contents
fs_read_filepathStringRead file contents
write_filepath, content()Write file
create_directorypath()Create directory
delete_pathpath()Delete file or directory
rename_pathsrc, dest()Rename/move path
copy_pathsrc, dest()Copy file or directory
fs_transfer_pathsdestDir, paths, mode ("move"|"copy"), allowRecursiveTransferResult { moved, skipped, errors, needs_confirm }Move/copy OS paths into a destination directory. Skips silently on name conflicts; returns needs_confirm=true (no-op) when a source is a directory and allowRecursive=false. Used by the drag-drop handler when dropping files onto a folder in the file browser.
add_to_gitignorepath, pattern()Add pattern to .gitignore
search_filespath, queryVec<SearchResult>Search files by name in directory
search_contentrepoPath, query, caseSensitive?, useRegex?, wholeWord?, limit?()Full-text content search; streams results progressively via content-search-batch events. Binary files and files >1 MB are skipped. Supports cancellation.

Plugin Management (plugins.rs)

CommandArgsReturnsDescription
list_user_pluginsVec<PluginManifest>List valid plugin manifests
get_plugin_readme_pathidOption<String>Get plugin README.md path
read_plugin_dataplugin_id, pathOption<String>Read plugin data file
write_plugin_dataplugin_id, path, content()Write plugin data file
delete_plugin_dataplugin_id, path()Delete plugin data file
install_plugin_from_zippathPluginManifestInstall from local ZIP
install_plugin_from_urlurlPluginManifestInstall from HTTPS URL
uninstall_pluginid()Remove plugin and all files
install_plugin_from_folderpathPluginManifestInstall from local folder
register_loaded_pluginplugin_id()Register a plugin as loaded (for lifecycle tracking)
unregister_loaded_pluginplugin_id()Unregister a plugin (on unload/disable)

Plugin Filesystem (plugin_fs.rs)

CommandArgsReturnsDescription
plugin_read_filepath, plugin_idStringRead file as UTF-8 (within $HOME, 10 MB limit)
plugin_read_file_tailpath, max_bytes, plugin_idStringRead last N bytes of file, skip partial first line
plugin_list_directorypath, pattern?, plugin_idVec<String>List filenames in directory (optional glob filter)
plugin_watch_pathpath, plugin_id, recursive?, debounce_ms?String (watch ID)Start watching path for changes
plugin_unwatchwatch_id, plugin_id()Stop watching a path
plugin_write_filepath, content, plugin_id()Write file within $HOME (path-traversal validated)
plugin_rename_pathsrc, dest, plugin_id()Rename/move path within $HOME (path-traversal validated)

Plugin HTTP (plugin_http.rs)

CommandArgsReturnsDescription
plugin_http_fetchurl, method?, headers?, body?, allowed_urls, plugin_idHttpResponseMake HTTP request (validated against allowed_urls)

Plugin CLI Execution (plugin_exec.rs)

CommandArgsReturnsDescription
plugin_exec_clibinary, args, cwd?, plugin_idStringExecute whitelisted CLI binary, return stdout. Allowed: mdkb. 30s timeout, 5 MB limit.

Plugin Credentials (plugin_credentials.rs)

CommandArgsReturnsDescription
plugin_read_credentialservice_name, plugin_idString?Read credential from system store (Keychain/file)

Plugin Registry (registry.rs)

CommandArgsReturnsDescription
fetch_plugin_registryVec<RegistryEntry>Fetch remote plugin registry index

Watchers

CommandArgsReturnsDescription
start_head_watcherpath()Watch .git/HEAD for branch changes
stop_head_watcherpath()Stop watching .git/HEAD
start_repo_watcherpath()Watch .git/ for repo changes
stop_repo_watcherpath()Stop watching .git/
start_dir_watcherpath()Watch directory for file changes (non-recursive)
stop_dir_watcherpath()Stop watching directory

System (lib.rs)

CommandArgsReturnsDescription
load_configAppConfigAlias for load_app_config
save_configconfig()Alias for save_app_config
hash_passwordpasswordStringBcrypt hash
list_markdown_filespathVec<MarkdownFileEntry>List .md files in dir
read_filepath, fileStringRead file contents
get_mcp_statusJSONMCP server status (no token — use get_connect_url for QR)
get_connect_urlipStringBuild QR connect URL server-side (token stays in backend)
check_update_channelchannelUpdateCheckResultCheck beta/nightly channel for updates (hardcoded URLs, SSRF-safe)
clear_caches()Clear in-memory caches
get_local_ipOption<String>Get primary local IP
get_local_ipsVec<LocalIpEntry>List local network interfaces
regenerate_session_token()Regenerate MCP session token (invalidates all remote sessions)
fetch_update_manifesturlJSONFetch update manifest via Rust HTTP (bypasses WebView CSP)
read_external_filepathStringRead file outside repo (standalone file open)
get_relay_statusJSONCloud relay connection status
get_tailscale_statusTailscaleStateTailscale daemon status (NotInstalled/NotRunning/Running with fqdn, https_enabled)

Global Hotkey

CommandArgsReturnsDescription
set_global_hotkeycombo: Option<String>()Set or clear the OS-level global hotkey
get_global_hotkeyOption<String>Get the currently configured global hotkey

App Logger (app_logger.rs)

CommandArgsReturnsDescription
push_loglevel, source, message()Push entry to ring buffer (survives webview reloads)
get_logslevel?, source?, limit?Vec<LogEntry>Query ring buffer with optional filters
clear_logs()Flush all log entries

Notification Sound (notification_sound.rs)

CommandArgsReturnsDescription
play_notification_soundsound_type()Play notification sound via Rust rodio (types: completion, question, error, info)
block_sleep()Prevent system sleep
unblock_sleep()Allow system sleep

TUIC SDK

The TUIC SDK provides window.tuic inside iframes hosted by TUICommander, enabling plugins and external pages to interact with the host app: open files, read content, launch terminals, copy to clipboard, receive theme updates, and more.

Two Injection Modes

1. Inline HTML Tabs (Plugins)

Plugins use html tabs — TUIC injects the SDK <script> directly into the iframe content. window.tuic is available immediately on load.

// Plugin panel — window.tuic is injected automatically
tuic.open("README.md");                         // relative to active repo
tuic.open("/absolute/path/file.txt", { pinned: true });
tuic.edit("src/App.tsx", { line: 42 });
tuic.terminal(tuic.activeRepo());

See Plugin Authoring Guide for full plugin details.

2. URL Tabs (External Pages)

Tabs created with url load content from a remote server in an iframe. The parent cannot inject scripts into cross-origin iframes, so the page must opt in to the SDK via a postMessage handshake.

Handshake Protocol

┌─────────────┐                            ┌─────────────┐
│  TUIC Host   │                            │  iframe URL  │
│  (parent)    │                            │  (child)     │
└──────┬──────┘                            └──────┬──────┘
       │  iframe onload                            │
       │── tuic:sdk-init ────────────────────────>│
       │── tuic:repo-changed ────────────────────>│
       │── tuic:theme-changed ───────────────────>│
       │                                           │ creates window.tuic
       │                                           │ dispatches "tuic:ready"
       │                                           │
       │  (fallback path — async listeners)        │
       │<── tuic:sdk-request ──────────────────────│
       │── tuic:sdk-init + repo + theme ─────────>│
       │                                           │
       │<── tuic:open, tuic:edit, ... ─────────────│ (on user action)
       │── tuic:get-file-result ─────────────────>│ (async response)
       │── tuic:host-message ────────────────────>│ (push from host)

Both paths are implemented in src/components/PluginPanel/PluginPanel.tsx. The version field carries TUIC_SDK_VERSION so the child can feature-detect.

Step 1: Child Page — Bootstrap Listener

Add this <script> in the <head> of your page, before any framework initialization. It must be synchronous so the listener is registered before the parent’s onload fires.

<script>
(function () {
  window.addEventListener("message", function (e) {
    if (!e.data || e.data.type !== "tuic:sdk-init") return;

    window.tuic = {
      version: "1.0",
      open: function (path, opts) {
        parent.postMessage({ type: "tuic:open", path: path, pinned: !!(opts && opts.pinned) }, "*");
      },
      edit: function (path, opts) {
        parent.postMessage({ type: "tuic:edit", path: path, line: (opts && opts.line) || 0 }, "*");
      },
      terminal: function (repoPath) {
        parent.postMessage({ type: "tuic:terminal", repoPath: repoPath }, "*");
      }
    };

    window.dispatchEvent(new Event("tuic:ready"));
  });
})();
</script>

Note: For URL-mode pages, only the basic methods (open, edit, terminal) are shown above. To use the full SDK (activeRepo, getFile, theme, etc.), copy the complete SDK from src/components/PluginPanel/tuicSdk.ts or use the inline HTML mode.

Step 2: Child Page — React to SDK Availability

Use the tuic:ready event to update your UI (e.g., show an “Open in TUIC” button):

// Alpine.js example
Alpine.data("myApp", () => ({
  _tuicReady: false,
  get hasTuic() { return this._tuicReady; },

  init() {
    window.addEventListener("tuic:ready", () => { this._tuicReady = true; });
    // If SDK was already initialized before Alpine mounted
    if (window.tuic) this._tuicReady = true;
  },

  tuicOpen(filePath) {
    if (window.tuic) window.tuic.open(filePath, { pinned: true });
  }
}));

API Reference

Files

tuic.open(path, opts?)

Open a file in a TUIC tab.

ParamTypeDescription
pathstringFile path — relative (resolved against active repo) or absolute
opts.pinnedbooleanPin the tab (default: false)

tuic.edit(path, opts?)

Open a file in the external editor.

ParamTypeDescription
pathstringFile path — relative or absolute
opts.linenumberLine number to jump to (default: 0)

tuic.getFile(path): Promise<string>

Read a file’s text content from the active repo.

ParamTypeDescription
pathstringFile path — relative or absolute

Returns a Promise that resolves with the file content string, or rejects with an Error if the file is not found, the path escapes the repo root, or no active repo is set.

tuic.getFile("package.json")
  .then(content => JSON.parse(content))
  .catch(err => console.error("Cannot read:", err.message));

Path Resolution

All file methods (open, edit, getFile) accept both relative and absolute paths:

  • Relative paths (e.g., "README.md", "src/App.tsx") are resolved against the active repository root.
  • Absolute paths (e.g., "/Users/me/code/repo/file.ts") are matched against known repositories (longest prefix wins).
  • Path traversal (../) that escapes the repo root is blocked and returns an error.
  • ./ prefixes are supported and normalized.
tuic.open("README.md");                    // → /active/repo/README.md
tuic.open("src/../README.md");             // → /active/repo/README.md
tuic.open("/Users/me/repo/file.ts");       // → absolute, matched to repo
tuic.getFile("../../../etc/passwd");       // → rejected (traversal)

HTML <a> tags with tuic:// href are automatically intercepted:

<a href="tuic://open/README.md">View README</a>
<a href="tuic://edit/src/main.rs?line=42">Edit main.rs:42</a>
<a href="tuic://terminal?repo=/path/to/repo">Open terminal</a>

Link pathnames are treated as relative paths (the leading / from URL parsing is stripped).

Repository

tuic.activeRepo(): string | null

Returns the path of the currently active repository, or null if none is active.

var repo = tuic.activeRepo();
// "/Users/me/code/myproject" or null

tuic.onRepoChange(callback)

Register a listener that fires when the active repo changes.

ParamTypeDescription
callback(repoPath: string | null) => voidCalled with the new active repo path

tuic.offRepoChange(callback)

Unregister a previously registered repo-change listener.

tuic.terminal(repoPath)

Open a terminal in the given repository.

ParamTypeDescription
repoPathstringRepository root path (absolute)

UI Feedback

tuic.toast(title, opts?)

Show a native toast notification in the host app.

ParamTypeDescription
titlestringToast title (required)
opts.messagestringOptional body text
opts.level"info" | "warn" | "error"Severity (default: "info")
opts.soundbooleanPlay a notification sound (default: false). Each level has a distinct tone: info = soft blip, warn = double beep, error = descending sweep.
tuic.toast("Import complete", { message: "42 items imported" });
tuic.toast("Rate limited", { message: "Try again in 30s", level: "warn", sound: true });

tuic.clipboard(text)

Copy text to the system clipboard. Works from sandboxed iframes (which cannot access navigator.clipboard directly).

ParamTypeDescription
textstringText to copy

Messaging

tuic.send(data)

Send structured data to the host. The host receives it via pluginRegistry.handlePanelMessage().

ParamTypeDescription
dataanyJSON-serializable payload

tuic.onMessage(callback)

Register a listener for messages pushed from the host.

ParamTypeDescription
callback(data: any) => voidCalled with the message payload

tuic.offMessage(callback)

Unregister a previously registered message listener.

Theme

tuic.theme: object | null

Read-only property containing the current theme as a key-value object. Keys are camelCase versions of CSS custom properties (e.g., --bg-primarybgPrimary).

var theme = tuic.theme;
// { bgPrimary: "#1e1e2e", fgPrimary: "#cdd6f4", accent: "#89b4fa", ... }

tuic.onThemeChange(callback)

Register a listener that fires when the host theme changes.

ParamTypeDescription
callback(theme: object) => voidCalled with the new theme object

tuic.offThemeChange(callback)

Unregister a previously registered theme-change listener.

Version

tuic.version: string

The SDK version string (currently "1.0").

Testing the SDK

An interactive test page is included at docs/examples/sdk-test.html. It runs automatic verification of all SDK methods and provides buttons for interactive testing.

How to launch it

From an AI agent (Claude Code, etc.):

Use the TUIC MCP ui tool to open it as an inline HTML tab:

mcp__tuicommander__ui action=tab id="sdk-test" title="SDK Test Suite" html="<contents of docs/examples/sdk-test.html>" pinned=false focus=true

From a plugin:

Register a plugin that serves the HTML content as a panel tab. The SDK is injected automatically into inline HTML tabs.

From JavaScript (dev console or app code):

// Read the file and open as a tab
const html = await invoke("fs_read_file", { repoPath: "/path/to/tuicommander", file: "docs/examples/sdk-test.html" });
mdTabsStore.addHtml("sdk-test", "SDK Test Suite", html);

The test page verifies:

  • SDK presence and version
  • activeRepo() return value
  • onRepoChange listener registration
  • Theme delivery and onThemeChange
  • onMessage listener registration
  • getFile("README.md") reads file content
  • getFile("../../../etc/passwd") is blocked by traversal guard

Interactive buttons test: open, edit, terminal, toast (all levels), clipboard, getFile, and send.

Timing Notes

The <script> bootstrap in the child page must be synchronous and in <head> to guarantee the message listener is registered before the parent’s iframe.onload fires tuic:sdk-init. If your page loads the bootstrap asynchronously (e.g., as an ES module), there is a race condition — the init message may arrive before the listener exists.

If you cannot guarantee synchronous loading, implement a retry: have the child send { type: "tuic:sdk-request" } to the parent on DOMContentLoaded (or whenever the listener is registered), and the parent will respond with tuic:sdk-init. This fallback is fully supported by the host.

Source Files

FileDescription
src/components/PluginPanel/tuicSdk.tsSDK script injected into iframes
src/components/PluginPanel/PluginPanel.tsxHost-side message handlers
src/components/PluginPanel/resolveTuicPath.tsPath resolution (relative + traversal guard)
docs/examples/sdk-test.htmlInteractive test/example page

Plugin Authoring Guide

TUICommander uses an Obsidian-style plugin system. Plugins extend the Activity Center (bell dropdown), watch terminal output, and interact with app state. Plugins can be built-in (compiled with the app) or external (loaded at runtime from the user’s plugins directory).

Quick Start: External Plugin

  1. Create a directory: ~/.config/tuicommander/plugins/my-plugin/
  2. Create manifest.json:
{
  "id": "my-plugin",
  "name": "My Plugin",
  "version": "1.0.0",
  "minAppVersion": "0.3.0",
  "main": "main.js"
}

Note: All manifest fields use camelCase (minAppVersion, agentTypes, contentUri) — this matches the Rust serde serialization format. Do not use snake_case.

// ✅ Correct
{ "minAppVersion": "0.5.0", "agentTypes": ["claude"] }
// ❌ Wrong
{ "min_app_version": "0.5.0", "agent_types": ["claude"] }
  1. Create main.js (ES module with default export):
const PLUGIN_ID = "my-plugin";

export default {
  id: PLUGIN_ID,
  onload(host) {
    host.registerSection({
      id: "my-section",
      label: "MY SECTION",
      priority: 30,
      canDismissAll: false,
    });

    host.registerOutputWatcher({
      pattern: /hello (\w+)/,
      onMatch(match, sessionId) {
        host.addItem({
          id: `hello:${match[1]}`,
          pluginId: PLUGIN_ID,
          sectionId: "my-section",
          title: `Hello ${match[1]}`,
          icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="8" r="6"/></svg>',
          dismissible: true,
        });
      },
    });
  },
  onunload() {},
};
  1. Restart the app (or save the file — hot reload will pick it up).

Architecture

PTY output ──> pluginRegistry.processRawOutput()
                  |
                  +-- LineBuffer (reassemble lines)
                  +-- stripAnsi (clean ANSI codes)
                  +-- dispatchLine() --> OutputWatcher.onMatch()
                                              |
                                              +-- host.addItem() --> Activity Center bell
                                                                           |
                                                              user clicks item
                                                                           |
                                              markdownProviderRegistry.resolve(contentUri)
                                                                           |
                                                              MarkdownTab renders content

Tauri OutputParser --> pluginRegistry.dispatchStructuredEvent(type, payload, sessionId)
                            |
                            +-- structuredEventHandler(payload, sessionId)

Plugin Lifecycle

  1. Discovery — Rust list_user_plugins scans ~/.config/tuicommander/plugins/ for manifest.json files
  2. Validation — Frontend validates manifest fields and minAppVersion
  3. Importimport("plugin://my-plugin/main.js") loads the module via the custom URI protocol
  4. Module check — Default export must have id, onload, onunload
  5. RegisterpluginRegistry.register(plugin, capabilities) calls plugin.onload(host)
  6. Active — Plugin receives PTY lines, structured events, and can use the PluginHost API
  7. Hot reload — File changes emit plugin-changed events; the plugin is unregistered and re-imported
  8. Unloadplugin.onunload() is called, then all registrations are auto-disposed

Crash Safety

Every boundary is wrapped in try/catch:

  • import() — syntax errors or missing exports are caught
  • Module validation — missing id, onload, or onunload logs an error and skips the plugin
  • plugin.onload() — if it throws, partial registrations are cleaned up automatically
  • Watcher/handler dispatch — exceptions are caught and logged, other plugins continue

A broken plugin produces a console error and is skipped. The app always continues.

Manifest Reference

File: ~/.config/tuicommander/plugins/{id}/manifest.json

FieldTypeRequiredDescription
idstringyesMust match the directory name
namestringyesHuman-readable display name
versionstringyesPlugin semver (e.g. "1.0.0")
minAppVersionstringyesMinimum TUICommander version required
mainstringyesEntry point filename (e.g. "main.js")
descriptionstringnoShort description
authorstringnoAuthor name
capabilitiesstring[]noTier 3/4 capabilities needed (defaults to [])
allowedUrlsstring[]noURL patterns allowed for net:http (e.g. ["https://api.example.com/*"])
agentTypesstring[]noAgent types this plugin targets (e.g. ["claude"]). Omit or [] for universal plugins.
binariesstring[]noCLI binaries this plugin may execute via exec:cli (e.g. ["rtk", "mdkb"])

Validation Rules

  • id must match the directory name exactly
  • id must not be empty
  • main must not contain path separators or ..
  • All capabilities must be known strings (see Capabilities section)
  • minAppVersion must be <= the current app version (semver comparison)

Plugin Interface

interface TuiPlugin {
  id: string;
  onload(host: PluginHost): void;
  onunload(): void;
}

The onload function receives a PluginHost object — this is your entire API surface. External plugins cannot import app internals; everything goes through host.

PluginHost API Reference

Tier 0: Logging (always available)

host.log(level, message, data?) -> void

Write to the plugin’s dedicated log ring buffer (max 500 entries). Viewable in Settings > Plugins > click “Logs” on any plugin row.

host.log("info", "Plugin initialized");
host.log("error", "Failed to process", { code: 404 });

Levels: "debug", "info", "warn", "error". The optional data parameter accepts any JSON-serializable value and is displayed alongside the message.

Errors thrown inside onload, onunload, output watchers, and structured event handlers are automatically captured to the plugin’s log. Use host.log() for additional diagnostic output. Error count badges appear on plugins with recent errors in the Settings panel.

Tier 1: Activity Center + Watchers + Providers (always available)

All register*() methods return a Disposable with a dispose() method. You do not need to call dispose() manually — all registrations are automatically disposed when onunload() is called (including during hot reload). Only call dispose() if you need to dynamically remove a registration while the plugin is still running.

host.registerSection(section) -> Disposable

Adds a section heading to the Activity Center dropdown.

host.registerSection({
  id: "my-section",        // Must match sectionId in addItem()
  label: "MY SECTION",     // Displayed as section header
  priority: 30,            // Lower number = higher position
  canDismissAll: false,     // Show "Dismiss All" button?
});

host.registerOutputWatcher(watcher) -> Disposable

Watches every PTY output line (after ANSI stripping and line reassembly).

host.registerOutputWatcher({
  pattern: /Deployed: (\S+) to (\S+)/,
  onMatch(match, sessionId) {
    // match[0] = full match, match[1] = first capture group, etc.
    // sessionId = the PTY session that produced the line
    host.addItem({ ... });
  },
});

Rules:

  • onMatch must be synchronous and fast (< 1ms) — it’s in the PTY hot path
  • pattern.lastIndex is reset before each test (safe to use global flag, but unnecessary)
  • Input is ANSI-stripped but may contain Unicode (checkmarks, arrows, emoji)
  • Arguments are positional: onMatch(match, sessionId) — NOT destructured

host.registerStructuredEventHandler(type, handler) -> Disposable

Handles typed events from the Rust OutputParser.

host.registerStructuredEventHandler("plan-file", (payload, sessionId) => {
  const { path } = payload as { path: string };
  host.addItem({ ... });
});

See Structured Event Types for all types and payload shapes.

host.registerMarkdownProvider(scheme, provider) -> Disposable

Provides content for a URI scheme when the user clicks an ActivityItem.

host.registerMarkdownProvider("my-scheme", {
  async provideContent(uri) {
    const id = uri.searchParams.get("id");
    if (!id) return null;
    try {
      return await host.invoke("read_file", { path: dir, file: name });
    } catch {
      return null;
    }
  },
});

host.addItem(item) / host.removeItem(id) / host.updateItem(id, updates)

Manage activity items:

host.addItem({
  id: "deploy:api:prod",       // Unique identifier
  pluginId: "my-plugin",       // Must match your plugin id
  sectionId: "my-section",     // Must match your registered section
  title: "api-server",         // Primary text
  subtitle: "Deployed to prod", // Secondary text (optional)
  icon: '<svg .../>',          // Inline SVG with fill="currentColor"
  iconColor: "#3fb950",        // Optional CSS color for the icon
  dismissible: true,
  contentUri: "my-scheme:detail?id=api",  // Opens in MarkdownTab on click
  // OR: onClick: () => { ... },          // Mutually exclusive with contentUri
});

host.updateItem("deploy:api:prod", { subtitle: "Rolled back" });
host.removeItem("deploy:api:prod");

Tier 2: Read-Only App State (always available)

host.getActiveRepo() -> RepoSnapshot | null

const repo = host.getActiveRepo();
// { path: "/Users/me/project", displayName: "project", activeBranch: "main", worktreePath: null }

host.getRepos() -> RepoListEntry[]

const repos = host.getRepos();
// [{ path: "/Users/me/project", displayName: "project" }, ...]

host.getActiveTerminalSessionId() -> string | null

const sessionId = host.getActiveTerminalSessionId();

host.getRepoPathForSession(sessionId) -> string | null

Resolves which repository owns a given terminal session by searching all repos and branches for a terminal matching the session ID. Returns null if the session is not associated with any repository (e.g. a standalone terminal or an unknown session ID). Useful in output watcher callbacks where sessionId is provided but you need the repo context.

host.registerOutputWatcher({
  pattern: /Deployed: (\S+)/,
  onMatch(match, sessionId) {
    const repoPath = host.getRepoPathForSession(sessionId);
    if (!repoPath) return; // session not tied to a repo
    // repoPath = "/Users/me/project"
  },
});

host.getClaudeProjectDir(repoPath) -> Promise<string | null>

Resolves a repository path to the absolute path of its Claude Code project directory (~/.claude/projects/<slug>). The slug encoding is handled by the Rust side — plugins should use this instead of constructing paths manually. Requires "fs:read" capability.

const projectDir = await host.getClaudeProjectDir("/Users/me/my-project");
// → "/Users/me/.claude/projects/-Users-me-my-project"
const files = await host.listDirectory(projectDir, "*.jsonl", { sortBy: "mtime" });

host.getPrNotifications() -> PrNotificationSnapshot[]

const prs = host.getPrNotifications();
// [{ id, repoPath, branch, prNumber, title, type }, ...]

host.getSettings(repoPath) -> RepoSettingsSnapshot | null

const settings = host.getSettings("/Users/me/project");
// { path, displayName, baseBranch: "main", color: "#3fb950" }

host.getTerminalState() -> TerminalStateSnapshot | null

Returns the active terminal’s state snapshot.

const state = host.getTerminalState();
// { sessionId, shellState: "busy"|"idle"|null, agentType: "claude"|null,
//   agentActive: boolean, awaitingInput: "question"|null, repoPath }

host.onStateChange(callback) -> Disposable

Register a callback for terminal/branch state changes. Fires on agent start/stop, branch change, shell state change, and awaiting-input change.

const sub = host.onStateChange((event) => {
  // event.type: "agent-started" | "agent-stopped" | "branch-changed"
  //           | "shell-state-changed" | "awaiting-input-changed"
  // event.sessionId, event.terminalId, event.detail (branch name for branch-changed)
});
// sub.dispose() to unsubscribe

Tier 2b: Git Read (capability-gated)

These methods require declaring "git:read" in manifest.json. They provide read-only access to git repository state.

host.getGitBranches(repoPath) -> Promise<Array<{ name, isCurrent }>>

const branches = await host.getGitBranches("/Users/me/project");
// [{ name: "main", isCurrent: true }, { name: "feature/x", isCurrent: false }]

host.getRecentCommits(repoPath, count?) -> Promise<Array<{ hash, message, author, date }>>

const commits = await host.getRecentCommits("/Users/me/project", 5);
// [{ hash: "abc1234", message: "fix: bug", author: "name", date: "2026-02-25" }]

host.getGitDiff(repoPath, scope?) -> Promise

const diff = await host.getGitDiff("/Users/me/project", "staged");
// Returns unified diff string

Tier 3: Write Actions (capability-gated)

These methods require declaring capabilities in manifest.json. Calling without the required capability throws PluginCapabilityError.

host.writePty(sessionId, data) -> Promise

Sends raw bytes to a terminal session. Requires "pty:write" capability.

Prefer sendAgentInput() for user input. writePty sends raw data — it does not handle Enter key semantics for Ink-based agents. Use it only when you need exact byte control.

await host.writePty(sessionId, "\x03"); // Send Ctrl-C

host.sendAgentInput(sessionId, text) -> Promise

Sends user input to an agent session with correct Enter handling. Requires "pty:write" capability.

Ink-based agents (Claude Code, Codex, etc.) run in raw mode and need Ctrl-U + text in one write, then \r in a separate write. Shell sessions receive everything in a single write. This method handles both cases automatically based on the detected agent type.

await host.sendAgentInput(sessionId, "y");       // confirm a prompt
await host.sendAgentInput(sessionId, "explain this code"); // send a message

host.openMarkdownPanel(title, contentUri) -> void

Opens a virtual markdown tab and shows the panel. Requires "ui:markdown" capability.

host.openMarkdownPanel("CI Report", "my-scheme:report?id=123");

host.openMarkdownFile(absolutePath) -> void

Opens a local markdown file in the markdown panel. Requires "ui:markdown" capability. The path must be absolute. This is useful for plugins that ship a README.md or other documentation files.

// Open the plugin's own README
host.openMarkdownFile("/Users/me/.config/tuicommander/plugins/my-plugin/README.md");

host.playNotificationSound(sound?) -> Promise

Plays a notification sound. Requires "ui:sound" capability.

ParameterTypeDefaultDescription
soundstring"info"One of: "question", "error", "completion", "warning", "info"
await host.playNotificationSound("error");      // CI failure, build error
await host.playNotificationSound("question");    // input prompt, awaiting user
await host.playNotificationSound("completion");  // task finished
await host.playNotificationSound();              // defaults to "info"

Tier 3b: Filesystem Operations (capability-gated)

These methods provide sandboxed filesystem access. All paths must be absolute and within the user’s home directory ($HOME).

host.readFile(absolutePath) -> Promise

Read a file’s content as UTF-8 text. Maximum file size: 10 MB. Requires "fs:read" capability.

const content = await host.readFile("/Users/me/.claude/projects/foo/conversation.jsonl");

host.listDirectory(path, pattern?, options?) -> Promise<string[]>

List filenames in a directory, optionally filtered by a glob pattern. Returns filenames only (not full paths). Requires "fs:list" capability.

Options:

  • sortBy: "name" (default, alphabetical) or "mtime" (newest first). Use "mtime" to efficiently find the most recently modified file when the directory contains many historical entries.
const files = await host.listDirectory("/Users/me/.claude/projects/foo", "*.jsonl");
// ["conversation-1.jsonl", "conversation-2.jsonl"]

// Find the currently active session JSONL among 100+ historical ones:
const recent = await host.listDirectory(dir, "*.jsonl", { sortBy: "mtime" });
const activeFile = recent[0]; // most recently written

host.watchPath(path, callback, options?) -> Promise

Watch a path for filesystem changes. Emits batched events after a debounce period. Requires "fs:watch" capability.

const watcher = await host.watchPath(
  "/Users/me/.claude/projects/foo",
  (events) => {
    for (const event of events) {
      console.log(event.type, event.path); // "create" | "modify" | "delete"
    }
  },
  { recursive: true, debounceMs: 500 },
);

// Later: stop watching
watcher.dispose();

Options:

  • recursive — Watch subdirectories (default: false)
  • debounceMs — Debounce window in milliseconds (default: 300)

FsChangeEvent:

interface FsChangeEvent {
  type: "create" | "modify" | "delete";
  path: string;
}

host.writeFile(absolutePath, content) -> Promise

Write content to a file within $HOME. Creates parent directories if needed. Refuses to overwrite directories. Max 10 MB. Requires "fs:write" capability.

await host.writeFile("/Users/me/project/stories/new-story.md", "---\nstatus: pending\n---\n# New Story");

host.renamePath(from, to) -> Promise

Rename or move a file within $HOME. Both paths must be absolute. Source must exist. Creates parent directories for destination if needed. Requires "fs:rename" capability.

await host.renamePath(
  "/Users/me/project/stories/old-name.md",
  "/Users/me/project/stories/new-name.md",
);

Tier 3c: Status Bar Ticker (capability-gated)

The status bar has a shared ticker area that rotates messages from multiple plugins. Messages are grouped by priority tier:

TierPriorityBehavior
Low< 10Shown only in the popover, not in rotation
Normal10–99Auto-rotates every 5s in the ticker area
Urgent>= 100Pinned — pauses rotation until cleared

Users can click the counter badge (e.g. 1/3 ▸) to cycle manually, or right-click the ticker to see all active messages in a popover.

host.setTicker(options) -> void

Set a ticker message in the shared status bar ticker. Preferred API — supports source labels. If a message with the same id from this plugin already exists, it is replaced. Requires "ui:ticker" capability.

host.setTicker({
  id: "my-status",
  text: "Processing: 42%",
  label: "MyPlugin",         // Shown as "MyPlugin · Processing: 42%"
  icon: '<svg viewBox="0 0 16 16" fill="currentColor">...</svg>',
  priority: 10,
  ttlMs: 60000,
  onClick: () => { /* optional click handler */ },
});

Options:

  • id — Unique message identifier (scoped to your plugin). Reusing an id replaces the previous message.
  • text — Message text displayed in the ticker rotation.
  • label — Optional human-readable source label shown before the text (e.g. "Usage").
  • icon — Optional inline SVG icon.
  • priority — Priority tier (see table above). Default: 0.
  • ttlMs — Auto-expire after N milliseconds. 0 = persistent (must be removed manually). Default: 60000.
  • onClick — Optional callback invoked when the user clicks the message text.

host.clearTicker(id) -> void

Remove a ticker message by id. Requires "ui:ticker" capability.

host.clearTicker("my-status");

host.postTickerMessage(options) -> void (legacy)

Alias for setTicker without label support. Prefer setTicker for new plugins.

host.removeTickerMessage(id) -> void (legacy)

Alias for clearTicker.

Tier 3d: Panel UI (capability-gated)

host.openPanel(options) -> PanelHandle

Open an HTML panel in a sandboxed iframe tab. Returns a handle for updating content or closing the panel. If a panel with the same id is already open, it will be activated and updated. Requires "ui:panel" capability.

const panel = host.openPanel({
  id: "my-dashboard",
  title: "Dashboard",
  html: "<html><body><h1>Hello</h1></body></html>",
  onMessage(data) {
    // Receive structured messages from the iframe
    console.log("Got message from iframe:", data);
    // Send response back
    panel.send({ type: "response", ok: true });
  },
});

// Update content later
panel.update("<html><body><h1>Updated</h1></body></html>");

// Send a message to the iframe at any time
panel.send({ type: "refresh", items: [...] });

// Close the panel
panel.close();

Inside the iframe:

<script>
  // Send message to host
  window.parent.postMessage({ type: "save", config: { ... } }, "*");

  // Receive messages from host
  window.addEventListener("message", (e) => {
    if (e.data?.type === "response") {
      console.log("Host says:", e.data.ok);
    }
  });
</script>

CSS Base Stylesheet + Theme Injection: Every plugin panel iframe receives two automatic CSS injections:

  1. Base stylesheet (pluginBaseStyles.ts) — a complete design foundation with reset, typography, buttons, inputs, cards, tables, badges, toasts, scrollbars, and empty states. All values use CSS custom properties from the app theme. Plugins get a polished, consistent look without writing any CSS.

  2. Theme variables — all CSS custom properties from the app’s :root are injected (e.g. --bg-primary, --fg-primary, --border, --accent, --error, --warning, --success, --text-on-accent). These match the user’s active theme.

Design strategy: Write minimal plugin-specific CSS that overrides the base. The base provides:

Base classDescription
bodyThemed background, font, color
button, .btnDefault button with hover/active states
button.primary, .btn-primaryAccent-colored button
button.danger, .btn-dangerError-colored button
input, textarea, selectThemed form controls with focus ring
.cardBordered container with hover elevation
table, th, tdStyled table with hover rows
.badgeInline label (combine with .badge-p1, .badge-error, .badge-success, .badge-accent, .badge-warning, .badge-muted)
label, .hintForm labels and help text
.filter-barFlex row for search/filter UI
.empty-stateCentered placeholder with .hint
.toast, .toast.error, .toast.successFixed-position notification (add .show to display)
h1h4Themed headings
code, a, hr, smallThemed inline elements
::-webkit-scrollbarStyled scrollbar matching the app

Example — minimal plugin CSS:

<style>
  /* Only what's specific to this plugin */
  body { padding: 16px; }
  .my-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
</style>

Dashboard layout classes. For analytics/status dashboards, the base stylesheet also ships a .dashboard/.dash-* class family that mirrors the built-in Claude Usage dashboard. Use them instead of inventing layout CSS — see docs/plugins-style.md for the full guide, checklist, and class reference.

host.registerDashboard(options) -> Disposable

Register a one-click entry point for the plugin’s dashboard. When registered, Settings → Plugins shows a Dashboard button in the plugin row that calls options.open() and automatically closes the Settings panel so the dashboard becomes visible.

host.registerDashboard({
  label: "My Plugin",       // optional, defaults to "Dashboard"
  icon: MY_PLUGIN_ICON,     // optional inline SVG string
  open: () => openDashboard(host),
});

A plugin may only register one dashboard — calling registerDashboard a second time replaces the previous entry. Dispose the returned handle in onunload (or rely on automatic cleanup via the plugin’s disposable tracker).

host.registerCommand(options) -> Disposable

Register a plugin command that users can bind to a keyboard shortcut. The command appears in Settings → Keyboard Shortcuts under a dedicated “Plugin Commands” section and can be rebound by the user. The action name is auto-namespaced as plugin:<pluginId>:<id>.

host.registerCommand({
  id: "open-dashboard",          // unique per plugin
  title: "Open My Dashboard",    // label in the Shortcuts UI
  defaultShortcut: "Cmd+Shift+M",// optional — leave unbound by default
  run: () => openDashboard(host),
});

If defaultShortcut conflicts with an existing binding (built-in or another plugin), the command is registered but left unbound; a warning is logged and the user can pick a free combo via Settings. The handle returned is automatically tracked and released on plugin unload.

All standard elements (buttons, inputs, tables) will look correct automatically.

Available CSS variables (from the app’s active theme):

VariableUsage
--bg-primaryMain canvas
--bg-secondarySidebar-level surfaces
--bg-tertiaryInputs, elevated surfaces
--bg-highlightHover states
--fg-primaryPrimary text
--fg-secondaryLabels, secondary text
--fg-mutedTertiary text
--accentLinks, primary actions
--accent-hoverHover on accent
--successPositive states
--warningCaution states
--errorError states
--borderAll borders
--text-on-accentText on colored backgrounds
--text-on-errorText on error backgrounds
--text-on-successText on success backgrounds

Security: The iframe uses sandbox="allow-scripts" without allow-same-origin, blocking access to Tauri IPC and the parent page DOM. The close-panel message type is handled as a system message; all other messages are routed to the onMessage callback.

TUIC SDK (window.tuic)

Every plugin iframe automatically receives the TUIC SDK — a lightweight JavaScript API for host integration. The SDK is injected alongside the base CSS and theme variables.

Feature detection:

if (window.tuic) {
  // Running inside TUICommander — SDK is available
  console.log("TUIC SDK version:", window.tuic.version);
}

Programmatic API:

MethodDescription
tuic.versionSDK version string (e.g. "1.0")
tuic.open(path, opts?)Open a markdown file in a new tab. path is absolute. opts.pinned pins the tab.
tuic.terminal(repoPath)Open a new terminal in the given repository.
// Open a file
tuic.open("/Users/me/myrepo/README.md");

// Open a pinned file
tuic.open("/Users/me/myrepo/docs/guide.md", { pinned: true });

// Open a terminal in a repo
tuic.terminal("/Users/me/myrepo");

Link interception: Standard HTML links with tuic:// scheme are intercepted automatically — no JavaScript required:

<!-- Opens a markdown file -->
<a href="tuic://open/Users/me/myrepo/README.md">View README</a>

<!-- Opens a pinned markdown file -->
<a href="tuic://open/Users/me/myrepo/docs/guide.md" data-pinned>Pinned Guide</a>

<!-- Opens a terminal -->
<a href="tuic://terminal?repo=/Users/me/myrepo">Open Terminal</a>

URL format:

URLAction
tuic://open/<absolute-path>Open file in markdown tab
tuic://terminal?repo=<repo-path>Open terminal in repository

Security: Paths are validated against the list of known repositories. Paths outside any registered repo are rejected with a warning. The SDK runs inside the sandbox and communicates with the host exclusively via postMessage.

Tier 3e: Sidebar Plugin Panels (capability-gated)

host.registerSidebarPanel(options) -> SidebarPanelHandle

Register a collapsible panel section in the sidebar, displayed below the branch list for each repo. Requires "ui:sidebar" capability.

Panels display structured data (not HTML) — the app renders items natively for visual consistency with the rest of the sidebar.

interface SidebarPanelOptions {
  id: string;              // Unique panel ID (scoped to plugin)
  label: string;           // Section header text
  icon?: string;           // Inline SVG for header
  priority?: number;       // Lower = higher in sidebar (default 100)
  collapsed?: boolean;     // Initial collapsed state (default true)
}

interface SidebarPanelHandle {
  setItems(items: SidebarItem[]): void;     // Replace all items
  setBadge(text: string | null): void;      // Header badge (e.g. "3")
  dispose(): void;                          // Remove panel
}

interface SidebarItem {
  id: string;              // Unique item ID (scoped to panel)
  label: string;           // Primary text
  subtitle?: string;       // Secondary text (smaller, muted)
  icon?: string;           // Inline SVG (fill="currentColor")
  iconColor?: string;      // CSS color
  onClick?: () => void;    // Click handler
  contextMenu?: SidebarItemAction[];  // Right-click actions
}

interface SidebarItemAction {
  label: string;
  action: () => void;
  disabled?: boolean;
}

Example:

const panel = host.registerSidebarPanel({
  id: "active-plans",
  label: "ACTIVE PLANS",
  icon: '<svg ...>...</svg>',
  priority: 10,
  collapsed: false,
});

panel.setItems([
  { id: "plan-1", label: "Feature Plan", subtitle: "In Progress · M", onClick: () => openPlan() },
]);
panel.setBadge("1");

Behavior:

  • Panels appear inside RepoSection, below branches, only when the repo is expanded
  • Items are rendered as native sidebar list items (same style as branches)
  • Right-click on items shows a context menu with plugin-defined actions
  • Badge appears as a small counter pill on the section header
  • On plugin unload, panels are automatically removed

Tier 3f: Context Menu Actions (capability-gated)

host.registerTerminalAction(action) -> Disposable

Register an action in the terminal right-click “Actions” submenu. Requires "ui:context-menu" capability.

The action handler receives a TerminalActionContext snapshot captured at right-click time (not at click time), avoiding race conditions if the user switches terminals between opening the menu and clicking.

interface TerminalActionContext {
  sessionId: string | null;  // PTY session ID of the right-clicked terminal
  repoPath: string | null;   // Repository path that owns the terminal
}

interface TerminalAction {
  id: string;                                              // Unique action ID (scoped to plugin)
  label: string;                                           // Display label in the menu
  action: (ctx: TerminalActionContext) => void;             // Handler
  disabled?: (ctx: TerminalActionContext) => boolean;       // Evaluated at menu-open time
}

Example:

const d = host.registerTerminalAction({
  id: "restart-agent",
  label: "Restart Agent",
  action: (ctx) => {
    if (ctx.sessionId) host.sendAgentInput(ctx.sessionId, "exit");
  },
  disabled: (ctx) => !ctx.sessionId,
});

Behavior:

  • Actions from all plugins are shown in a flat list under the “Actions” submenu
  • The submenu is hidden when no actions are registered
  • disabled callback is re-evaluated each time the context menu opens
  • On plugin unload, actions are automatically removed; stale handler references are no-ops

host.registerContextMenuAction(action) -> Disposable

Register an action in context menus for a specific target type. Requires "ui:context-menu" capability.

type ContextMenuTarget = "terminal" | "branch" | "repo" | "tab";

interface ContextMenuAction {
  id: string;
  label: string;
  icon?: string;              // Inline SVG
  target: ContextMenuTarget;
  action: (ctx: ContextMenuContext) => void;
  disabled?: (ctx: ContextMenuContext) => boolean;
}

interface ContextMenuContext {
  target: ContextMenuTarget;
  sessionId?: string;     // terminal, tab
  repoPath?: string;      // branch, repo, terminal
  branchName?: string;    // branch only
  tabId?: string;         // tab only
}

Example:

host.registerContextMenuAction({
  id: "deploy",
  label: "Deploy Branch",
  target: "branch",
  action: (ctx) => {
    if (ctx.branchName) deploy(ctx.repoPath, ctx.branchName);
  },
});

Behavior:

  • Actions appear after built-in items, separated by a divider
  • disabled callback is re-evaluated each time the context menu opens
  • On plugin unload, actions are automatically removed

Tier 3g: Credential Access (capability-gated)

host.readCredential(serviceName) -> Promise<string | null>

Read credentials from the system credential store by service name. Returns the raw credential JSON string, or null if not found. Requires "credentials:read" capability.

First call from an external plugin shows a user consent dialog. Built-in plugins skip the dialog.

const credJson = await host.readCredential("Claude Code-credentials");
if (credJson) {
  const creds = JSON.parse(credJson);
  const token = creds.claudeAiOauth.accessToken;
}

Platforms:

  • macOS: Reads from Keychain (security find-generic-password -s <service> -w)
  • Linux/Windows: Reads from ~/.claude/.credentials.json

Tier 3h: HTTP Requests (capability-gated)

host.httpFetch(url, options?) -> Promise

Make an HTTP request. Non-2xx status codes are returned normally (not thrown as errors). Requires "net:http" capability.

External plugins can only fetch URLs matching their manifest’s allowedUrls patterns.

const resp = await host.httpFetch("https://api.example.com/data", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ key: "value" }),
});
if (resp.status === 200) {
  const data = JSON.parse(resp.body);
}

HttpResponse:

interface HttpResponse {
  status: number;
  headers: Record<string, string>;
  body: string;
}

Security and limits:

  • file://, data://, ftp:// schemes are blocked
  • 30-second timeout, 10 MB response limit, max 5 redirects
  • Localhost (localhost, 127.0.0.1, ::1, [::1], 0.0.0.0) is blocked unless explicitly declared in allowedUrls
  • Built-in plugins (no capabilities array) can fetch any http:// or https:// URL without restrictions

allowedUrls pattern matching:

  • Patterns use prefix matching with an optional trailing * wildcard
  • "https://api.example.com/*" — matches any path under that origin
  • "https://api.example.com/v2/data" — matches that exact URL only
  • "http://localhost:8080/*" — allows localhost on that port (required to unblock localhost)
  • The URL must start with the pattern prefix (before *) to match

Tier 3i: File Tail (capability-gated)

host.readFileTail(absolutePath, maxBytes) -> Promise

Read the last N bytes of a file, skipping any partial first line. Useful for reading recent entries from large JSONL files. Requires "fs:read" capability.

const tail = await host.readFileTail("/Users/me/.claude/hud-tracking.jsonl", 512 * 1024);
const lines = tail.split("\n").filter(Boolean);

Tier 3j: CLI Execution (capability-gated)

host.execCli(binary, args, cwd?) -> Promise

Execute a CLI binary declared in the plugin’s manifest and return its stdout. Requires "exec:cli" capability.

Only binaries listed in the manifest’s binaries field can be executed. The on-disk manifest is the source of truth — the frontend cannot grant access to undeclared binaries.

// manifest.json
{ "capabilities": ["exec:cli"], "binaries": ["mdkb"] }
const raw = await host.execCli("mdkb", ["--format", "json", "status"], "/Users/me/project");
const status = JSON.parse(raw);
console.log(status.index.documents); // 1486

Security and limits:

  • Only binaries declared in the plugin’s binaries manifest field can be executed
  • Working directory must be absolute and within $HOME
  • 30-second timeout
  • 5 MB stdout limit
  • Binary is resolved via PATH lookup and known install locations (~/.cargo/bin/, /usr/local/bin/, etc.)

Tier 4: Scoped Tauri Invoke (whitelisted commands only)

host.invoke(cmd, args?) -> Promise

Invokes a whitelisted Tauri command. Non-whitelisted commands throw immediately.

Whitelisted commands:

CommandArgsReturnsCapability
read_file{ path: string, file: string }stringinvoke:read_file
list_markdown_files{ path: string }Array<{ path, git_status }>invoke:list_markdown_files
read_plugin_data{ plugin_id: string, path: string }stringnone (always allowed)
write_plugin_data{ plugin_id: string, path: string, content: string }voidnone (always allowed)
delete_plugin_data{ plugin_id: string, path: string }voidnone (always allowed)

Plugin data storage is sandboxed to ~/.config/tuicommander/plugins/{id}/data/. No capability required — every plugin can store its own data.

// Store cache data
await host.invoke("write_plugin_data", {
  plugin_id: "my-plugin",
  path: "cache.json",
  content: JSON.stringify({ lastCheck: Date.now() }),
});

// Read it back
const raw = await host.invoke("read_plugin_data", {
  plugin_id: "my-plugin",
  path: "cache.json",
});
const cache = JSON.parse(raw);

Capabilities

Capabilities gate access to Tier 3 and Tier 4 methods. Declare them in manifest.json:

{
  "capabilities": ["pty:write", "ui:sound"]
}
CapabilityUnlocksRisk
pty:writehost.writePty(), host.sendAgentInput()Can send input to terminals
ui:markdownhost.openMarkdownPanel(), host.openMarkdownFile()Can open panels and files in the UI
ui:soundhost.playNotificationSound(sound?)Can play sounds (question, error, completion, warning, info)
ui:panelhost.openPanel()Can render arbitrary HTML in sandboxed iframe
ui:tickerhost.setTicker(), host.clearTicker()Can post messages to the shared status bar ticker
credentials:readhost.readCredential()Can read system credentials (consent dialog shown)
net:httphost.httpFetch()Can make HTTP requests (scoped to allowedUrls)
invoke:read_filehost.invoke("read_file", ...)Can read files on disk
invoke:list_markdown_fileshost.invoke("list_markdown_files", ...)Can list directory contents
fs:readhost.readFile(), host.readFileTail()Can read files within $HOME (10 MB limit)
fs:listhost.listDirectory()Can list directory contents within $HOME
fs:watchhost.watchPath()Can watch filesystem paths within $HOME for changes
fs:writehost.writeFile()Can write files within $HOME (10 MB limit)
fs:renamehost.renamePath()Can rename/move files within $HOME
exec:clihost.execCli()Can execute CLI binaries declared in manifest binaries field
git:readhost.getGitBranches(), host.getRecentCommits(), host.getGitDiff()Read-only access to git repository state
ui:context-menuhost.registerTerminalAction()Can add actions to the terminal right-click “Actions” submenu
ui:sidebarhost.registerSidebarPanel()Can register collapsible panel sections in the sidebar
ui:file-iconshost.registerFileIconProvider()Can provide file/folder icons for the file browser (e.g. VS Code icon themes)

Tier 1, Tier 2, and plugin data commands are always available without capabilities.

Agent-Scoped Plugins

Plugins can declare which AI agents they target via the agentTypes manifest field:

{
  "id": "claude-usage",
  "agentTypes": ["claude"],
  ...
}

Behavior

  • Universal plugins (agentTypes omitted or []): receive events from all terminals. This is the default.
  • Agent-scoped plugins (agentTypes: ["claude"]): output watchers and structured event handlers only fire for terminals where the detected foreground process matches one of the listed agent types.

What gets filtered

Dispatch methodFiltered by agentTypes
registerOutputWatcher callbacksYes
registerStructuredEventHandler callbacksYes
All other PluginHost methods (Tier 1-4)No — always available

How agent detection works

TUICommander polls the foreground process of each terminal’s PTY every 3 seconds (via get_session_foreground_process). The process name is classified into an agent type:

Process nameAgent type
claude"claude"
gemini"gemini"
opencode"opencode"
aider"aider"
codex"codex"
amp"amp"
cursor-agent"cursor"
oz"warp"
droid"droid"
git"git"
(anything else)null (plain shell)

Timing considerations

Agent detection is polled, not instant. When a user launches claude in a terminal, there is a brief window (up to 3 seconds) before the first poll detects it. During this window, agent-scoped plugins will not receive events from that terminal. This is by design — it avoids false matches during shell startup.

Example: Claude-only plugin

{
  "id": "claude-usage",
  "name": "Claude Usage Dashboard",
  "version": "1.0.0",
  "minAppVersion": "0.3.0",
  "main": "main.js",
  "agentTypes": ["claude"],
  "capabilities": ["fs:read", "ui:panel", "ui:ticker"]
}

This plugin’s output watchers will only fire when the terminal is running Claude Code. If the user switches to a plain shell or runs Gemini, the watchers are silently skipped.

Example: Multi-agent plugin

{
  "agentTypes": ["claude", "gemini", "codex"]
}

Targets Claude, Gemini, and Codex terminals. All other terminals are ignored.

Content URI Format

scheme:path?key=value&key2=value2

Examples:

  • plan:file?path=%2Frepo%2Fplans%2Ffoo.md
  • stories:detail?id=324-9b46&dir=%2Frepo%2Fstories

Icons

All icons must be monochrome inline SVGs with fill="currentColor" and viewBox 0 0 16 16:

const ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor"><path d="..."/></svg>';

Never use emoji — they render inconsistently across platforms.

Hot Reload

When any file in a plugin directory changes, the app:

  1. Emits a plugin-changed event with the plugin ID
  2. Calls pluginRegistry.unregister(id) (runs onunload, disposes all registrations)
  3. Re-imports the module with a cache-busting query (?t=timestamp)
  4. Validates and re-registers the plugin

This means you can edit main.js and see changes without restarting the app.

Build & Install

External plugins must be pre-compiled ES modules. Use esbuild:

esbuild src/main.ts --bundle --format=esm --outfile=main.js --external:nothing

Install by copying the directory to:

  • macOS: ~/Library/Application Support/com.tuic.commander/plugins/my-plugin/
  • Linux: ~/.config/tuicommander/plugins/my-plugin/
  • Windows: %APPDATA%/com.tuic.commander/plugins/my-plugin/

Directory structure:

my-plugin/
  manifest.json
  main.js

Plugin Management (Settings > Plugins)

The Settings panel has a Plugins tab with two sub-tabs:

Installed

  • Lists all plugins (built-in and external) with toggle, logs, and uninstall buttons
  • Built-in plugins show a “Built-in” badge and cannot be toggled or uninstalled
  • Error count badges appear on plugins with recent errors
  • “Logs” button opens an expandable log viewer showing the plugin’s ring buffer
  • “Install from file…” button opens a file dialog accepting .zip archives

Browse

  • Shows plugins from the community registry (fetched from GitHub)
  • “Install” button downloads and installs directly
  • “Update available” badge when a newer version exists
  • “Refresh” button forces a new registry fetch (normally cached for 1 hour)

Enable/Disable

Plugin enabled state is persisted in AppConfig.disabled_plugin_ids. Disabled plugins appear in the Installed list but are not loaded.

ZIP Plugin Installation

Plugins can be distributed as ZIP archives:

  1. From Settings: Click “Install from file…” in the Plugins tab
  2. From URL: Use tuic://install-plugin?url=https://example.com/plugin.zip
  3. From Rust: invoke("install_plugin_from_zip", { path }) or invoke("install_plugin_from_url", { url })

ZIP requirements:

  • Must contain a valid manifest.json (at root or in a single top-level directory)
  • All paths are validated for zip-slip attacks (no .. traversal)
  • If updating an existing plugin, the data/ directory is preserved

TUICommander registers the tuic:// URL scheme for external integration:

URLAction
tuic://install-plugin?url=https://...Download ZIP, show confirmation, install
tuic://open-repo?path=/path/to/repoSwitch to repo (must already be in sidebar)
tuic://settings?tab=pluginsOpen Settings to a specific tab
tuic://open/<path>Open markdown file in tab (iframe SDK only)
tuic://terminal?repo=<path>Open terminal in repo (iframe SDK only)

Security: install-plugin requires HTTPS URLs and shows a confirmation dialog. open-repo only accepts paths already in the repository list. open and terminal validate paths against known repos (available only inside plugin iframes via the TUIC SDK, not as OS-level deep links).

Plugin Registry

The registry is a JSON file hosted on GitHub (sstraus/tuicommander-plugins repo). The app fetches it on demand (Browse tab) with a 1-hour TTL cache.

Registry entries include: id, name, description, author, latestVersion, minAppVersion, capabilities, downloadUrl.

The Browse tab compares installed versions to detect available updates.

Per-Plugin Error Logging

Each plugin has a dedicated ring buffer logger (500 entries max). Errors from onload, onunload, output watchers, and structured event handlers are automatically captured.

Plugins can also write to their log via host.log(level, message, data).

View logs in Settings > Plugins > click “Logs” on any plugin row.

Built-in Plugins

Built-in plugins are TypeScript modules in src/plugins/ compiled with the app. They have unrestricted access (no capability checks).

PluginFileSectionDetects
planplanPlugin.tsACTIVE PLANplan-file structured events (repo-scoped)

Note: Session prompt tracking is now a native Rust feature (via input_line_buffer.rs and the Activity Dashboard). The former sessionPromptPlugin built-in has been removed.

See examples/plugins/report-watcher/ for a template showing how to extract terminal output into Activity Center items with a markdown viewer.

To create a built-in plugin, add it to BUILTIN_PLUGINS in src/plugins/index.ts.

Testing

Mock setup for plugin tests

import { describe, it, expect, beforeEach, vi } from "vitest";

vi.mock("../../invoke", () => ({
  invoke: vi.fn(),
  listen: vi.fn().mockResolvedValue(() => {}),
}));

import { invoke } from "../../invoke";
import { pluginRegistry } from "../../plugins/pluginRegistry";
import { activityStore } from "../../stores/activityStore";
import { markdownProviderRegistry } from "../../plugins/markdownProviderRegistry";

beforeEach(() => {
  pluginRegistry.clear();
  activityStore.clearAll();
  markdownProviderRegistry.clear();
  vi.mocked(invoke).mockReset();
});

Testing output watchers

it("detects deployment from PTY output", () => {
  pluginRegistry.register(myPlugin);
  pluginRegistry.processRawOutput("Deployed: api-server to prod\n", "session-1");

  const items = activityStore.getForSection("my-section");
  expect(items).toHaveLength(1);
  expect(items[0].title).toBe("api-server");
});

Testing capability gating

it("external plugin without pty:write throws on sendAgentInput", async () => {
  let host;
  pluginRegistry.register(
    { id: "ext", onload: (h) => { host = h; }, onunload: () => {} },
    [], // no capabilities
  );
  await expect(host.sendAgentInput("s1", "hello")).rejects.toThrow(PluginCapabilityError);
});

CSS Classes

Activity items use these CSS classes (defined in src/styles.css):

ClassElement
activity-section-headerSection heading row
activity-section-labelSection label text
activity-dismiss-all“Dismiss All” button
activity-itemIndividual item row
activity-item-iconItem icon container
activity-item-bodyTitle + subtitle wrapper
activity-item-titlePrimary text
activity-item-subtitleSecondary text
activity-item-dismissDismiss button
activity-last-item-btnShortcut button in toolbar
activity-last-item-iconShortcut button icon
activity-last-item-titleShortcut button text

Structured Event Types

The Rust OutputParser detects patterns in terminal output and emits typed events. Handle them with host.registerStructuredEventHandler(type, handler).

plan-file

Detected when a plan file path appears in terminal output. The path is always resolved to an absolute path before emission:

  • Relative paths (e.g. plans/foo.md, .claude/plans/bar.md) are resolved against the terminal session’s CWD
  • Tilde paths (~/.claude/plans/bar.md) are expanded to the user’s home directory
  • Already-absolute paths are passed through unchanged

If the session has no CWD (rare), relative paths are emitted as-is and may fail to open.

{ type: "plan-file", path: string }
// path: always absolute, e.g. "/Users/me/project/plans/foo.md",
//       "/Users/me/.claude/plans/graceful-rolling-quasar.md"

Repo scoping: The built-in plan plugin only displays plans from terminals whose CWD matches the active repository in the sidebar. Plans from other projects are silently filtered out.

rate-limit

Detected when AI API rate limits are hit.

{
  type: "rate-limit",
  pattern_name: string,           // e.g. "claude-http-429", "openai-http-429"
  matched_text: string,           // the matched substring
  retry_after_ms: number | null,  // ms to wait (default 60000)
}

Pattern names: claude-http-429, claude-overloaded, openai-http-429, cursor-rate-limit, gemini-resource-exhausted, http-429, retry-after-header, openai-retry-after, openai-tpm-limit, openai-rpm-limit.

status-line

Detected when an AI agent emits a status/progress line.

{
  type: "status-line",
  task_name: string,              // e.g. "Reading files"
  full_line: string,              // complete line trimmed
  time_info: string | null,       // e.g. "12s"
  token_info: string | null,      // e.g. "2.4k tokens"
}

pr-url

Detected when a GitHub/GitLab PR/MR URL appears in output.

{
  type: "pr-url",
  number: number,     // PR/MR number
  url: string,        // full URL
  platform: string,   // "github" or "gitlab"
}

progress

Detected from OSC 9;4 terminal progress sequences.

{
  type: "progress",
  state: number,  // 0=remove, 1=normal, 2=error, 3=indeterminate
  value: number,  // 0-100
}

question

Detected when an interactive prompt appears (Y/N prompts, numbered menus, inquirer-style).

{
  type: "question",
  prompt_text: string,  // the question line (ANSI-stripped)
}

usage-limit

Detected when Claude Code reports usage limits.

{
  type: "usage-limit",
  percentage: number,    // 0-100
  limit_type: string,    // "weekly" or "session"
}

Example Plugins

See examples/plugins/ for complete working examples:

ExampleTierCapabilitiesDemonstrates
hello-world1noneOutput watcher, addItem
auto-confirm1+3pty:writeAuto-responding to Y/N prompts
ci-notifier1+3ui:sound, ui:markdownSound notifications, markdown panels
repo-dashboard1+2noneRead-only state, dynamic markdown
claude-status1noneAgent-scoped (agentTypes: ["claude"]), structured events
telegram-notifier1+3net:http, ui:panel, ui:tickerTelegram push notifications, per-event toggles, settings panel

Distributable Plugins

Available from the plugin registry (submodule at plugins/). Installable via Settings > Plugins > Browse.

PluginTierCapabilitiesDescription
mdkb-dashboard2+3exec:cli, fs:read, ui:panel, ui:tickermdkb knowledge base dashboard
rtk-dashboard3exec:cli, ui:panel, ui:context-menuRTK token savings dashboard (binaries: ["rtk"])

Troubleshooting

ProblemCauseFix
Plugin not loadingmanifest.json missing or malformedCheck console for validation errors
requires app version X.Y.ZminAppVersion too highLower minAppVersion or update app
not in the invoke whitelistCalling non-whitelisted Tauri commandOnly use commands listed in the whitelist table
not declared in plugin ... manifest binariesBinary not in manifest binaries fieldAdd the binary name to the binaries array in manifest.json
requires capability "X"Missing capability in manifestAdd the capability to manifest.json capabilities array
Module not foundmain field doesn’t match filenameEnsure "main": "main.js" matches your actual file
Changes not reflectingHot reload cacheSave the file again, or restart the app
default export errorModule doesn’t export default { ... }Ensure your module has a default export with id, onload, onunload

Development Setup

Prerequisites

  • Node.js (LTS)
  • Rust (stable toolchain via rustup)
  • Tauri CLI (cargo install tauri-cli)
  • git and gh (GitHub CLI) for git/GitHub features

Windows users: See the Windows-specific prerequisites section below before proceeding.

Install Dependencies

npm install

Development

Native Tauri App

npm run tauri dev

Starts Vite dev server + Tauri app with hot reload.

Browser Mode

When the MCP server is enabled in settings, the frontend can run standalone:

npm run dev

Connects to the Rust HTTP server via WebSocket/REST.

Build

npm run tauri build

Produces platform-specific installers:

  • macOS: .dmg and .app
  • Windows: .nsis (.exe setup installer)
  • Linux: .deb and .AppImage

Note: The .msi bundle may fail on Windows due to WiX tooling issues. Use --bundles nsis to produce a working .exe installer:

cargo tauri build --bundles nsis

Testing

npm test              # Run all tests
npm run test:watch    # Watch mode
npm run test:coverage # Coverage report

Test tiers:

  • Tier 1: Pure functions (utils, type transformations)
  • Tier 2: Store logic (state management)
  • Tier 3: Component rendering
  • Tier 4: Integration (hooks + stores)

Framework: Vitest + SolidJS Testing Library + happy-dom

Coverage: ~80%+ (830 tests)

Project Structure

See Architecture Overview for full directory structure.

Key Files

FilePurpose
src/App.tsxCentral orchestrator (829 lines)
src-tauri/src/lib.rsRust app setup, command registration
src-tauri/src/pty.rsPTY session management
src/hooks/useAppInit.tsApp initialization
src/stores/terminals.tsTerminal state
src/stores/repositories.tsRepository state
SPEC.mdFeature specification
IDEAS.mdFeature concepts under evaluation

Configuration

App config stored in platform config directory:

  • macOS: ~/Library/Application Support/tuicommander/
  • Linux: ~/.config/tuicommander/
  • Windows: %APPDATA%/tuicommander/

See Configuration docs for all config files.

Makefile Targets

make dev      # Tauri dev mode
make build    # Production build
make test     # Run tests
make lint     # Run linter
make clean    # Clean build artifacts

Windows Prerequisites

Building on Windows requires a few extra tools beyond the standard prerequisites. Install them in this order.

1. Visual Studio Build Tools (C++ compiler)

Download and install Visual Studio Build Tools and select the “Desktop development with C++” workload. VS Build Tools 2019 or later is fine.

Or via winget:

winget install Microsoft.VisualStudio.2022.BuildTools

2. Rust

winget install Rustlang.Rustup

Restart your terminal after installation, then verify:

rustc --version
cargo --version

3. Node.js

winget install OpenJS.NodeJS.LTS

4. CMake

Required to compile whisper-rs (the on-device dictation library).

winget install Kitware.CMake

5. LLVM 18 (libclang — required for whisper-rs bindings)

whisper-rs uses bindgen to generate Rust bindings for whisper.cpp, which requires libclang. Use LLVM 18 — LLVM 19+ produces broken bindings for this crate on Windows.

Download the LLVM 18 installer from GitHub releases (LLVM-18.1.8-win64.exe) and install it. Then set the environment variable so bindgen can find it:

# Add to your PowerShell profile or set permanently in System Environment Variables
$env:LIBCLANG_PATH = "C:\Program Files\LLVM\bin"

If you have LLVM 22+ installed (e.g. from winget), install LLVM 18 to a separate directory and point LIBCLANG_PATH there instead.

6. Tauri CLI

cargo install tauri-cli --version "^2"

Full Windows Build Command

Always set LIBCLANG_PATH before building:

$env:LIBCLANG_PATH = "C:\Program Files\LLVM\bin"   # adjust path if LLVM 18 is elsewhere
cargo tauri build --bundles nsis

The installer will be at:

src-tauri\target\release\bundle\nsis\TUICommander_0.x.x_x64-setup.exe

Windows Known Issues

SymptomCauseFix
whisper-rs-sys build fails with “couldn’t find libclang”LLVM not installed or LIBCLANG_PATH not setInstall LLVM 18 and set LIBCLANG_PATH
whisper-rs-sys compile error: attempt to compute 1_usize - 296_usizeLLVM 19+ generates broken bindings for this crateUse LLVM 18 specifically
WiX .msi bundle failsWiX light.exe tooling issueUse --bundles nsis instead
App window opens but shows a black screenNavigation guard in lib.rs blocked http://tauri.localhost/ (Windows’ internal Tauri URL)Fixed in current code — tauri.localhost is explicitly allowed
Update check failed: windows-x86_64-nsis not foundCustom local build isn’t listed in official release manifestHarmless — auto-update simply won’t trigger

Performance Profiling

Repeatable profiling infrastructure for identifying bottlenecks across the full stack: Rust backend, SolidJS frontend, Tauri IPC, and terminal I/O.

Quick Start

# Install profiling tools (one-time)
scripts/perf/setup.sh

# Run all automated benchmarks (app must be running)
scripts/perf/run-all.sh

# Or run individual benchmarks
scripts/perf/bench-ipc.sh              # IPC latency
scripts/perf/bench-pty.sh              # PTY throughput
scripts/perf/record-cpu.sh             # CPU flamegraph
scripts/perf/record-tokio.sh           # Tokio runtime inspector
scripts/perf/snapshot-memory.sh        # Memory profiling guide

Results are saved in scripts/perf/results/ (gitignored).

Tools

ToolWhat it profilesInstall
samplyRust CPU time (flamegraphs)cargo install samply
tokio-consoleAsync task scheduling, lock contentioncargo install tokio-console
hyperfineCommand-line benchmarkingbrew install hyperfine
Chrome DevToolsJS rendering, memory, layoutBuilt into Tauri webview
Solid DevToolsSolidJS signal/memo reactivity graphBrowser extension

All tools are installed by scripts/perf/setup.sh.

Rust Backend

CPU Flamegraph

scripts/perf/record-cpu.sh --duration 60

Builds a release binary with debug symbols, records under samply for 60 seconds, saves a JSON profile. Open the result with:

samply load scripts/perf/results/cpu-YYYYMMDD-HHMMSS.json

What to look for:

  • Functions with wide bars = high cumulative CPU time
  • std::process::Command in hot paths = subprocess forks
  • serde_json::to_value / serde_json::to_string = serialization overhead
  • parking_lot::Mutex::lock = contention

Tokio Runtime Inspector

scripts/perf/record-tokio.sh

Builds with the tokio-console Cargo feature and launches both the app and the console UI. The console shows live stats for every Tokio task.

What to look for:

  • Tasks with high “busy” time = CPU-bound work on the async executor
  • Tasks with high “idle” time = blocked on I/O or lock contention
  • Tasks stuck in “waiting” = possible deadlock
  • Many short-lived spawn_blocking tasks = check if batching would help

Note: This uses a debug build. Timing numbers are not representative of production performance, but relative proportions and task scheduling patterns are valid.

Building with tokio-console manually

cd src-tauri
RUSTFLAGS="--cfg tokio_unstable" cargo build --features tokio-console

Then run the binary and connect tokio-console separately:

tokio-console

The console subscriber listens on 127.0.0.1:6669 by default.

IPC Latency

scripts/perf/bench-ipc.sh                     # auto-detect repo
scripts/perf/bench-ipc.sh /path/to/repo 50    # 50 iterations

Measures round-trip latency for key git commands via the HTTP API on port 9877. Reports p50, p95, and mean for each endpoint.

Endpoints measured:

  • repo_info (cached after first call, 5s TTL)
  • git_panel_context (cached, 5s TTL)
  • diff_stats, changed_files, branches
  • recent_commits, stash_list, remote_url

Interpreting results:

  • p50 < 5ms for cached endpoints = healthy
  • p50 < 50ms for uncached git commands = healthy
  • p95 > 200ms = investigate (large repo? slow disk? lock contention?)

To measure cold vs warm cache, run bench-ipc.sh twice in quick succession: the first run hits cache misses, the second should show cache hits.

PTY Throughput

scripts/perf/bench-pty.sh       # 10MB default
scripts/perf/bench-pty.sh 50    # 50MB stress test

Creates a PTY session, blasts data through it, and measures throughput in MB/s. Tests the full pipeline: PTY read -> UTF-8 decode -> escape processing -> VT100 parse -> Tauri event emit.

If the API doesn’t support session creation, the script prints manual commands to run in an existing terminal tab for the same measurement.

Frontend

Performance Recording

  1. Open DevTools in TUICommander: Cmd+Shift+I
  2. Go to Performance tab
  3. Click Record
  4. Exercise the scenario for 10-30 seconds
  5. Stop recording

What to look for:

  • Long Tasks (>50ms red bars) = jank
  • Layout/Recalculate Style = CSS forcing reflow
  • requestAnimationFrame gaps = dropped frames
  • Frequent minor GC = allocation pressure

Memory Profiling

Run scripts/perf/snapshot-memory.sh for detailed scenario instructions. Key scenarios:

  1. Terminal memory — open/close 5 terminals, compare heap snapshots
  2. Panel leak check — open/close Settings/Activity/Git panels 10x
  3. Long-running session — compare snapshots at 0/10/20 minutes

Expected baselines:

  • Each terminal: ~1.6MB heap (10k scrollback lines at 80 cols)
  • Panel open/close cycle: <500KB retained after GC
  • 20-minute session: sub-linear growth (not linear)

SolidJS Reactivity

Install Solid DevTools browser extension. In the devtools panel:

  • Check how many times each createMemo re-evaluates
  • Find effects with unexpectedly high execution counts
  • Trace which signal changes trigger cascading updates

Key areas to watch:

  • terminalsStore updates propagating to StatusBar/TabBar/SmartButtonStrip
  • debouncedBusy signal reactivity scope
  • githubStore polling triggering re-renders in unrelated components

Profiling Scenarios

Scenario 1: Startup Performance

scripts/perf/record-cpu.sh --duration 30

Open the app, wait for it to fully load, open DevTools Performance tab. Measure time-to-interactive.

Target: < 2s from launch to first terminal ready.

Scenario 2: Multi-Terminal Steady State

  1. Open 5 terminal tabs
  2. Run an AI agent in 2 of them
  3. Record CPU + memory for 2 minutes
  4. Check: is CPU usage stable? Is memory growing?

Scenario 3: Git-Heavy Workflow

  1. Open a large repo (>1000 commits, >50 branches)
  2. Open the Git panel
  3. Switch branches
  4. Run bench-ipc.sh against this repo

Scenario 4: High-Throughput Output

In a terminal tab:

dd if=/dev/urandom bs=1024 count=10240 | base64    # ~14MB of random base64
yes | head -n 500000                                 # ~2MB of repetitive data
find / -type f 2>/dev/null                           # realistic filesystem output

Monitor CPU usage and app responsiveness during output.

Comparing Results Across Sessions

Results accumulate in scripts/perf/results/:

results/
  ipc-20260328-143000.txt    # IPC latency run
  ipc-20260330-091500.txt    # After optimization
  cpu-20260328-150000.json   # CPU flamegraph
  pty-throughput.log          # PTY throughput history (appended)

Compare IPC results:

diff scripts/perf/results/ipc-{before,after}.txt

The PTY throughput log is append-only — each run adds a line for trend tracking.

Architecture Reference

The profiling targets map to these code areas:

LayerKey filesWhat to measure
Tauri commandssrc-tauri/src/git.rsspawn_blocking overhead, subprocess latency
PTY pipelinesrc-tauri/src/pty.rsRead buffer throughput, event emission rate
IPC serializationsrc-tauri/src/pty.rs, git.rsJSON payload sizes, serde time
State managementsrc/stores/terminals.tsSignal propagation scope, batch effectiveness
Renderingsrc/components/Terminal/Terminal.tsxxterm.js write batching, WebGL atlas rebuilds
Pollingsrc/hooks/useAgentPolling.ts, src/stores/github.tsInterval frequency, IPC calls per tick
Bundlevite.config.tsChunk sizes, initial parse/eval time