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_ptyTauri 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+1throughCmd+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 editorfile://URLs are recognized in addition to plain paths — the prefix is stripped and the path resolved like any other- Supports
:lineand:line:colsuffixes 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+Fopens search overlay — context-aware: routes to terminal, markdown tab, or diff tab based on active view- Terminal: incremental search via
@xterm/addon-searchwith 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
Escapecloses 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
cdto 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 viaupdate_session_cwdIPC and stored per-session assession_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_flagsTauri 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
onDragDropEventAPI (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/.mdxfiles 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/droppreventDefaultprevents the Tauri webview from treating drops as browser navigation (which would replace the UI with a white screen) - macOS file association:
.md/.mdxfiles registered with TUICommander — double-click in Finder opens them directly
1.14 Cross-Terminal Search
- 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 / -Nadditions/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+Ctrlheld - 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) orCtrl+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
.mdand.mdxfiles with syntax-highlighted code blocks - File list from repository’s markdown files
- Clickable file paths in terminal open
.mdfiles 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+Fsearch: 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.mdfile 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
Cbutton 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_contentTauri command; results delivered viacontent-search-batchevents
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
Entersubmits idea,Shift+Enterinserts 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+Vpastes 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
- Images saved to
- Edit preserves note identity (in-place update, no ID change)
Escapecancels 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-virtualfor 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:
Escapeto close the panelCtrl/Cmd+1–4to 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,Enterto execute,Escto 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_METAmap)
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 viaintent:tokenlastPrompt(speech bubble icon) — last user prompt (>= 10 words). Shown only when noagentIntentis 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_logsTauri 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-fileevents from the output parser and viaplans/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.jsonon 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()insrc/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_fileIPC for text content - CSP allows
asset:andhttp://asset.localhostinframe-srcandmedia-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 asCmd+[- Hotkey hint visible during quick switcher
4.2 Branch Display
- Center: shows
repo / branchname - 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/.mdxfiles 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:
- Rate limit warning (highest): count + countdown timer when sessions are rate-limited
- Claude Usage API ticker: live utilization from Anthropic API (click opens dashboard)
- PTY usage limit: weekly/session percentage from terminal output detection
- 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
| Agent | Binary | Resume Command |
|---|---|---|
| Claude Code | claude | claude --resume <uuid> (session-aware) / claude --continue (fallback) |
| Gemini CLI | gemini | gemini --resume <uuid> (session-aware) / gemini --resume (fallback) |
| OpenCode | opencode | opencode -c |
| Aider | aider | aider --restore-chat-history |
| Codex CLI | codex | codex resume <uuid> (session-aware) / codex resume --last (fallback) |
| Amp | amp | amp threads continue |
| Cursor Agent | cursor-agent | cursor-agent resume |
| Warp Oz | oz | — |
| 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;sessionIdfield 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_SESSIONto 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_SESSIONis used as--session-idautomatically - Custom scripts:
$TUIC_SESSIONis 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] Taskformat, 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, thenproc_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:
- 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 - Silence-based (Strategy 2, fallback): if terminal output stops for 10s after a line ending with
?, the session is treated as awaiting input
- 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
- 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-lineevent 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+Aaction - 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
Intentevents 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-bridgeinto 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=1without 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=1injected 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-createdandsession-closedevents 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 | Ctokens, 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 | action3at 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 (
1–9to select,Escto 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_modeactivates and the output parser scans the bottom screen rows for slash command menus - Detection: 2+ consecutive rows starting with
/commandpatterns, with❯highlight for the selected item - Produces
ParsedEvent::SlashMenu { items }— used by mobile PWA to render a native bottom-sheet overlay slash_modecleared on user-input events and status-line events
6.13 Inter-Agent Messaging
- New
messagingMCP tool for agent-to-agent coordination when multiple agents are spawned in parallel - Identity: Each agent uses its
$TUIC_SESSIONenv 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/channelover SSE when the client supports channels; polling fallback viainboxalways available - Channel support: TUICommander declares
experimental.claude/channelcapability; 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/PeerUnregisteredevents 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:11434with live model list), Anthropic, OpenAI, OpenRouter, custom OpenAI-compatible endpoint. Provider abstraction viagenaicrate - Per-turn terminal context: last
context_linesrows fromVtLogBuffer(ANSI-stripped, alt-screen suppressed),SessionState, recentParsedEvents, git branch/diff. Attach / detach / auto-attach terminal via header dropdown - API keys stored in OS keyring (service
tuicommander-ai-chat, userapi-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
SafetyCheckertrait — 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 likerm -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
Inferredoutcomes otherwise. Persisted to<config_dir>/ai-sessions/<session_id>.jsonwith 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 boundedmpscworker asks the active AI provider for a one-linesemantic_intentand stamps it onto theCommandOutcome(identified by stableid: 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 (preferssend_key+wait_forover line-orientedsend_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 matchesEsc 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 viapluginRegistry.dispatchStructuredEvent("choice-prompt", …); rendered as PWA overlay - Single-key replies routed through
sendPtyKey()(src/utils/sendCommand.ts) — nevertext + \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_HEADfor 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+Wor 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 --allvia 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::RecommendedWatcherwith 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
.gitignorerules — ignored paths do not trigger refreshes - Gitignore hot-reload: editing
.gitignorerebuilds the ignore filter without restarting the watcher - When a terminal runs
git checkout -b new-branchin 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/solidwith 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+Fsearch 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
ghCLI 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_filterfield) 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/closecloses 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_TOKENenv →GITHUB_TOKENenv → OAuth keyring token →gh_tokencrate →gh auth tokenCLI gh_tokencrate with empty-string bug workaround- Fallback to
gh auth tokenCLI
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
| Model | Size | Quality |
|---|---|---|
| tiny | ~75 MB | Low |
| base | ~140 MB | Fair |
| small | ~460 MB | Good |
| medium | ~1.5 GB | Very good |
| large-v3-turbo | ~1.6 GB | Best (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+Kto 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 branchCmd+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+Kor 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
| Category | Prompts |
|---|---|
| Git & Commit | Smart Commit, Commit & Push, Amend Commit, Generate Commit Message |
| Code Review | Review Changes, Review Staged, Review PR, Address Review Comments |
| Pull Requests | Create PR, Update PR Description, Generate PR Description |
| Merge & Conflicts | Resolve Conflicts, Merge Main Into Branch, Rebase on Main |
| CI & Quality | Fix CI Failures, Fix Lint Issues, Write Tests, Run & Fix Tests |
| Investigation | Investigate Issue, What Changed?, Summarize Branch, Explain Changes |
| Code Operations | Suggest Refactoring, Security Audit |
10.7 Context Variables
Variables are resolved from the Rust backend (resolve_context_variables) and frontend stores:
| Variable | Source | Description |
|---|---|---|
{branch} | git | Current branch name |
{base_branch} | git | Detected default branch (main/master/develop) |
{repo_name} | git | Repository directory name |
{repo_path} | git | Full filesystem path to the repository root |
{repo_owner} | git | GitHub owner parsed from remote URL |
{repo_slug} | git | Repository name parsed from remote URL |
{diff} | git | Full working tree diff (truncated to 50KB) |
{staged_diff} | git | Staged changes diff (truncated to 50KB) |
{changed_files} | git | Short status output |
{dirty_files_count} | git | Number of modified files (derived from changed_files) |
{commit_log} | git | Last 20 commits (oneline) |
{last_commit} | git | Last commit hash + message |
{conflict_files} | git | Files with merge conflicts |
{stash_list} | git | Stash entries |
{branch_status} | git | Ahead/behind remote tracking branch |
{remote_url} | git | Remote origin URL |
{current_user} | git | Git config user.name |
{pr_number} | GitHub store | PR number for current branch |
{pr_title} | GitHub store | PR title |
{pr_url} | GitHub store | PR URL |
{pr_state} | GitHub store | PR state (OPEN, MERGED, CLOSED) |
{pr_author} | GitHub store | PR author username |
{pr_labels} | GitHub store | PR labels (comma-separated) |
{pr_additions} | GitHub store | Lines added in PR |
{pr_deletions} | GitHub store | Lines deleted in PR |
{pr_checks} | GitHub store | CI check summary (passed/failed/pending) |
{merge_status} | GitHub store | PR mergeable status |
{review_decision} | GitHub store | PR review decision |
{agent_type} | terminal store | Active agent type (claude, gemini, etc.) |
{cwd} | terminal store | Active terminal working directory |
{issue_number} | manual | Prompted 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_scriptTauri command. No agent involved — runs content as-is viash -c(macOS/Linux) orcmd /C(Windows) in the repo directory. Output routed viaoutputTarget. 60-second timeout cap. No prerequisites (no terminal, agent, or API config needed) - Headless: runs a one-shot subprocess via
execute_headless_promptTauri command. Requires a per-agent headless template configured in Settings → Agents (e.g.claude -p "{prompt}"). Output routed to clipboard or toast depending onoutputTarget. Falls back to inject in PWA mode. 5-minute timeout cap
10.9 UI Integration Points
| Location | Prompts shown | Trigger |
|---|---|---|
| Toolbar dropdown | All enabled prompts with toolbar placement | Cmd+Shift+K or lightning bolt button |
| Git Panel — Changes tab | SmartButtonStrip with git-changes placement | Inline buttons above changed files |
| PR Detail Popover | SmartButtonStrip with pr-popover placement | Inline buttons in PR detail view |
| Command Palette | All prompts with Smart: prefix | Cmd+P then type “Smart” |
| Branch context menu | Prompts with git-branches placement | Right-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 Promptbutton - 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-mcpon 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.jsonin 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.jsonmerging — 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.jsonin the platform config directory - Auto-populated from
actionRegistry.ts(ACTION_METAmap) — 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 settingsnotification_config.json— sound settingsui_prefs.json— sidebar visibility/widthrepo_settings.json— per-repo worktree/script settingsrepositories.json— repository list, groups, branchesagents.json— per-agent run configurationsprompt_library.json— saved promptsnotes.json— ideas panel datadictation_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 beforehydrate()completes to prevent data loss
13. Cross-Platform
13.1 Supported Platforms
- macOS (primary), Windows, Linux
13.2 Platform Adaptations
Cmd↔Ctrlkey abstractionresolve_cli(): probes well-known directories when PATH unavailable (release builds)- Windows:
cmd.exeshell escaping,CreateToolhelp32Snapshotfor process detection - IDE detection:
.appbundles (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
keepawakeintegration 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
ConfirmDialogcomponent replaces native Tauriask()dialogs - Dark-themed to match the app (native macOS sheets render in light mode)
useConfirmDialoghook provides aconfirm()→Promise<boolean>API- Pre-built helpers:
confirmRemoveWorktree(),confirmCloseTerminal(),confirmRemoveRepo() - Keyboard support:
Enterto confirm,Escapeto 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-bridgeships 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 realconnect()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
mdkbMCP 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_graphtool - Requires
mdkbbinary 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
Secureflag on TLS connections - Settings panel shows Tailscale status with actionable guidance
15. Keyboard Shortcut Reference
Terminal
| Shortcut | Action |
|---|---|
Cmd+T | New terminal tab |
Cmd+W | Close tab / close active split pane |
Cmd+Shift+T | Reopen last closed tab |
Cmd+1–Cmd+9 | Switch to tab by number |
Ctrl+Tab / Ctrl+Shift+Tab | Next / previous tab |
Cmd+L | Clear terminal |
Cmd+C | Copy selection |
Cmd+V | Paste to terminal |
Cmd+Home | Scroll to top |
Cmd+End | Scroll to bottom |
Shift+PageUp | Scroll one page up |
Shift+PageDown | Scroll one page down |
Cmd+R | Run saved command |
Cmd+Shift+R | Edit and run command |
Zoom
| Shortcut | Action |
|---|---|
Cmd+= | Zoom in (+2px) |
Cmd+- | Zoom out (-2px) |
Cmd+0 | Reset zoom |
Split Panes
| Shortcut | Action |
|---|---|
Cmd+\ | Split vertically |
Cmd+Alt+\ | Split horizontally |
Alt+←/→ | Navigate vertical panes |
Alt+↑/↓ | Navigate horizontal panes |
Cmd+Shift+Enter | Maximize / restore active pane |
Cmd+Alt+Enter | Focus mode (hide sidebar, tab bar, panels) |
AI
| Shortcut | Action |
|---|---|
Cmd+Alt+A | Toggle AI Chat panel (toggle-ai-chat) |
Cmd+Enter (panel focused) | Send message |
Esc (panel focused) | Cancel in-flight stream |
Panels
| Shortcut | Action |
|---|---|
Cmd+[ | Toggle sidebar |
Cmd+Shift+D | Toggle Git Panel |
Cmd+Shift+M | Toggle markdown panel |
Cmd+Alt+N | Toggle Ideas panel |
Cmd+E | Toggle file browser |
Cmd+O | Open file… (picker) |
Cmd+N | New file… (picker for name + location) |
Cmd+P | Command palette |
Cmd+Shift+P | Toggle plan panel |
Cmd+, | Open settings |
Cmd+? | Toggle help panel |
Cmd+Shift+K | Prompt library |
Cmd+J | Task queue |
Cmd+Shift+E | Error log |
Cmd+Shift+W | Worktree manager |
Cmd+Shift+A | Activity dashboard |
Cmd+Shift+M | MCP servers popup (per-repo) |
Git
| Shortcut | Action |
|---|---|
Cmd+B | Quick branch switch (fuzzy search) |
Cmd+Shift+D | Git Panel (opens on last active tab) |
Cmd+G | Git Panel — Branches tab |
Branches Panel (when panel is focused)
| Shortcut | Action |
|---|---|
↑ / ↓ | Navigate branches |
Enter | Checkout selected branch |
n | Create new branch |
d | Delete branch |
R | Rename branch (inline edit) |
M | Merge selected into current |
r | Rebase current onto selected |
P | Push branch |
p | Pull current branch |
f | Fetch all remotes |
File Browser (when focused)
| Shortcut | Action |
|---|---|
↑/↓ | Navigate files |
Enter | Open file / enter directory |
Backspace | Go to parent directory |
Cmd+C | Copy file |
Cmd+X | Cut file |
Cmd+V | Paste file |
Cmd+Shift+F | Open file browser and activate content search |
Code Editor (when focused)
| Shortcut | Action |
|---|---|
Cmd+F | Find |
Cmd+G | Find next |
Cmd+Shift+G | Find previous |
Cmd+H | Find and replace |
Cmd+S | Save file |
Ideas Panel (when textarea focused)
| Shortcut | Action |
|---|---|
Enter | Submit idea |
Shift+Enter | Insert newline |
Cmd+V / Ctrl+V | Paste image from clipboard |
Escape | Cancel edit mode |
Quick Switcher
| Shortcut | Action |
|---|---|
Hold Cmd+Ctrl | Show quick switcher overlay |
Cmd+Ctrl+1-9 | Switch to branch by index |
Voice Dictation
| Shortcut | Action |
|---|---|
Hold F5 | Push-to-talk (configurable) |
Mouse Actions
| Action | Where | Effect |
|---|---|---|
| Click | Sidebar branch | Switch to branch |
| Double-click | Sidebar branch name | Rename branch |
| Double-click | Tab name | Rename tab |
| Right-click | Tab | Tab context menu |
| Right-click | Sidebar branch | Branch context menu |
| Right-click | Sidebar repo ⋯ | Repo context menu |
| Right-click | Sidebar group header | Group context menu |
| Right-click | File browser entry | File context menu |
| Middle-click | Tab | Close tab |
| Drag | Tab | Reorder tabs |
| Drag | Sidebar right edge | Resize sidebar |
| Drag | Panel left edge | Resize panel |
| Drag | Split pane divider | Resize panes |
| Drag | Repo onto group | Move repo to group |
| Click | Status bar CWD path | Copy to clipboard |
| Click | PR badge (sidebar/status) | Open PR detail popover |
| Click | CI ring | Open PR detail popover |
| Click | Toolbar bell | Open notifications popover |
| Click | Status bar panel buttons | Toggle panels |
| Hold | Mic button (status bar) | Record dictation |
16. Build & Release
16.1 Makefile Targets
| Target | Description |
|---|---|
dev | Start development server |
build | Build production app |
build-dmg | Build macOS DMG |
sign | Code sign the app |
notarize | Notarize with Apple |
release | Build + sign + notarize |
build-github-release | Build for GitHub release (CI) |
publish-github-release | Publish GitHub release |
github-release | One-command release |
clean | Clean build artifacts |
16.2 CI/CD
- GitHub Actions for cross-platform builds
- macOS code signing and notarization
- Linux:
libasound2-devdependency,-fPICflags - 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/clearTickerAPI with source labels, priority tiers (low <10, normal 10-99, urgent >=100), counter badge, click-to-cycle, right-click popover - Agent-scoped plugins:
agentTypesmanifest 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
.zipfile 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-pluginsrepo) - Fetched on demand with 1-hour TTL cache
- Version comparison for “Update available” detection
- Install/update via download URL
17.4 Deep Links (tuic://)
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 tabtuic://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.versionreports 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 automaticallydata-pinnedattribute on links sets pinned flag- Interactive test page:
docs/examples/sdk-test.html(seedocs/tuic-sdk.mdfor 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 exampleauto-confirm— Auto-respond to Y/N promptsci-notifier— Sound notifications and markdown panelsrepo-dashboard— Read-only state and dynamic markdownreport-watcher— Generic report file watcher with markdown viewerclaude-status— Agent-scoped plugin (agentTypes: ["claude"]) tracking usage and rate limitswiz-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 /sessionswith 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_erroris 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
/commandentries; tap to sendCtrl-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,/statusfor Claude Code) accessible via expandable button - Text command input with 16px font (prevents iOS auto-zoom),
inputmode="text" - Offline retry queue:
write_ptycalls 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_inputstate - 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-capablemeta 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) andPtyExit(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
rodiocrate (Tauri commandplay_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: texton 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 desktopglobal.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 /mcpStreamable 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/listresponse
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
Failedstate - Recovery: successful tool call or health check resets the circuit breaker
19.5 Health Checks
- Background task probes every
Readyupstream every 60 seconds viatools/list(HTTP) or process liveness check (stdio) CircuitOpenupstreams 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
idfield; 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? }joinsBeareras a credential type; endpoints auto-discovered from the resource server’sWWW-Authenticatechallenge when omitted- Completion via native deep link
tuic://oauth-callback?code=…&state=…— callbacks never touch the WebView console TokenManagershared across everyHttpMcpClientrefresh path with a per-upstream semaphore that defeats thundering-herd refresh. 60 s expiry margin;None expires_attreated as validUpstreamError::NeedsOAuth { www_authenticate }transitions the registry toneeds_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
envoverrides applied on top
19.10 SSE Events
upstream_status_changedevents emitted on status transitions (connecting, ready, circuit_open, disabled, failed)tools/list_changednotification emitted when upstream tool lists change, enabling live tool-list updates for connected MCP clients- Delivered via
GET /eventsSSE stream
19.11 Metrics (per upstream, lock-free)
call_count— total tool calls routederror_count— total failed callslast_latency_ms— last observed round-trip time
19.12 Validation
- Names: must match
[a-z0-9_-]+, must be unique - HTTP URLs: must use
http://orhttps://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_filesmerged 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) replacespsfork- Eliminates ~100 fork+exec/min with 5 terminals open
20.5 MCP Concurrent Tool Calls
HttpMcpClientusesRwLockinstead ofMutex- 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-consolefeature 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
-
Add a repository — Click the
+button at the top of the sidebar, or use the “Add Repository” option. Select a git repository folder. -
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.
-
Start typing — The terminal is ready. Your default shell is loaded. Type commands, run AI agents, or execute scripts.
-
Open more tabs — Press
Cmd+T(macOS) orCtrl+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.
Sidebar
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 — Tabs, splits, zoom, detachable tabs, find-in-terminal
- Sidebar — Repos, branches, groups, park repos, quick branch switcher
- AI Agents — Agent detection, rate limits, question detection
- Keyboard Shortcuts — All shortcuts and how to customize them
- Command Palette & Activity Dashboard — Fuzzy-search actions and monitor sessions
- File Browser & Code Editor — Browse files, edit code, git status
- GitHub Integration — PR monitoring, CI rings, notifications
- Git Worktrees — Worktree workflow, configuration
- Branch Management — Checkout, create, delete, merge, rebase, push/pull
- Prompt Library — Template management, variables
- Plugins — Install, browse, and manage plugins
- Voice Dictation — Push-to-talk setup
- Remote Access — Access from a browser on another device
- Settings — All configuration options
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
| Indicator | Meaning |
|---|---|
| Grey dot (dim) | Idle — no session or command never ran |
| Blue pulsing dot | Busy — producing output now |
| Green dot | Done — command completed |
| Purple dot | Unseen — completed while you were viewing another tab (clears when selected) |
| Orange pulsing dot | Question — agent needs user input |
| Red pulsing dot | Error — API error or agent stuck |
| Question icon | Agent is asking a question |
| Progress bar | Operation in progress (OSC 9;4) |
| Amber gradient | Session 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
| Shortcut | Action |
|---|---|
Cmd+Home | Scroll to top |
Cmd+End | Scroll to bottom |
Shift+PageUp | Scroll one page up |
Shift+PageDown | Scroll one page down |
Zoom
Per-terminal font size control:
| Action | Shortcut | Effect |
|---|---|---|
| Zoom in | Cmd+= | +2px font size |
| Zoom out | Cmd+- | -2px font size |
| Reset | Cmd+0 | Back 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.
Navigating Split Panes
- 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:
- Right-click a tab → Detach to Window
- The terminal opens in an independent floating window
- 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:
- Press
Cmd+F— a search overlay appears at the top of the active terminal pane - Type your search query — matches highlight as you type (yellow for all matches, orange for active match)
- Navigate matches:
EnterorCmd+G— Next matchShift+EnterorCmd+Shift+G— Previous match
- Toggle search options: Case sensitive, Whole word, Regex
- Match counter shows “N of M” results
- Press
Escapeto close the search and refocus the terminal
Uses @xterm/addon-search for native integration with the terminal buffer.
Cross-Terminal Search
Search text across all open terminal buffers from the command palette:
- Press
Cmd+Pand type~followed by your search query (e.g.~error) - Results show terminal name, line number, and highlighted match text
- Press
Enteror click a result to switch to that terminal and scroll to the matched line (centered in viewport) - 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+Vwrites 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/.mdxfiles open in the Markdown viewer panel- All other code files open in your configured IDE, at the line number if a
:lineor:line:colsuffix 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.
OSC 8 Hyperlinks
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 row → Switch 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 Group → New 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 Group → Ungrouped
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:
- Creates a git worktree (for non-main branches) if one doesn’t exist
- Shows the branch’s terminals (or creates a new one)
- Hides terminals from the previous branch
Branch Indicators
Each branch row can show:
| Indicator | Meaning |
|---|---|
| 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 and deletions |
| Merged badge | Branches merged into main show a “Merged” badge |
| Question icon | An agent in this branch’s terminal is asking a question |
| Grey icon | No 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:
- Hold
Cmd+Ctrl(macOS) orCtrl+Alt(Windows/Linux) - All branches show numbered badges (1, 2, 3…)
- Press a number (
1–9) to switch to that branch instantly - Release the modifier to dismiss the overlay
Git Quick Actions
When a repo is active, the bottom of the sidebar shows quick action buttons:
- Pull —
git pullin the active terminal - Push —
git push - Fetch —
git fetch - Stash —
git 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.
Navigation
- 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:
| Mode | Order |
|---|---|
| Name (default) | Directories first, then alphabetical |
| Date | Directories 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:
| Color | Label | Meaning |
|---|---|---|
| Orange | mod | Modified (unstaged changes) |
| Green | staged | Staged for commit |
| Blue | new | Untracked (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:
| Mode | Description |
|---|---|
| List (default) | Flat directory listing with breadcrumb navigation and .. parent entry |
| Tree | Collapsible 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.
Filename Search
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).
Content Search
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):
| Toggle | Meaning |
|---|---|
| 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:
| Action | Shortcut | Notes |
|---|---|---|
| Copy Path | — | Copies the full absolute path to the clipboard |
| Copy | Cmd+C | Files only; stores file in the internal clipboard |
| Cut | Cmd+X | Files only; cut entries are shown dimmed |
| Paste | Cmd+V | Pastes into the current directory; disabled when clipboard is empty |
| Rename… | — | Opens a rename dialog; enter the new name and confirm |
| Delete | — | Requires confirmation; directories are deleted recursively |
| Add to .gitignore | — | Appends 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
.mdor.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
- Save —
Cmd+Ssaves 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
- Press
Cmd+P— the palette opens with a search input focused - Type to filter actions by name or category (substring match, case-insensitive)
- Navigate with
↑/↓arrow keys - Press
Enterto execute the selected action - Press
Escapeor 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:
| Prefix | Mode | Description |
|---|---|---|
! | Filename search | Search files by name (min 1 char) |
? | Content search | Search inside file contents (min 3 chars) |
~ | Terminal search | Search 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
Enteror 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:
| Command | Action |
|---|---|
| Search Terminals | Opens palette with ~ prefix |
| Search Files | Opens palette with ! prefix |
| Search in File Contents | Opens 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:
| Column | Description |
|---|---|
| Terminal name | The tab name |
| Agent type | Detected agent (Claude, Aider, etc.) with brand icon |
| Status | Current state with color indicator |
| Last activity | Relative timestamp (“2s ago”, “1m ago”) — auto-refreshes |
Status Colors
| Color | Meaning |
|---|---|
| Green | Agent is actively working |
| Yellow | Agent is waiting for input |
| Red | Agent is rate-limited (with countdown) |
| Gray | Terminal 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"usesCmdas the platform-agnostic modifier (resolved to Meta on macOS, Ctrl on Win/Linux)- Set
"key": ""or"key": nullto 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
CmdandCtrlare 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
| Shortcut | Action |
|---|---|
Cmd+T | New terminal tab |
Cmd+W | Close tab (or close active pane in split mode) |
Cmd+Shift+T | Reopen last closed tab |
Cmd+R | Run saved command |
Cmd+Shift+R | Edit and run command |
Cmd+L | Clear terminal |
Cmd+F | Find in terminal / diff tab |
Cmd+G | Git Panel — Branches tab (or Find next match when search is open) |
Enter | Find next match (when search is open) |
Cmd+Shift+G / Shift+Enter | Find previous match (when search is open) |
Escape | Close search overlay |
Cmd+C | Copy selection |
Cmd+V | Paste to terminal |
Cmd+Home | Scroll to top |
Cmd+End | Scroll to bottom |
Shift+PageUp | Scroll page up |
Shift+PageDown | Scroll page down |
Tab Navigation
| Shortcut | Action |
|---|---|
Cmd+1 through Cmd+9 | Switch to tab by number |
Ctrl+Tab | Next tab |
Ctrl+Shift+Tab | Previous tab |
Zoom
| Shortcut | Action |
|---|---|
Cmd+= (or Cmd++) | Zoom in (active terminal) |
Cmd+- | Zoom out (active terminal) |
Cmd+0 | Reset zoom to default (active terminal) |
Cmd+Shift+= (or Cmd+Shift++) | Zoom in all terminals |
Cmd+Shift+- | Zoom out all terminals |
Cmd+Shift+0 | Reset zoom all terminals |
Font size range: 8px to 32px, step 2px per action.
Split Panes
| Shortcut | Action |
|---|---|
Cmd+\ | Split vertically (side by side) |
Cmd+Alt+\ | Split horizontally (stacked) |
Alt+← / Alt+→ | Navigate panes (vertical split) |
Alt+↑ / Alt+↓ | Navigate panes (horizontal split) |
Cmd+W | Close active pane (collapses to single) |
Cmd+Shift+Enter | Maximize / restore active pane |
Cmd+Alt+Enter | Focus mode — hide sidebar, tab bar, and all side panels (keeps toolbar + status bar) |
Panels
| Shortcut | Action |
|---|---|
Cmd+[ | Toggle sidebar |
Cmd+Shift+D | Toggle Git Panel |
Cmd+Shift+M | Toggle markdown panel |
Cmd+Alt+N | Toggle Ideas panel |
Cmd+E | Toggle file browser |
Cmd+O | Open file… (picker) |
Cmd+N | New file… (picker for name + location) |
Cmd+, | Open settings |
Cmd+? | Toggle help panel |
Cmd+Shift+K | Prompt library |
Cmd+K | Clear scrollback |
Cmd+Shift+W | Worktree Manager |
Cmd+J | Task queue |
Cmd+Shift+P | Toggle plan panel |
Cmd+Shift+E | Toggle error log |
Cmd+Shift+M | MCP servers popup (per-repo) |
Note: File browser and Markdown panels are mutually exclusive — opening one closes the other.
Navigation
| Shortcut | Action |
|---|---|
Cmd+P | Command palette |
Cmd+Shift+A | Activity dashboard |
Git
| Shortcut | Action |
|---|---|
Cmd+Shift+D | Git Panel (opens on last active tab) |
Cmd+G | Git Panel — Branches tab |
Cmd+B | Quick branch switch (fuzzy search) |
Branches Panel (when panel is focused)
| Shortcut | Action |
|---|---|
↑ / ↓ | Navigate branch list |
Enter / double-click | Checkout selected branch |
n | Create new branch (inline form) |
d | Delete branch (safe; hold to force) |
R | Rename branch (inline edit) |
M | Merge selected branch into current |
r | Rebase current onto selected branch |
P | Push branch (auto-sets upstream if missing) |
p | Pull current branch |
f | Fetch all remotes |
Ctrl/Cmd+1–4 | Switch Git Panel tab (1=Changes, 2=Log, 3=Stashes, 4=Branches) |
Quick Branch Switcher
| Shortcut | Action |
|---|---|
Hold Cmd+Ctrl (macOS) or Ctrl+Alt (Win/Linux) | Show quick switcher overlay |
Cmd+Ctrl+1-9 | Switch 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)
| Shortcut | Action |
|---|---|
↑ / ↓ | Navigate files |
Enter | Open file or enter directory |
Backspace | Go to parent directory |
Cmd+C | Copy selected file |
Cmd+X | Cut selected file |
Cmd+V | Paste file into current directory |
Code Editor (when editor tab is focused)
| Shortcut | Action |
|---|---|
Cmd+S | Save file |
Ideas Panel (when textarea is focused)
| Shortcut | Action |
|---|---|
Enter | Submit idea |
Shift+Enter | Insert newline |
Voice Dictation
| Shortcut | Action |
|---|---|
Hold F5 | Push-to-talk (configurable in Settings) |
Hold to record, release to transcribe and inject text into active terminal.
Tab Context Menu (Right-click on tab)
| Action | Shortcut |
|---|---|
| Close Tab | Cmd+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
| Action | Where | Effect |
|---|---|---|
| Click | Sidebar branch | Switch to branch |
| Double-click | Sidebar branch name | Rename branch |
| Double-click | Tab name | Rename tab |
| Right-click | Tab | Context menu |
| Right-click | Sidebar branch | Branch context menu |
| Middle-click | Tab | Close tab |
| Drag | Tab | Reorder tabs |
| Drag | Sidebar right edge | Resize sidebar (200-500px) |
| Click | PR badge / CI ring | Open PR detail popover |
| Click | Status bar CWD path | Copy path to clipboard |
| Click | Status bar panel buttons | Toggle Git/MD/FB/Ideas panels |
| Drag | Panel left edge | Resize right-side panel (200-800px) |
| Drag | Split pane divider | Resize split terminal panes |
Action Names Reference (for keybindings.json)
| Action Name | Default Shortcut | Description |
|---|---|---|
zoom-in | Cmd+= | Zoom in |
zoom-out | Cmd+- | Zoom out |
zoom-reset | Cmd+0 | Reset zoom |
zoom-in-all | Cmd+Shift+= | Zoom in all terminals |
zoom-out-all | Cmd+Shift+- | Zoom out all terminals |
zoom-reset-all | Cmd+Shift+0 | Reset zoom all terminals |
new-terminal | Cmd+T | New terminal tab |
close-terminal | Cmd+W | Close terminal/pane |
reopen-closed-tab | Cmd+Shift+T | Reopen closed tab |
clear-terminal | Cmd+L | Clear terminal |
run-command | Cmd+R | Run saved command |
edit-command | Cmd+Shift+R | Edit and run command |
split-vertical | Cmd+\ | Split vertically |
split-horizontal | Cmd+Alt+\ | Split horizontally |
prev-tab | Ctrl+Shift+Tab | Previous tab |
next-tab | Ctrl+Tab | Next tab |
switch-tab-1..9 | Cmd+1..9 | Switch to tab N |
toggle-sidebar | Cmd+[ | Toggle sidebar |
toggle-markdown | Cmd+Shift+M | Toggle markdown panel |
toggle-notes | Cmd+Alt+N | Toggle ideas panel |
open-file | Cmd+O | Open file picker |
new-file | Cmd+N | Create new file |
toggle-file-browser | Cmd+E | Toggle file browser |
prompt-library | Cmd+Shift+K | Prompt library |
toggle-settings | Cmd+, | Open settings |
toggle-task-queue | Cmd+J | Task queue |
toggle-help | Cmd+? | Toggle help panel |
toggle-git-ops | Cmd+Shift+D | Git Panel |
toggle-git-branches | Cmd+G | Git Panel — Branches tab |
worktree-manager | Cmd+Shift+W | Worktree Manager panel |
quick-branch-switch | Cmd+B | Quick branch switch |
find-in-terminal | Cmd+F | Find in terminal |
command-palette | Cmd+P | Command palette |
activity-dashboard | Cmd+Shift+A | Activity dashboard |
toggle-error-log | Cmd+Shift+E | Toggle error log |
toggle-mcp-popup | Cmd+Shift+M | MCP servers popup (per-repo) |
toggle-plan | Cmd+Shift+P | Toggle plan panel |
switch-branch-1..9 | Cmd+Ctrl+1..9 | Switch to branch N |
scroll-to-top | Cmd+Home | Scroll to top |
scroll-to-bottom | Cmd+End | Scroll to bottom |
scroll-page-up | Shift+PageUp | Scroll page up |
scroll-page-down | Shift+PageDown | Scroll page down |
zoom-pane | Cmd+Shift+Enter | Maximize/restore pane |
toggle-focus-mode | Cmd+Alt+Enter | Focus mode — hide sidebar/tab bar/panels |
toggle-file-browser-content-search | Cmd+Shift+F | File content search |
toggle-diff-scroll | Cmd+Shift+G | Diff scroll view |
toggle-global-workspace | Cmd+Shift+X | Toggle global workspace |
Settings
Open settings with Cmd+,. Settings are organized into tabs.
General Tab
| Setting | Description |
|---|---|
| Language | UI language |
| Default IDE | IDE 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.) |
| Shell | Custom shell path (e.g., /bin/zsh, /usr/local/bin/fish). Leave empty for system default. |
| Confirm before quitting | Show dialog when closing app with active terminals |
| Confirm before closing tab | Ask before closing terminal tab |
| Prevent sleep when busy | Keep machine awake while agents are working |
| Auto-check for updates | Check for new versions on startup |
| Auto-show PR popover | Automatically 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 defaults | Base branch, file handling, setup/run scripts applied to new repos |
Appearance Tab
| Setting | Type | Default | Description |
|---|---|---|---|
| Terminal theme | — | — | Color theme with preview swatches |
| Terminal font | — | JetBrains Mono | 13 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 size | — | — | 8–32px slider. Applies to new terminals; existing terminals keep their zoom level. |
| Split tab mode | — | — | Separate or unified tab appearance |
| Max tab name length | — | — | 10–60 slider |
| Repository groups | — | — | Create, rename, delete, and color-code groups |
| Reset panel sizes | — | — | Restore sidebar and panel widths to defaults |
| Copy on Select | boolean | true | Auto-copy terminal selection to clipboard |
| Bell Style | none/visual/sound/both | visual | Terminal bell behavior |
Agents Tab
Each supported agent has an expandable row showing detection status, version, and MCP badge.
| Setting | Description |
|---|---|
| Agent Detection | Auto-detects running agents from terminal output patterns. Shows “Available” or “Not found” for each agent. |
| Run Configurations | Custom 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 Integration | Install/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:
| Setting | Description |
|---|---|
| OAuth Login | Device Flow login — click “Sign in with GitHub”, enter code on github.com. Token stored in OS keyring. |
| Auth Status | Shows current login, avatar, token source (OAuth/env/CLI), and available scopes |
| Disconnect | Clear all GitHub tokens (keyring + env cache). Falls back to next available source. |
| Diagnostics | Token source details, scope verification, API connectivity check |
| Issue Filter | Which issues to show in the GitHub panel: Assigned (default), Created, Mentioned, All, or Disabled |
| Auto-show PR popover | Automatically show PR detail popover when opening a branch with an active PR |
| Auto-delete on PR close | Off (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-bridgesidecar (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.jsonin 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
| Agent | Binary | Resume Command | Session Binding |
|---|---|---|---|
| Claude Code | claude | claude --continue | claude --resume $TUIC_SESSION |
| Codex CLI | codex | codex resume --last | codex resume $TUIC_SESSION |
| Aider | aider | aider --restore-chat-history | — |
| Gemini CLI | gemini | gemini --resume | gemini --resume $TUIC_SESSION |
| OpenCode | opencode | opencode -c | — |
| Amp | amp | amp threads continue | — |
| Cursor Agent | cursor-agent | cursor-agent resume | — |
| Warp Oz | oz | — | — |
| 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:
- Changes the tab indicator to a
?icon - Shows a prompt overlay with keyboard navigation:
↑/↓to navigate optionsEnterto select- Number keys
1-9for numbered options Escapeto dismiss
- 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
- When a terminal tab is created, a UUID is generated via
crypto.randomUUID() - The UUID is saved with the tab and restored when the app restarts
- On PTY creation, the UUID is injected as
TUIC_SESSION=<uuid>in the shell environment - Agents can use
$TUIC_SESSIONfor 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:
- Verified session — If
$TUIC_SESSIONmaps to an existing session file (e.g.~/.claude/projects/…/<uuid>.jsonl), the agent resumes with--resume <uuid> - No session file — Falls back to the agent’s default resume behavior (e.g.
claude --continuefor the last session) - No agent detected — Tab opens a plain shell;
$TUIC_SESSIONis 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 Settings → General → Prevent sleep when busy
- Uses the
keepawakesystem 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.
Navigating Teammates
Claude Code supports two display modes for teammates:
| Mode | How it works | Requirement |
|---|---|---|
| In-process | All teammates run inside the lead’s terminal. Use Shift+Down to cycle between them. | None |
| Split panes | Each 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)
| Key | Action |
|---|---|
Shift+Down | Cycle to next teammate |
Enter | View a teammate’s session |
Escape | Interrupt a teammate’s current turn |
Ctrl+T | Toggle 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 —
/resumedoes 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_TEAMSshould print1 - 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 / Flag | Value | Purpose |
|---|---|---|
TUIC_SESSION | Stable UUID per tab | Agent identity for messaging |
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS | 1 | Unlocks TeamCreate/TaskCreate/SendMessage |
--dangerously-load-development-channels server:tuicommander | (CLI flag, agent spawn only) | Enables real-time channel push from TUICommander |
Messaging Flow
-
Register — The agent reads its
$TUIC_SESSIONand registers as a peer:messaging action=register tuic_session="$TUIC_SESSION" name="worker-1" project="/path/to/repo" -
Discover peers — Find other agents connected to TUICommander:
messaging action=list_peers messaging action=list_peers project="/path/to/repo" # filter by repo -
Send a message — Address by the recipient’s
tuic_sessionUUID:messaging action=send to="<recipient-tuic-session>" message="PR review done, 3 issues found" -
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
| Delivery | When | Latency | Requires |
|---|---|---|---|
| Channel push | Recipient has active SSE stream | Real-time | --dangerously-load-development-channels server:tuicommander on the recipient’s CC process |
| Inbox buffer | Always | Poll-based | Registration 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:
-
Set
TUIC_SESSION— export a stable UUID:export TUIC_SESSION=$(uuidgen) -
Connect to TUIC’s MCP server — add to your
.mcp.json:{ "mcpServers": { "tuicommander": { "url": "http://localhost:17463/mcp" } } } -
Enable channel push (optional, for real-time delivery):
claude --dangerously-load-development-channels server:tuicommander -
Register on first turn — the agent must call
messaging action=registerwith its$TUIC_SESSIONbefore sending or receiving.
Messaging vs Claude Code Native SendMessage
| Feature | TUIC Messaging | CC Native SendMessage |
|---|---|---|
| Transport | MCP tool call → server-side routing | File append + polling (~/.claude/teams/) |
| Real-time push | Yes (MCP channel notifications) | No (polling only) |
| Cross-app | Any MCP client can participate | Claude Code processes only |
| Discovery | list_peers with project filter | Team config file |
| Persistence | In-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
| Category | Prompts |
|---|---|
| Git & Commit | Smart Commit, Commit & Push, Amend Commit, Generate Commit Message |
| Code Review | Review Changes, Review Staged, Review PR, Address Review Comments |
| Pull Requests | Create PR, Update PR Description, Generate PR Description |
| Merge & Conflicts | Resolve Conflicts, Merge Main Into Branch, Rebase on Main |
| CI & Quality | Fix CI Failures, Fix Lint Issues, Write Tests, Run & Fix Tests |
| Investigation | Investigate Issue, What Changed?, Summarize Branch, Explain Changes |
| Code Operations | Suggest 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)
| Variable | Description |
|---|---|
{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)
| Variable | Description |
|---|---|
{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
| Variable | Description |
|---|---|
{agent_type} | Active agent type (claude, aider, codex, etc.) |
{cwd} | Active terminal working directory |
Manual Input Variables
| Variable | Description |
|---|---|
{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:
| Tab | Shows |
|---|---|
| All | Every saved prompt, sorted by most recently used |
| Custom | User-created prompts |
| Favorites | Prompts you have starred |
| Recent | Last 10 prompts you used |
Keyboard Navigation
| Key | Action |
|---|---|
↑ / ↓ | Move selection up/down |
Enter | Insert selected prompt into terminal |
| Double-click | Insert and immediately execute (adds newline) |
Ctrl+N / Cmd+N | Create a new prompt |
Ctrl+E / Cmd+E | Edit the selected prompt |
Ctrl+F / Cmd+F | Toggle favorite on the selected prompt |
Escape | Close the drawer |
Creating a Prompt
- Open the drawer (
Cmd+Shift+K) and click + New Prompt, or pressCtrl+N/Cmd+N - 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
- 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:
| Variable | Value |
|---|---|
{{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
- Open Settings → Services → Voice Dictation
- Enable dictation
- Download a Whisper model (recommended:
large-v3-turbo, ~1.6 GB) - Wait for download to complete (progress shown in UI)
- Optionally configure language and hotkey
Usage
Push-to-talk workflow:
- Hold the dictation hotkey (default:
F5) or the mic button in the status bar - Speak your text
- Release the key/button
- 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
| Model | Size | Quality |
|---|---|---|
| tiny | ~75 MB | Low (fast, inaccurate) |
| base | ~140 MB | Fair |
| small | ~460 MB | Good |
| medium | ~1.5 GB | Very good |
| large-v3-turbo | ~1.6 GB | Best (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:
| Spoken | Replaced 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
| Indicator | Meaning |
|---|---|
| Mic button (status bar) | Click/hold to start recording |
| Recording animation | Audio is being captured |
| Processing spinner | Whisper is transcribing |
| Model downloading | Progress 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:
| Key | Action |
|---|---|
↑ / ↓ | Navigate branches |
Enter | Checkout selected branch |
n | Create new branch |
d | Delete selected branch |
R | Rename selected branch (inline edit) |
M | Merge selected branch into current |
r | Rebase current branch onto selected |
P | Push selected branch |
p | Pull current branch |
f | Fetch all remotes |
Escape | Close panel |
Switching between Git Panel tabs:
| Key | Tab |
|---|---|
Ctrl/Cmd+1 | Changes |
Ctrl/Cmd+2 | Log |
Ctrl/Cmd+3 | Stashes |
Ctrl/Cmd+4 | Branches |
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:
- Type the new branch name
- Optionally change the start point (defaults to HEAD)
- Toggle “Checkout after create” (on by default)
- Press
Enterto confirm orEscapeto 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:
| Action | Description |
|---|---|
| Checkout | Switch to this branch |
| Create Branch from Here | Create a new branch starting from this commit |
| Delete | Delete branch (safe by default) |
| Rename | Rename inline |
| Merge into Current | Merge this branch into the current one |
| Rebase Current onto This | Rebase current branch onto this one |
| Push | Push this branch |
| Pull | Pull this branch |
| Fetch | Fetch all remotes |
| Compare | Show 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:
- TUICommander creates a git worktree for that branch
- A terminal opens in the worktree directory
- 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):
| Strategy | Location | Use 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)
| Setting | Options | Default |
|---|---|---|
| Storage | Sibling / App directory / Inside repo | Sibling |
| Prompt on create | On / Off | On |
| Delete branch on remove | On / Off | On |
| Auto-archive merged | On / Off | Off |
| Orphan cleanup | Manual / Prompt / Auto | Manual |
| PR merge strategy | Merge / Squash / Rebase | Merge |
| After merge | Archive / Delete / Ask | Archive |
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:
- Merge the branch into the main branch
- 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
- Archive: Moves the worktree directory to
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 -con macOS/Linux,cmd /Con 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:
- Closes all terminals associated with that branch
- Runs
git worktree removeto clean up - 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 directorysuggested_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:
Option 1: OAuth Login (Recommended)
- Open Settings > GitHub
- Click “Login with GitHub”
- A code appears — it’s auto-copied to your clipboard
- Your browser opens GitHub’s authorization page
- Paste the code and authorize
- 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:
GH_TOKENenvironment variableGITHUB_TOKENenvironment variable- OAuth token (from Settings login)
ghCLI 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:
| Color | State |
|---|---|
| Green | Open PR |
| Purple | Merged |
| Red | Closed |
| Gray/dim | Draft |
Click the PR badge to open the PR detail popover.
CI Ring
A circular indicator showing CI check status:
| Segment | Color | Meaning |
|---|---|---|
| Green arc | — | Passed checks |
| Red arc | — | Failed checks |
| Yellow arc | — | Pending 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
| Type | Meaning |
|---|---|
| Merged | PR was merged |
| Closed | PR was closed without merge |
| Conflicts | Merge conflicts detected |
| CI Failed | One or more CI checks failed |
| Changes Req. | Reviewer requested changes |
| Ready | PR 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
| State | Label | Meaning |
|---|---|---|
| MERGEABLE + CLEAN | Ready to merge | All checks pass, no conflicts |
| MERGEABLE + UNSTABLE | Checks failing | Mergeable but some checks fail |
| CONFLICTING | Has conflicts | Merge conflicts with base branch |
| BEHIND | Behind base | Base branch has newer commits |
| BLOCKED | Blocked | Branch protection prevents merge |
| DRAFT | Draft | PR is in draft state |
Review State Classification
| Decision | Label |
|---|---|
| APPROVED | Approved |
| CHANGES_REQUESTED | Changes requested |
| REVIEW_REQUIRED | Review 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.
| Mode | Behavior |
|---|---|
| Off (default) | No action taken |
| Ask | Shows a confirmation dialog before deleting |
| Auto | Deletes 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:
| Button | When Shown | What It Does |
|---|---|---|
| View Diff | Always | Opens PR diff in a dedicated panel tab |
| Merge | PR is open, approved, CI green | Merges via GitHub API (auto-detects allowed merge method) |
| Approve | Remote-only PRs | Submits an approving review via GitHub API |
Post-Merge Cleanup
After merging a PR from the popover, a cleanup dialog appears with checkable steps:
- 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
- Pull base branch — fast-forward only
- Delete local branch — closes terminals first, refuses to delete default branch
- 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:
| Filter | Shows |
|---|---|
| Assigned (default) | Issues assigned to you |
| Created | Issues you opened |
| Mentioned | Issues that mention you |
| All | All open issues in the repo |
| Disabled | Hides 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
| Action | Description |
|---|---|
| Open in GitHub | Opens the issue in your browser |
| Close / Reopen | Changes issue state via GitHub API |
| Copy number | Copies #123 to clipboard |
Troubleshooting
No PR data showing:
- Check
gh auth status— must be authenticated - Check repository has a GitHub remote (
git remote -v) - Check that
gh pr listworks 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
- Open Settings (
Cmd+,) → Plugins tab → Browse - Browse available plugins — each shows name, description, and author
- Click Install on any plugin
- The plugin is downloaded and activated immediately
An “Update available” badge appears when a newer version exists in the registry.
From a ZIP File
- Open Settings → Plugins → Installed
- Click Install from file…
- Select a
.ziparchive containing the plugin
Via Deep Link
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:
| Tier | Access | Examples |
|---|---|---|
| 1 | Always available | Watch terminal output, add Activity Center items, provide markdown content |
| 2 | Always available | Read repository list, active branch, terminal sessions (read-only) |
| 3 | Requires capability | Send 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) |
| 4 | Requires capability | Invoke 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/:
| Plugin | What it does |
|---|---|
hello-world | Minimal example — watches terminal output, adds Activity Center items |
auto-confirm | Auto-responds to Y/N prompts in terminal |
ci-notifier | Sound notifications and markdown panels for CI events |
repo-dashboard | Reads repo state, generates dynamic markdown summaries |
report-watcher | Watches 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
| Problem | Fix |
|---|---|
| Plugin not appearing | Check 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 effect | Save the file again to trigger hot reload, or restart the app |
| Plugin errors | Check 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.
| Field | Example | Notes |
|---|---|---|
| Name | github | Lowercase letters, digits, hyphens, underscores only |
| Type | HTTP | |
| URL | https://mcp.example.com/mcp | Must be http:// or https:// |
| Timeout | 30 | Seconds per request. 0 = no timeout |
| Enabled | On | Uncheck to disable without removing |
Stdio Server
Use this for locally installed MCP servers (npm packages, Python scripts, etc.) that communicate over stdin/stdout.
| Field | Example | Notes |
|---|---|---|
| Name | filesystem | Same naming rules as above |
| Type | Stdio | |
| Command | npx | Executable name or full path |
| Args | -y @modelcontextprotocol/server-filesystem | Space-separated |
| Env | ALLOWED_PATHS=/home/user | Optional extra environment variables |
| Enabled | On |
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:
- Go to Settings > Services > MCP Upstreams
- Find your server in the list
- Click the key icon next to it
- 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:
| Status | Meaning |
|---|---|
| Connecting | Handshake in progress |
| Ready | Connected, tools available |
| Circuit Open | Too many failures, retrying with backoff |
| Disabled | Disabled by you in config |
| Failed | Permanently 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”
- Check the server URL or command is correct.
- Verify the server process is running (for stdio servers).
- Check credentials are set if the server requires authentication.
- Click Reconnect to retry.
Tools are not appearing
- The upstream must be in
Readystatus 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
Argsfield 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:
- Go to Settings > Services > MCP Upstreams.
- Click the key icon for the server.
- 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. OnlyPATH,HOME,USER,LANG,LC_ALL,TMPDIR,TEMP,TMP,SHELL, andTERMare passed through. Add anything else explicitly in theEnvfield. - Self-referential HTTP URLs (pointing to TUIC’s own MCP port) are rejected to prevent circular proxying.
- Only
http://andhttps://URL schemes are accepted.
Remote Access
Access TUICommander from a browser on another device on your network.
Setup
- Open Settings (
Cmd+,) → Services → Remote Access - Configure:
- Port — Default
9876(range 1024–65535) - Username — Basic Auth username
- Password — Basic Auth password (stored as a bcrypt hash, never in plaintext)
- Port — Default
- Enable remote access
Once enabled, the settings panel shows the access URL: http://<your-ip>:<port>
Connecting from Another Device
- Open a browser on any device on the same network
- Navigate to the URL shown in settings (e.g.,
http://192.168.1.42:9876) - Enter the username and password you configured
- 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.sockon macOS/Linux, or named pipe\\.\pipe\tuicommander-mcpon Windows - AI agents connect via the
tuic-bridgesidecar 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_enabledtoggle in Settings → Services 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
- Enable remote access (see Setup above)
- Navigate to
http://<your-ip>:<port>/mobilefrom your phone - 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
| Problem | Fix |
|---|---|
| Can’t connect from another device | Check that both devices are on the same network. Try pinging the host IP. |
| Connection refused | Verify the port isn’t blocked by a firewall. The settings panel includes a reachability check. |
| Authentication fails | Re-enter the password in settings — the stored bcrypt hash may be from a different password. |
| Terminals not responding | WebSocket connection may have dropped. Refresh the browser page. |
Architecture Overview
Tech Stack
| Layer | Technology | Purpose |
|---|---|---|
| Frontend | SolidJS + TypeScript | Reactive UI with fine-grained updates |
| Build | Vite + LightningCSS | Fast dev server, optimized CSS |
| Backend | Tauri (Rust) | Native APIs, PTY, git, system integration |
| Terminal | xterm.js + WebGL | GPU-accelerated terminal rendering |
| State | SolidJS reactive stores | Frontend state management |
| Persistence | JSON files via Rust | Platform-specific config directory |
| Testing | Vitest + SolidJS Testing Library | Unit/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
- Rust (
main.rs): Callstui_commander_lib::run() - Library (
lib.rs): CreatesAppState, loads config, spawns HTTP server if enabled, builds Tauri app with plugins, registers 73+ commands, sets up native menu - Frontend (
index.tsx): Mounts<App />component - App (
App.tsx): Initializes all hooks, callsinitApp()which hydrates stores from backend, detects binaries, sets up keyboard shortcuts, starts GitHub polling - 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)
| Event | Payload | Source |
|---|---|---|
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:
DashMapfor lock-free concurrent read/write of session mapsMutexfor interior mutability of individual PTY writers and buffersArc<AtomicBool>for pause/resume signaling per sessionAtomicUsizefor 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
| Buffer | Purpose | Capacity |
|---|---|---|
Utf8ReadBuffer | Accumulates bytes until valid UTF-8 boundary | Variable |
EscapeAwareBuffer | Holds incomplete ANSI escape sequences | Variable |
OutputRingBuffer | Circular buffer for MCP output access | 64 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
| Store | File | Purpose | Persisted |
|---|---|---|---|
terminalsStore | terminals.ts | Terminal instances, active tab, split layout | Partial (IDs in repos) |
repositoriesStore | repositories.ts | Saved repos, branches, terminal associations, repo groups | repositories.json |
settingsStore | settings.ts | App settings (font, shell, IDE, theme, update channel) | config.json |
repoSettingsStore | repoSettings.ts | Per-repository settings (scripts, worktree) | repo-settings.json |
repoDefaultsStore | repoDefaults.ts | Default settings for new repositories | repo-defaults.json |
uiStore | ui.ts | Panel visibility, sidebar width | ui-prefs.json |
githubStore | github.ts | PR/CI data per branch, remote tracking (ahead/behind), PR state transitions | Not persisted |
promptLibraryStore | promptLibrary.ts | Prompt templates | prompt-library.json |
notificationsStore | notifications.ts | Notification preferences | notification-config.json |
dictationStore | dictation.ts | Dictation config and state | dictation-config.json |
errorHandlingStore | errorHandling.ts | Error retry config | ui-prefs.json |
rateLimitStore | ratelimit.ts | Active rate limits | Not persisted |
tasksStore | tasks.ts | Agent task queue | Not persisted |
promptStore | prompt.ts | Active prompt overlay state | Not persisted |
diffTabsStore | diffTabs.ts | Open diff tabs | Not persisted |
mdTabsStore | mdTabs.ts | Open markdown tabs and plugin panels | Not persisted |
notesStore | notes.ts | Ideas/notes with repo tagging and used-at tracking | notes.json |
statusBarTicker | statusBarTicker.ts | Priority-based rotating status bar messages | Not persisted |
userActivityStore | userActivity.ts | Tracks last user click/keydown for activity-based timeouts | Not persisted |
updaterStore | updater.ts | App update state (check, download, install) | Not persisted |
keybindingsStore | keybindings.ts | Custom keyboard shortcut bindings | keybindings.json |
agentConfigsStore | agentConfigs.ts | Per-agent run configs and toggles | agents.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).
| API | Returns |
|---|---|
__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:
| Platform | Path |
|---|---|
| macOS | ~/Library/Application Support/tuicommander/ |
| Linux | ~/.config/tuicommander/ |
| Windows | %APPDATA%/tuicommander/ |
Legacy path ~/.tuicommander/ is auto-migrated on first launch.
Config File Map
| File | Contents | Rust Type |
|---|---|---|
config.json | Shell, font, theme, MCP, remote access, update channel | AppConfig |
notification-config.json | Sound preferences, volume | NotificationConfig |
ui-prefs.json | Sidebar, error handling settings | UIPrefsConfig |
repo-settings.json | Per-repo scripts, worktree options | RepoSettingsMap |
repo-defaults.json | Default settings for new repos (base branch, scripts) | RepoDefaultsConfig |
repositories.json | Saved repos, branches, groups | serde_json::Value |
prompt-library.json | Prompt templates | PromptLibraryConfig |
dictation-config.json | Dictation on/off, hotkey, language, model | DictationConfig |
notes.json | Ideas/notes with repo tags and used-at timestamps | serde_json::Value |
keybindings.json | Custom keyboard shortcut overrides | serde_json::Value |
agents.json | Per-agent run configs and toggles | AgentsConfig |
claude-usage-cache.json | Incremental JSONL parse offsets for session stats | SessionStatsCache |
Terminal State Machine
Definitive reference for terminal activity states, notifications, and question detection.
State Variables
Each terminal has these reactive fields in terminalsStore:
| Field | Type | Default | Source of truth |
|---|---|---|---|
shellState | "busy" | "idle" | null | null | Rust (emitted as parsed event) |
awaitingInput | "question" | "error" | null | null | Frontend (from parsed events) |
awaitingInputConfident | boolean | false | Frontend (from Question event) |
activeSubTasks | number | 0 | Rust (parsed + stored per session) |
debouncedBusy | boolean | false | Frontend (derived from shellState with 2s hold) |
unseen | boolean | false | Frontend (set by fireCompletion, cleared on tab focus) |
agentType | AgentType | null | null | Frontend (from agent detection) |
Rust-side per-session state:
| Field | Location | Purpose |
|---|---|---|
SilenceState.last_output_at | pty.rs | Timestamp of last real output (not mode-line ticks) |
SilenceState.last_chunk_at | pty.rs | Timestamp of last chunk of any kind (real or chrome-only). Used by backup idle timer to detect reader thread activity. |
SilenceState.last_status_line_at | pty.rs | Timestamp of last spinner/status-line |
SilenceState.pending_question_line | pty.rs | Candidate ?-ending line for silence detection |
SilenceState.output_chunks_after_question | pty.rs | Staleness counter: real-output chunks since last ? candidate |
SilenceState.question_already_emitted | pty.rs | Prevents re-emission of the same question |
SilenceState.suppress_echo_until | pty.rs | Deadline to ignore PTY echo of user-typed ? lines |
active_sub_tasks | AppState.session_states | Sub-agent count per session |
shell_states | AppState.shell_states | DashMap<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_ms | AppState.last_output_ms | Epoch 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
| From | To | Trigger | Condition |
|---|---|---|---|
null | busy | First real output chunk | — |
busy | idle | Chrome-only chunk or silence timer | last_output_at > threshold (500ms shell / 2.5s agent) AND active_sub_tasks == 0 AND not resize grace |
idle | busy | Real output chunk | — |
busy | idle | Session ends (reader thread exit) | Always (cleanup) |
| any | null | Terminal removed from store | cleanup |
What does NOT cause transitions
| Event | Why 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 event | Updates counter, doesn’t produce real output |
| Resize redraw | Suppressed 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
| Event | debouncedBusy effect |
|---|---|
| shellState → busy | Immediately true. Cancel any running cooldown. Record busySince (first time only). |
| shellState → idle | Start 2s cooldown. If cooldown expires: set false, fire onBusyToIdle(id, duration). |
| shellState → busy during cooldown | Cancel 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
| Trigger | Clears “question”? | Clears “error”? | Why |
|---|---|---|---|
| StatusLine parsed event | Yes | Yes | Agent is working again (showing a task) |
| Progress parsed event | Yes | Yes | Agent is making progress |
User keystroke (terminal.onData) | Yes | Yes | User typed something — prompt answered |
| shellState idle → busy | Yes | No | Agent resumed real output (reliable post-refactor since mode-line ticks no longer cause idle→busy) |
| Process exit | Yes | Yes | Session over |
What does NOT clear awaitingInput
| Event | Why it doesn’t clear |
|---|---|
| shellState idle → busy | Clears "question" but not "error". API errors are persistent and need explicit agent activity (status-line) or process exit to clear. |
| Mode-line tick | Chrome-only output, not agent activity |
| activeSubTasks change | Sub-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
| Event | Effect |
|---|---|
ActiveSubtasks { count: N } parsed event | Set to N |
UserInput parsed event | Reset to 0 (new agent cycle) |
| Process exit | Reset 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 detection → Frontend 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 type | last_chunk_at | last_output_at | last_status_line_at | staleness counter | pending_question_line |
|---|---|---|---|---|---|
| Real output, no ‘?’ | Reset to now | Reset to now | — | +1 (if pending exists) | Cleared if >10 |
| Real output with ‘?’ | Reset to now | Reset to now | — | Reset to 0 | Set to new line |
| Real output + status-line | Reset to now | Reset to now | Reset to now | (per above rules) | (per above rules) |
| Mode-line tick only | Reset to now | Not reset | Not reset | Not incremented | Not affected |
| Regex question fired | Reset to now | Reset to now | — | Reset to 0 | Cleared (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
| Constant | Value | Location | Purpose |
|---|---|---|---|
| Shell idle threshold | 500ms | pty.rs (Rust) | Real output silence before idle (plain shell) |
| Agent idle threshold | 2.5s | pty.rs (Rust) | Real output silence before idle (agent sessions) |
| Debounce hold | 2s | terminals.ts | debouncedBusy hold after idle |
| Silence question threshold | 10s | pty.rs | Silence before ‘?’ line → question |
| Silence check interval | 1s | pty.rs | Timer thread wake frequency |
| Backup idle chunk threshold | 2s | pty.rs | Skip backup idle if any chunk arrived within this window |
| Stale question chunks | 10 | pty.rs | Real-output chunks before discarding ‘?’ candidate |
| Resize grace | 1s | pty.rs | Suppress all events after resize |
| Echo suppress window | 500ms | pty.rs | Ignore PTY echo of user-typed ‘?’ lines |
| Screen verify rows | 5 | pty.rs | Bottom N rows checked for screen verification |
| Completion threshold | 5s | App.tsx | Minimum busy duration for completion notification |
| Completion deferral | 10s | App.tsx | Extra 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
| File | Responsibility |
|---|---|
src-tauri/src/pty.rs | SilenceState, spawn_silence_timer, shellState derivation, extract_question_line, verify_question_on_screen, extract_last_chat_line, spawn_reader_thread |
src-tauri/src/output_parser.rs | parse_question (INK_FOOTER_RE), parse_active_subtasks, ParsedEvent enum |
src-tauri/src/state.rs | AppState (includes shell_state, active_sub_tasks maps) |
src/stores/terminals.ts | shellState, awaitingInput, debouncedBusy, handleShellStateChange, onBusyToIdle |
src/components/Terminal/Terminal.tsx | handlePtyData (xterm write), pty-parsed event handler, notification effect |
src/components/Terminal/awaitingInputSound.ts | getAwaitingInputSound edge detection |
src/App.tsx | onBusyToIdle → completion notification with deferral + guards |
src/stores/notifications.ts | play(), playQuestion(), playCompletion() etc. |
src/components/TabBar/TabBar.tsx | Tab indicator class priority logic |
src/components/TabBar/TabBar.module.css | Indicator 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:
- Shared concepts and detection strategies
- Code architecture and known gaps
- 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:
| Agent | UI Type | Parsing Strategy | chrome.rs applies? |
|---|---|---|---|
| Claude Code | CLI inline (Ink) | Changed-rows delta analysis | Yes |
| Codex CLI | CLI inline (Ink) | Changed-rows delta analysis | Yes |
| OpenCode | Full-screen TUI (Bubble Tea) | Screen snapshot analysis | No (all rows are “chrome”) |
| Gemini CLI | CLI inline | Changed-rows delta analysis | Yes |
| Aider | CLI sequential | Changed-rows delta analysis | Yes |
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:
| Agent | Prompt char | Unicode |
|---|---|---|
| Claude Code | ❯ | U+276F |
| Codex CLI | › | U+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.
| Agent | Uses separators | Style |
|---|---|---|
| Claude Code | Yes | ──── around prompt box |
| Codex CLI | Partially | ──── between tool output and summary only |
| Gemini CLI | No | — |
| Aider | No | — |
Interactive Menu Detection
All observed agent menus share the pattern Esc to in their footer:
| Footer variant | Agent / Context |
|---|---|
Esc to cancel · Tab to amend | CC permission prompt |
Enter to select · Tab/Arrow keys to navigate · Esc to cancel | CC custom Ink menu |
↑↓ to navigate · Enter to confirm · Esc to cancel | CC built-in (/mcp) |
Esc to cancel · r to cycle dates · ctrl+s to copy | CC built-in (/stats) |
←/→ tab to switch · ↓ to return · Esc to close | CC built-in (/status) |
Enter to select · ↑/↓ to navigate · Esc to cancel | CC Ink select |
esc again to edit previous message | Codex (after interrupt) |
Esc to is the most reliable cross-agent signal for “interactive menu active.”
OSC Sequences
Terminal escape sequences that carry structured metadata:
| Sequence | Purpose | Agent |
|---|---|---|
\033]777;notify;Claude Code;...\007 | User attention notification | CC |
\033]0;...\007 | Window title (task name + spinner) | CC, Codex |
\033]8;;url\007 | Hyperlink | CC |
\033]9;4;N;\007 | Progress notification | CC |
\033]10;?\033\\ | Query foreground color | Codex |
\033]11;?\033\\ | Query background color | Codex |
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
| Pipeline | File | What it uses from chrome.rs |
|---|---|---|
| Changed-rows parser | pty.rs | is_chrome_row (for chrome_only), is_separator_line, is_prompt_line |
| Screen trim (REST) | session.rs | find_chrome_cutoff (replaces local trim_screen_chrome body) |
| Log trim (mobile) | state.rs | find_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_mstimestamp updatesSHELL_BUSY→SHELL_IDLEtransitionsSilenceState::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:
- Old format:
⏵⏵ <mode> · N <type>— markers first, count last - New format:
N <type> · ⏵⏵ <mode>— count first, markers last - Count only:
N <type>— no markers at all (e.g.,1 shell) - 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:
- Scan from bottom, find prompt line (
❯,›,>) - Walk up past separators and empty lines
- 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 agentstest_active_subtasks_single_bash—›› reading config files · 1 bashtest_active_subtasks_background_tasks—›› fixing tests · 3 background taskstest_active_subtasks_single_local_agent—›› writing code · 1 local agenttest_active_subtasks_bare_mode_line_resets_to_zero—›› bypass permissions ontest_active_subtasks_explicit_zero_count—›› finishing · 0 bashtest_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 linetest_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 modetest_chrome_only_wrapped_statusline_is_chrome—⏵⏵ bypass permissions on+✻ timertest_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
- Create a fresh session:
session action=create - Start agent in restricted mode (CC:
--permission-mode default, Codex:-a untrusted) - Request operations that trigger approval prompts
- Capture raw output — compare separators, colors, footers
- 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
- Raw ANSI capture via MCP:
session action=output format=rawreveals cursor positioning, colors, and OSC sequences invisible in clean output - Cursor-up distance as height probe:
\033[NAreveals bottom zone height - OSC sequence interception:
\033]777;notify;...and\033]0;...carry metadata - Color as semantic signal: RGB colors distinguish interactive vs chrome elements
- Forced state transitions: specific CLI flags surface all UI variants
- Screen clear detection:
\033[2J\033[3J\033[Hdistinguishes 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
| Property | Claude Code | Codex CLI | Gemini CLI | Aider | OpenCode |
|---|---|---|---|---|---|
| Version tested | v2.1.81 | v0.116.0 | v0.34.0 | v0.86.2 | v1.2.20 |
| Date tested | 2026-03-21 | 2026-03-21 | 2026-03-22 | 2026-03-22 | 2026-03-22 |
| Rendering engine | Ink (React) | Ink (React) | Ink-like (Node.js) | Python rich + readline | Bubble Tea (Go) |
| Cursor positioning | Relative (\033[NA]) | Absolute (\033[r;cH) | Relative (\033[1A]) | Sequential (no cursor) | Absolute (\033[r;cH) |
| Scroll mechanism | \r\n padding | Scroll regions (\033[n;mr]) | \r\n padding | Normal scroll | Full-screen redraw |
| Screen clear on menus | Sometimes (\033[2J) | No | No | N/A | Full-screen TUI |
| Parsing strategy | Changed-rows delta | Changed-rows delta | Changed-rows delta | Changed-rows delta | Screen snapshot |
2. Prompt Line
| Property | Claude Code | Codex CLI | Gemini CLI | Aider | OpenCode |
|---|---|---|---|---|---|
| Prompt char | ❯ (U+276F) | › (U+203A, bold) | > (purple, rgb 215,175,255) | > (green, ANSI #40) | None (framed ┃ box) |
| Prompt background | None | Dark gray (rgb 57,57,57) | Dark gray (rgb 65,65,65) | None | Dark (rgb 30,30,30) |
| Prompt box border | ──── separators | Background color only | ▀▀▀ top / ▄▄▄ bottom | None | ┃╹▀ vertical frame |
| Ghost text style | dim cell attribute | \033[2m dim | Gray (rgb 175,175,175) | N/A | Gray placeholder |
| Multiline input | Enter = submit | Enter = newline | Enter = submit | Enter = submit | Unknown |
3. Separator Lines
| Property | Claude Code | Codex CLI | Gemini CLI | Aider | OpenCode |
|---|---|---|---|---|---|
| Uses separators | Yes | Partially | Yes | Yes (green ─────) | No (uses ┃╹▀) |
| Separator chars | ─ (U+2500) | ─ (U+2500) | ─ (U+2500) | ─ (U+2500) | ┃ ╹ ▀ (vertical frame) |
| Separator color | Gray (rgb 136,136,136) | Standard | Dark gray (rgb 88,88,88) | Green (rgb 0,204,0) | N/A |
| Separator purpose | Frame prompt box | Between tool output & summary | Above prompt area | Between conversation turns | Prompt box border |
| Decorated separators | Yes (──── label ──) | No | No | No | N/A |
| Min run length | 4+ chars | Full width | Full width | Full width | N/A |
4. Status / Chrome Lines
| Property | Claude Code | Codex CLI | Gemini CLI | Aider | OpenCode |
|---|---|---|---|---|---|
| Mode line | ⏵⏵ <mode> (last row) | None | None | None | Mode in prompt box (Build/Plan) |
| Status line(s) | 0-N below separator | 1 line below prompt | 2-row status bar (4 columns) | Token report after response | Right panel (context, cost, LSP) |
| Status indent | 2 spaces (\033[2C) | 2 spaces | 1 space | None | N/A (panel layout) |
| Info line | None | None | Shift+Tab to accept edits + MCP/skills count | None | tab agents · ctrl+p commands |
| Subprocess count | In mode line | None | None | None | None (progress bar instead) |
5. Spinner / Working Indicators
| Property | Claude Code | Codex CLI | Gemini CLI | Aider | OpenCode |
|---|---|---|---|---|---|
| Spinner chars | ✶✻✳✢· (U+2720-273F) | • (U+2022) | ⠋⠙⠹⠸⠴⠦⠧⠇ (braille) | ░█ / █░ (Knight Rider) | ■⬝ (progress bar) |
| Spinner color | White | Standard | Blue/green (varies) | Standard | Standard |
| Spinner position | Above separator | Inline with output | Below output, above separator | Inline (backspace overwrite) | Footer row |
| Time display | (1m 32s) | (10s • esc to interrupt) | (esc to cancel, Ns) | None | None |
| Token display | ↓ 2.2k tokens | None | None | Tokens: Nk sent, N received. Cost: $X.XX | None |
| Tip text | Spinner verb names | None | Italic tips during spinner | None | None |
| Detected by | is_chrome_row ✓ | is_chrome_row ✓ | parse_status_line ✓ | parse_status_line ✓ | N/A (full TUI) |
6. Interactive Menus
| Property | Claude Code | Codex CLI | Gemini CLI | Aider | OpenCode |
|---|---|---|---|---|---|
| Permission prompt | Multiselect (❯ 1. Yes) | Not observed (sandbox) | None (model-level refusal) | File add: Y/N/A/S/D | △ Permission required inline |
| Selection char | ❯ (blue) | Not observed | N/A | N/A | ⇆ select |
| Footer pattern | Esc to cancel/close | esc to interrupt | esc to cancel (in spinner) | None | enter confirm |
| OSC 777 notify | Yes | No | No | No | No |
| OSC 0 window title | Yes (task + spinner) | Yes | Yes (◇ Ready (workspace)) | No | No |
| Slash commands | /mcp, /stats, /status | /model, /mcp, /fast | /help, /settings, /model, /stats | /help | None observed |
7. System Messages
| Property | Claude Code | Codex CLI | Gemini CLI | Aider | OpenCode |
|---|---|---|---|---|---|
| Output prefix | ⏺ (white/green/red) | • (U+2022) | ✦ (U+2726, purple) | None (blue text) | None (inline in panel) |
| Tool call display | ⏺ + verb | • + description | ╭───╮ ✓ ToolName ╰───╯ box | None | → read / ← write |
| Warning prefix | N/A | ⚠ (U+26A0) | N/A | Orange text | N/A |
| Error indicator | ⏺ (red) | ✗ | ✦ + error text | Red text | ┃ Error: inline |
| Interrupt marker | N/A | ■ | Not observed | ^C | esc interrupt hint |
| Tool result | ⎿ (U+23BF) | └ or inline | Inside ╭───╮ box | Inline | ▣ completion marker |
Trigger Procedures
How to force each UI state for analysis and testing.
Procedure A: Start agent in each permission mode
| Agent | Restricted mode | Permissive mode |
|---|---|---|
| Claude Code | claude --permission-mode default | claude --permission-mode bypassPermissions |
| Codex CLI | codex -a untrusted | codex (suggest mode, default) |
| Gemini CLI | gemini (default, workspace-restricted) | gemini --sandbox=false (unconfirmed) |
| Aider | N/A (no sandbox) | N/A |
| OpenCode | Unknown | Unknown |
Procedure B: Trigger permission/approval prompt
| Agent | Action | Expected result |
|---|---|---|
| Claude Code (default mode) | “create a file /tmp/test.txt with hello” | Multiselect: Yes/Yes+allow/No |
| Codex CLI (untrusted) | Same | Not observed — auto-approves in sandbox |
| Gemini CLI | “create a file /tmp/test.txt with hello” | Text refusal (workspace restriction) |
| Aider | Open file not in chat | Add file to the chat? (Y)es/(N)o/(A)ll/(S)kip all/(D)on't ask again |
| OpenCode | Access external directory | △ Permission required with Allow once / Allow always / Reject |
Procedure C: Trigger interactive menus
| Agent | Command | Expected result |
|---|---|---|
| Claude Code | /mcp | Server list with ❯ selection |
| Claude Code | /stats | Usage heatmap with date cycling |
| Claude Code | /status | Settings panel with search box |
| Codex CLI | /model | Model selector |
| Codex CLI | /mcp | MCP server list |
| Gemini CLI | /settings | Settings panel (unconfirmed) |
| Gemini CLI | /stats | Usage stats |
Procedure D: Observe working state
| Agent | Action | What to capture |
|---|---|---|
| Any | Send a complex multi-tool task | Spinner animation, cursor-up distance |
| Any | Send task during active subprocess | Subprocess count display |
| Any | Press Escape during work | Interrupt 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
- Fill the detection matrix columns by running procedures A-E
- Create
docs/architecture/agents/<name>.mdwith observed layouts - Update
chrome.rsif new markers/chars are needed - Add test cases from real captured text
- Run
/agent-ui-auditskill 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.json → statusLine, 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
| Format | Example | Source |
|---|---|---|
| Mode only | ⏵⏵ bypass permissions on | test |
| Mode + hint | ⏵⏵ bypass permissions on (shift+tab to cycle) | live |
| Mode + subprocess (old) | ⏵⏵ bypass permissions on · 1 shell | test |
| Mode + subprocess (old, plural) | ⏵⏵ bypass permissions on · 2 local agents | test |
| Subprocess + mode (new) | 1 shell · ⏵⏵ bypass permissions on | live |
| Subprocess only | 1 shell | screenshot |
| Plan mode | ⏸ plan mode on (shift+tab to cycle) | test |
| Accept edits | ⏵⏵ accept edits on (shift+tab to cycle) | test |
| Auto mode | ⏵⏵ auto mode | test |
| Empty | `` | test |
| Absent (default mode) | N/A — no mode line at all | live |
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
| Feature | Normal | Permission prompt |
|---|---|---|
| Top separator | Gray ─ (rgb 136,136,136) | Blue ─ (rgb 177,185,249) |
| Content separator | None | Dotted ╌ (U+254C) |
❯ char | Gray prompt | Blue selection (rgb 177,185,249) |
| Mode line | ⏵⏵/⏸ on last row | None |
| Last line | Mode indicator | Esc 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;41rto define scrollable content area - Reverse index —
\033Mto 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
| Feature | Claude Code | Codex CLI |
|---|---|---|
| Prompt char | ❯ (U+276F) | › (U+203A, bold) |
| Prompt box | Separator-framed (────) | Background color (rgb 57,57,57) |
| Cursor positioning | Relative (\033[8A) | Absolute (\033[12;2H) |
| Scrolling | \r\n padding | Scroll regions (\033[12;41r) + reverse index (\033M) |
| Status line | Multi-line, indented 2sp | Single line, indented 2sp, dim |
| Mode line | ⏵⏵ bypass permissions on etc. | None observed |
| Separator usage | Around prompt box | Between tool output and summary |
| System messages | ⏺ prefix (white/green/red) | • prefix (bullet) |
| Warnings | N/A | ⚠ prefix (yellow) |
| Interrupt marker | N/A | ■ prefix |
| Ghost text | Via dim cell attribute | Via \033[2m dim |
| Submit key | Enter | Enter (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>
| Policy | Behavior |
|---|---|
untrusted | Sandbox commands (does NOT prompt for approval) |
on-failure | DEPRECATED — auto-run, ask only on failure |
on-request | Model decides when to ask |
never | Never 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_lineviaAIDER_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_lineviaAIDER_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 vin startup banner░█/█░Knight Rider spinnerTokens:+Cost:report after responsesAdd file to the chat?approval prompt
Chrome Detection (is_chrome_row)
░█/█░— not in current marker set but detected byparse_status_line- Separator
─────— detected byis_separator_line✓ - Prompt
>— detected byis_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-commitsdisabled, 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_REinparse_status_linedetects the Knight Rider scannerAIDER_TOKENS_REdetects token reportsis_separator_linematches the green─────separatorsis_prompt_linematches the bare>prompt░(U+2591) and█(U+2588) detected byis_chrome_row— Knight Rider spinner classified as chromehas_status_lineinchrome_onlycalculation — spinner-only chunks don’t reset silence timerfind_chrome_cutoffcorrectly 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 shortcutsdisappears, 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 outputsuggest: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 onrgb(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), greenrgb(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 grayrgb(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)whenno 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 indicatorReady— current state(tuicommander)— workspace name- Updates on state changes
Detection Signals
Agent Identification
Gemini CLI vin startup banner- Geometric ASCII logo
▝▜▄ ✦(U+2726) output prefix- Braille spinner
⠋⠙⠹⠸⠴⠦⠧⠇ ? for shortcutshint line- OSC 0 with
◇diamond
Chrome Detection (is_chrome_row)
- Braille spinner chars — detected by
parse_status_lineviaGEMINI_SPINNER_RE - Separator
─────— detected byis_separator_line✓ - Prompt
>— detected byis_prompt_line✓ ▀▀▀/▄▄▄prompt box borders — NOT in chrome marker set- Status bar labels/values — NOT chrome markers
✦(U+2726) — NOT inis_chrome_rowmarker 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 cancelpermission 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_lineviaGEMINI_SPINNER_RE - Separator
─────detected byis_separator_line - Prompt
>detected byis_prompt_line
Not Yet Supported
✦(U+2726) is the Gemini agent output prefix (NOT chrome — do not add tois_chrome_row)- Tool call boxes (
╭╮╰╯│) not classified as chrome ? for shortcutshint line not classified as chrome- Status bar labels (bottom 2 rows) have no chrome markers (but
find_chrome_cutofftrims 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 viais_chrome_row- Braille spinner classified as chrome via
has_status_lineinchrome_onlycalculation find_chrome_cutoffcorrectly trims the full Gemini 8-row bottom zone- Separator
─────detected byis_separator_line - Prompt
>detected byis_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:
Build→Planin 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.20or• OpenCode 1.2.20
Navigation Hints
tab agents— switch to agents panelctrl+p commands— command palettectrl+f fullscreen— toggle fullscreen (in permission dialog)⇆ select— select between optionsenter 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 requiredis a unique text signalAllow once Allow always Rejectfooter 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
BuildtoPlanin prompt box - Interrupt hint:
esc interruptin 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
OPENCODEtext on first screen ┃╹▀vertical frame chars- Mouse tracking enabled on startup
• OpenCode X.Y.Zin 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:
format | Response shape | Description |
|---|---|---|
| (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) |
| Param | Default | Description |
|---|---|---|
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 parserexit— Session process exitedclosed— 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.
| Event | Payload | Description |
|---|---|---|
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,errorsource— 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 pathdeleteBranch(optional, defaulttrue) – whentrue, 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.
| Command | Module | Description |
|---|---|---|
get_claude_usage_api | claude_usage.rs | Fetch rate-limit usage from Anthropic OAuth API |
get_claude_usage_timeline | claude_usage.rs | Get hourly token usage timeline from session transcripts |
get_claude_session_stats | claude_usage.rs | Scan session transcripts for aggregated token/session stats |
get_claude_project_list | claude_usage.rs | List Claude project slugs with session counts |
plugin_read_file | plugin_fs.rs | Read file as UTF-8 (within $HOME, 10 MB limit) |
plugin_read_file_tail | plugin_fs.rs | Read last N bytes of file, skip partial first line |
plugin_list_directory | plugin_fs.rs | List filenames in directory (optional glob filter) |
plugin_watch_path | plugin_fs.rs | Start watching path for changes |
plugin_unwatch | plugin_fs.rs | Stop watching a path |
plugin_http_fetch | plugin_http.rs | Make HTTP request (validated against allowed_urls) |
plugin_read_credential | plugin_credentials.rs | Read credential from system store |
fetch_plugin_registry | registry.rs | Fetch remote plugin registry index |
install_plugin_from_zip | plugins.rs | Install plugin from local ZIP file |
install_plugin_from_url | plugins.rs | Install plugin from HTTPS URL |
uninstall_plugin | plugins.rs | Remove a plugin and all its files |
get_agent_mcp_status | agent_mcp.rs | Check MCP config status for an agent |
install_agent_mcp | agent_mcp.rs | Install TUICommander MCP entry in agent config |
remove_agent_mcp | agent_mcp.rs | Remove 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)
| Command | Args | Returns | Description |
|---|---|---|---|
create_pty | config: PtyConfig | String (session ID) | Create PTY session |
create_pty_with_worktree | pty_config, worktree_config | WorktreeResult | Create worktree + PTY |
write_pty | session_id, data | () | Write to PTY |
resize_pty | session_id, rows, cols | () | Resize PTY |
pause_pty | session_id | () | Pause reader thread |
resume_pty | session_id | () | Resume reader thread |
close_pty | session_id, cleanup_worktree | () | Close PTY session |
can_spawn_session | – | bool | Check session limit |
get_orchestrator_stats | – | OrchestratorStats | Active/max/available |
get_session_metrics | – | JSON | Spawn/fail/byte counts |
list_active_sessions | – | Vec<ActiveSessionInfo> | List all sessions |
list_worktrees | – | Vec<JSON> | List managed worktrees |
update_session_cwd | session_id, cwd | () | Update session working directory (from OSC 7) |
get_session_foreground_process | session_id | JSON | Get foreground process info |
get_kitty_flags | session_id | u32 | Get Kitty keyboard protocol flags for session |
get_last_prompt | session_id | Option<String> | Get last user-typed prompt from input line buffer |
get_shell_state | session_id | Option<String> | Get current shell state (“busy”, “idle”, or null) |
has_foreground_process | session_id: String | bool | Checks if a non-shell foreground process is running |
debug_agent_detection | session_id: String | AgentDiagnostics | Returns diagnostic breakdown of agent detection pipeline |
set_session_name | session_id, name | () | Set custom display name for a session |
Git Operations (git.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
get_repo_info | path | RepoInfo | Repo name, branch, status |
get_git_diff | path | String | Full git diff |
get_diff_stats | path | DiffStats | Addition/deletion counts |
get_changed_files | path | Vec<ChangedFile> | Changed files with stats |
get_file_diff | path, file | String | Single file diff |
get_git_branches | path | Vec<JSON> | All branches (sorted) |
get_recent_commits | path | Vec<JSON> | Recent git commits |
rename_branch | path, old_name, new_name | () | Rename branch |
check_is_main_branch | branch | bool | Is main/master/develop |
get_initials | name | String | 2-char repo initials |
get_merged_branches | repo_path | Vec<String> | Branches merged into default branch |
get_repo_summary | repo_path | RepoSummary | Aggregate snapshot: worktree paths + merged branches + per-path diff stats in one IPC |
get_repo_structure | repo_path | RepoStructure | Fast phase: worktree paths + merged branches only (Phase 1 of progressive loading) |
get_repo_diff_stats | repo_path | RepoDiffStats | Slow phase: per-worktree diff stats + last commit timestamps (Phase 2 of progressive loading) |
run_git_command | path, args | GitCommandResult | Run arbitrary git command (success, stdout, stderr, exit_code) |
get_git_panel_context | path | GitPanelContext | Rich context for Git Panel (branch, ahead/behind, staged/changed/stash counts, last commit, rebase/cherry-pick state). Cached 5s TTL. |
get_working_tree_status | path | WorkingTreeStatus | Full porcelain v2 status: branch, upstream, ahead/behind, stash count, staged/unstaged entries, untracked files |
git_stage_files | path, files | () | Stage files (git add). Path-traversal validated |
git_unstage_files | path, files | () | Unstage files (git restore --staged). Path-traversal validated |
git_discard_files | path, files | () | Discard working tree changes (git restore). Destructive. Path-traversal validated |
git_commit | path, message, amend? | String (commit hash) | Commit staged changes; optional --amend. Returns new HEAD hash |
get_commit_log | path, count?, after? | Vec<CommitLogEntry> | Paginated commit log (default 50, max 500). after is a commit hash for cursor-based pagination |
get_stash_list | path | Vec<StashEntry> | List stash entries (index, ref_name, message, hash) |
git_stash_apply | path, index | () | Apply stash entry by index |
git_stash_pop | path, index | () | Pop stash entry by index |
git_stash_drop | path, index | () | Drop stash entry by index |
git_stash_show | path, index | String | Show diff of stash entry |
git_apply_reverse_patch | path, 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_history | path, file, count?, after? | Vec<CommitLogEntry> | Per-file commit log following renames (default 50, max 500) |
get_file_blame | path, file | Vec<BlameLine> | Per-line blame: hash, author, author_time (unix), line_number, content |
get_branches_detail | path | Vec<BranchDetail> | Rich branch listing: name, ahead/behind, last commit date, tracking upstream, merged status |
delete_branch | path, 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_branch | path, name, start_point, checkout | () | Create a new branch from start_point (defaults to HEAD). checkout=true switches to it immediately |
get_recent_branches | path, limit | Vec<String> | Recently checked-out branches from reflog, ordered by recency |
Commit Graph (git_graph.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
get_commit_graph | path, 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)
| Command | Args | Returns | Description |
|---|---|---|---|
github_start_login | — | DeviceCodeResponse | Start OAuth Device Flow, returns user/device code |
github_poll_login | device_code | PollResult | Poll for token; saves to keyring on success |
github_logout | — | () | Delete OAuth token from keyring, fall back to env/CLI |
github_auth_status | — | AuthStatus | Current auth: login, avatar, source, scopes |
github_disconnect | — | () | Disconnect GitHub (clear all tokens from keyring and env cache) |
github_diagnostics | — | JSON | Diagnostics: token sources, scopes, API connectivity |
GitHub Integration (github.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
get_github_status | path | GitHubStatus | PR + CI for current branch |
get_ci_checks | path | Vec<JSON> | CI check details |
get_repo_pr_statuses | path, include_merged | Vec<BranchPrStatus> | Batch PR status (all branches) |
approve_pr | repo_path, pr_number | String | Submit approving review via GitHub API |
merge_pr_via_github | repo_path, pr_number, merge_method | String | Merge PR via GitHub API |
get_all_pr_statuses | path | Vec<BranchPrStatus> | Batch PR status for all branches (includes merged) |
get_pr_diff | repo_path, pr_number | String | Get PR diff content |
fetch_ci_failure_logs | repo_path, run_id | String | Fetch failure logs from a GitHub Actions run for CI auto-heal |
check_github_circuit | path | CircuitState | Check GitHub API circuit breaker state |
Worktree Management (worktree.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
create_worktree | base_repo, branch_name | JSON | Create git worktree |
remove_worktree | repo_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_branch | repo_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_dirty | repo_path, branch_name | bool | Check if a branch’s worktree has uncommitted changes. Returns false if no worktree exists |
get_worktree_paths | repo_path | HashMap<String,String> | Worktree paths for repo |
get_worktrees_dir | – | String | Worktrees base directory |
generate_worktree_name_cmd | existing_names | String | Generate unique name |
list_local_branches | path | Vec<String> | List local branches |
checkout_remote_branch | repo_path, branch_name | () | Check out a remote-only branch as a new local tracking branch |
detect_orphan_worktrees | repo_path | Vec<String> | Detect worktrees in detached HEAD state (branch deleted) |
remove_orphan_worktree | repo_path, worktree_path | () | Remove an orphan worktree by filesystem path (validated against repo) |
switch_branch | repo_path, branch_name | () | Switch main worktree to a different branch (with dirty-state and process checks) |
merge_and_archive_worktree | repo_path, branch_name | MergeResult | Merge worktree branch into base and archive |
finalize_merged_worktree | repo_path, branch_name | () | Clean up worktree after merge (delete branch + worktree) |
list_base_ref_options | repo_path | Vec<String> | List valid base refs for worktree creation |
run_setup_script | repo_path, worktree_path | () | Run post-creation setup script in new worktree |
generate_clone_branch_name_cmd | base_name, existing_names | String | Generate hybrid branch name for clone worktree |
Configuration (config.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
load_app_config | – | AppConfig | Load app settings |
save_app_config | config | () | Save app settings |
load_notification_config | – | NotificationConfig | Load notifications |
save_notification_config | config | () | Save notifications |
load_ui_prefs | – | UIPrefsConfig | Load UI preferences |
save_ui_prefs | config | () | Save UI preferences |
load_repo_settings | – | RepoSettingsMap | Load per-repo settings |
save_repo_settings | config | () | Save per-repo settings |
check_has_custom_settings | path | bool | Has non-default settings |
load_repo_defaults | – | RepoDefaultsConfig | Load repo defaults |
save_repo_defaults | config | () | Save repo defaults |
load_repositories | – | JSON | Load saved repositories |
save_repositories | config | () | Save repositories |
load_prompt_library | – | PromptLibraryConfig | Load prompts |
save_prompt_library | config | () | Save prompts |
load_notes | – | JSON | Load notes |
save_notes | config | () | Save notes |
save_note_image | note_id, data_base64, extension | String (absolute path) | Decode base64 image, validate ≤10 MB, write to config_dir()/note-images/<note_id>/<timestamp>.<ext> |
delete_note_assets | note_id | () | Remove note-images/<note_id>/ directory recursively (no-op if missing) |
get_note_images_dir | – | String | Return config_dir()/note-images/ absolute path |
load_keybindings | – | JSON | Load keybinding overrides |
save_keybindings | config | () | Save keybinding overrides |
load_agents_config | – | AgentsConfig | Load per-agent run configs |
save_agents_config | config | () | Save per-agent run configs |
load_activity | – | ActivityConfig | Load activity dashboard state |
save_activity | config | () | Save activity dashboard state |
load_repo_local_config | repo_path | RepoLocalConfig? | Read .tuic.json from repo root; returns null if absent or malformed |
Agent Detection (agent.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
detect_agent_binary | binary | AgentBinaryDetection | Check binary in PATH |
detect_all_agent_binaries | – | Vec<AgentBinaryDetection> | Detect all known agents |
detect_claude_binary | – | String | Detect Claude binary |
detect_installed_ides | – | Vec<String> | Detect installed IDEs |
open_in_app | path, app | () | Open path in application |
spawn_agent | pty_config, agent_config | String (session ID) | Spawn agent in PTY |
discover_agent_session | session_id, agent_type, cwd | Option<String> | Discover agent session UUID from filesystem for session-aware resume |
verify_agent_session | agent_type, session_id, cwd | bool | Verify 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.
| Command | Args | Returns | Description |
|---|---|---|---|
load_ai_chat_config | – | AiChatConfig | Load provider / model / base URL / temperature / context_lines from ai-chat-config.json |
save_ai_chat_config | config | () | Persist chat config |
has_ai_chat_api_key | – | bool | Whether an API key is stored in the OS keyring for the current provider |
save_ai_chat_api_key | key: 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_status | – | OllamaStatus | Probe GET /api/tags on the configured base URL (default http://localhost:11434/v1/); returns reachable + model list |
test_ai_chat_connection | – | String | Validate API key + base URL with a minimal completion request |
list_conversations | – | Vec<ConversationMeta> | List persisted conversations (id, title, updated_at, message count) |
load_conversation | id: String | Conversation | Load a saved conversation body |
save_conversation | conversation: Conversation | () | Persist a conversation to ai-chat-conversations/<id>.json |
delete_conversation | id: String | () | Remove a saved conversation (idempotent) |
new_conversation_id | – | String | Mint a fresh conversation UUID |
stream_ai_chat | session_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_chat | chat_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.
| Command | Args | Returns | Description |
|---|---|---|---|
start_agent_loop | session_id, goal | String (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_loop | session_id | String | Cancel the active agent loop. Errors if no loop is active. |
pause_agent_loop | session_id | String | Pause the active agent loop between iterations. |
resume_agent_loop | session_id | String | Resume a paused agent loop. |
agent_loop_status | session_id | { active: bool, state: AgentState?, session_id } | Query whether an agent is active and its current state (running/paused/pending_approval). |
approve_agent_action | session_id, approved | String | Approve or reject the pending destructive command the agent wants to run. Errors if no agent is active. |
get_session_knowledge | session_id | SessionKnowledgeSummary | Lightweight 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_sessions | filter?: { 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_detail | session_id | SessionDetail? | 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):
| Tool | Args | Description |
|---|---|---|
read_screen | session_id, lines? | Read visible terminal text (default 50 lines). Secrets redacted. |
send_input | session_id, command | Send a text command to the PTY (Ctrl-U prefix + \r). |
send_key | session_id, key | Send a special key (enter, tab, ctrl+c, escape, arrows). |
wait_for | session_id, pattern?, timeout_ms?, stability_ms? | Wait for regex match or screen stability. |
get_state | session_id | Structured session metadata (shell_state, cwd, terminal_mode). |
get_context | session_id | Compact ~500-char context summary. |
Filesystem tools (sandboxed per session via FileSandbox):
| Tool | Args | Description |
|---|---|---|
read_file | file_path, offset?, limit? | Paginated file read (default 200, max 2000 lines). Binary/10MB rejected. Secrets redacted. |
write_file | file_path, content | Atomic create/overwrite (tmp+rename). Sensitive paths flagged. |
edit_file | file_path, old_string, new_string, replace_all? | Search-and-replace. Must be unique unless replace_all=true. |
list_files | pattern, path? | Glob match (e.g. src/**/*.rs). Max 500 entries. |
search_files | pattern, path?, glob?, context_lines? | Regex search, .gitignore-aware. Max 50 matches with context. |
run_command | command, 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.
| Command | Args | Returns | Description |
|---|---|---|---|
start_mcp_upstream_oauth | name: String | StartOAuthResponse | Begin 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_callback | code: 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_oauth | name: 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.
| Command | Args | Returns | Description |
|---|---|---|---|
load_mcp_upstreams | – | UpstreamMcpConfig | Load upstream config from mcp-upstreams.json |
save_mcp_upstreams | config: UpstreamMcpConfig | () | Validate, persist, and hot-reload upstream config. Errors if validation fails |
reconnect_mcp_upstream | name: String | () | Disconnect and reconnect a single upstream by name. Useful after credential changes or transient failures |
get_mcp_upstream_status | – | Vec<UpstreamStatus> | Get live status of all upstream MCP servers. Status values: connecting, ready, circuit_open, disabled, failed, authenticating, needs_auth |
save_mcp_upstream_credential | name: String, token: String | () | Store a Bearer token for an upstream in the OS keyring |
delete_mcp_upstream_credential | name: 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:
| Value | Meaning |
|---|---|
connecting | Handshake in progress |
ready | Tools available |
circuit_open | Circuit breaker open, backoff active |
disabled | Disabled in config |
failed | Permanently failed, manual reconnect required |
Agent MCP Configuration (agent_mcp.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
get_agent_mcp_status | agent | AgentMcpStatus | Check MCP config for an agent |
install_agent_mcp | agent | String | Install TUICommander MCP entry |
remove_agent_mcp | agent | String | Remove TUICommander MCP entry |
get_agent_config_path | agent | String | Get agent’s MCP config file path |
Prompt Processing (prompt.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
extract_prompt_variables | content | Vec<String> | Parse {var} placeholders |
process_prompt_content | content, variables | String | Substitute variables |
resolve_context_variables | repo_path: String | HashMap<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)
| Command | Args | Returns | Description |
|---|---|---|---|
execute_headless_prompt | command: 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_script | script_content: String, timeout_ms: u64, repo_path: String | Result<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)
| Command | Args | Returns | Description |
|---|---|---|---|
get_claude_usage_api | – | UsageApiResponse | Fetch rate-limit usage from Anthropic OAuth API |
get_claude_usage_timeline | scope, days? | Vec<TimelinePoint> | Hourly token usage from session transcripts |
get_claude_session_stats | scope | SessionStats | Aggregated token/session stats from JSONL transcripts |
get_claude_project_list | – | Vec<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/)
| Command | Args | Returns | Description |
|---|---|---|---|
start_dictation | – | () | Start recording |
stop_dictation_and_transcribe | – | TranscribeResponse | Stop + transcribe. Returns {text, skip_reason?, duration_s} |
inject_text | text | String | Apply corrections |
get_dictation_status | – | DictationStatus | Model/recording status |
get_model_info | – | Vec<ModelInfo> | Available models |
download_whisper_model | model_name | String | Download model |
delete_whisper_model | model_name | String | Delete model |
get_correction_map | – | HashMap<String,String> | Load corrections |
set_correction_map | map | () | Save corrections |
list_audio_devices | – | Vec<AudioDevice> | List input devices |
get_dictation_config | – | DictationConfig | Load config |
set_dictation_config | config | () | Save config |
check_microphone_permission | – | String | Check macOS microphone TCC permission status |
open_microphone_settings | – | () | Open macOS System Settings > Privacy > Microphone |
Filesystem (fs.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
resolve_terminal_path | path | String | Resolve terminal path |
list_directory | path | Vec<DirEntry> | List directory contents |
fs_read_file | path | String | Read file contents |
write_file | path, content | () | Write file |
create_directory | path | () | Create directory |
delete_path | path | () | Delete file or directory |
rename_path | src, dest | () | Rename/move path |
copy_path | src, dest | () | Copy file or directory |
fs_transfer_paths | destDir, paths, mode ("move"|"copy"), allowRecursive | TransferResult { 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_gitignore | path, pattern | () | Add pattern to .gitignore |
search_files | path, query | Vec<SearchResult> | Search files by name in directory |
search_content | repoPath, 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)
| Command | Args | Returns | Description |
|---|---|---|---|
list_user_plugins | – | Vec<PluginManifest> | List valid plugin manifests |
get_plugin_readme_path | id | Option<String> | Get plugin README.md path |
read_plugin_data | plugin_id, path | Option<String> | Read plugin data file |
write_plugin_data | plugin_id, path, content | () | Write plugin data file |
delete_plugin_data | plugin_id, path | () | Delete plugin data file |
install_plugin_from_zip | path | PluginManifest | Install from local ZIP |
install_plugin_from_url | url | PluginManifest | Install from HTTPS URL |
uninstall_plugin | id | () | Remove plugin and all files |
install_plugin_from_folder | path | PluginManifest | Install from local folder |
register_loaded_plugin | plugin_id | () | Register a plugin as loaded (for lifecycle tracking) |
unregister_loaded_plugin | plugin_id | () | Unregister a plugin (on unload/disable) |
Plugin Filesystem (plugin_fs.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
plugin_read_file | path, plugin_id | String | Read file as UTF-8 (within $HOME, 10 MB limit) |
plugin_read_file_tail | path, max_bytes, plugin_id | String | Read last N bytes of file, skip partial first line |
plugin_list_directory | path, pattern?, plugin_id | Vec<String> | List filenames in directory (optional glob filter) |
plugin_watch_path | path, plugin_id, recursive?, debounce_ms? | String (watch ID) | Start watching path for changes |
plugin_unwatch | watch_id, plugin_id | () | Stop watching a path |
plugin_write_file | path, content, plugin_id | () | Write file within $HOME (path-traversal validated) |
plugin_rename_path | src, dest, plugin_id | () | Rename/move path within $HOME (path-traversal validated) |
Plugin HTTP (plugin_http.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
plugin_http_fetch | url, method?, headers?, body?, allowed_urls, plugin_id | HttpResponse | Make HTTP request (validated against allowed_urls) |
Plugin CLI Execution (plugin_exec.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
plugin_exec_cli | binary, args, cwd?, plugin_id | String | Execute whitelisted CLI binary, return stdout. Allowed: mdkb. 30s timeout, 5 MB limit. |
Plugin Credentials (plugin_credentials.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
plugin_read_credential | service_name, plugin_id | String? | Read credential from system store (Keychain/file) |
Plugin Registry (registry.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
fetch_plugin_registry | – | Vec<RegistryEntry> | Fetch remote plugin registry index |
Watchers
| Command | Args | Returns | Description |
|---|---|---|---|
start_head_watcher | path | () | Watch .git/HEAD for branch changes |
stop_head_watcher | path | () | Stop watching .git/HEAD |
start_repo_watcher | path | () | Watch .git/ for repo changes |
stop_repo_watcher | path | () | Stop watching .git/ |
start_dir_watcher | path | () | Watch directory for file changes (non-recursive) |
stop_dir_watcher | path | () | Stop watching directory |
System (lib.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
load_config | – | AppConfig | Alias for load_app_config |
save_config | config | () | Alias for save_app_config |
hash_password | password | String | Bcrypt hash |
list_markdown_files | path | Vec<MarkdownFileEntry> | List .md files in dir |
read_file | path, file | String | Read file contents |
get_mcp_status | – | JSON | MCP server status (no token — use get_connect_url for QR) |
get_connect_url | ip | String | Build QR connect URL server-side (token stays in backend) |
check_update_channel | channel | UpdateCheckResult | Check beta/nightly channel for updates (hardcoded URLs, SSRF-safe) |
clear_caches | – | () | Clear in-memory caches |
get_local_ip | – | Option<String> | Get primary local IP |
get_local_ips | – | Vec<LocalIpEntry> | List local network interfaces |
regenerate_session_token | – | () | Regenerate MCP session token (invalidates all remote sessions) |
fetch_update_manifest | url | JSON | Fetch update manifest via Rust HTTP (bypasses WebView CSP) |
read_external_file | path | String | Read file outside repo (standalone file open) |
get_relay_status | – | JSON | Cloud relay connection status |
get_tailscale_status | – | TailscaleState | Tailscale daemon status (NotInstalled/NotRunning/Running with fqdn, https_enabled) |
Global Hotkey
| Command | Args | Returns | Description |
|---|---|---|---|
set_global_hotkey | combo: Option<String> | () | Set or clear the OS-level global hotkey |
get_global_hotkey | — | Option<String> | Get the currently configured global hotkey |
App Logger (app_logger.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
push_log | level, source, message | () | Push entry to ring buffer (survives webview reloads) |
get_logs | level?, source?, limit? | Vec<LogEntry> | Query ring buffer with optional filters |
clear_logs | – | () | Flush all log entries |
Notification Sound (notification_sound.rs)
| Command | Args | Returns | Description |
|---|---|---|---|
play_notification_sound | sound_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.tsor 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.
| Param | Type | Description |
|---|---|---|
path | string | File path — relative (resolved against active repo) or absolute |
opts.pinned | boolean | Pin the tab (default: false) |
tuic.edit(path, opts?)
Open a file in the external editor.
| Param | Type | Description |
|---|---|---|
path | string | File path — relative or absolute |
opts.line | number | Line number to jump to (default: 0) |
tuic.getFile(path): Promise<string>
Read a file’s text content from the active repo.
| Param | Type | Description |
|---|---|---|
path | string | File 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)
tuic:// Links
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.
| Param | Type | Description |
|---|---|---|
callback | (repoPath: string | null) => void | Called 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.
| Param | Type | Description |
|---|---|---|
repoPath | string | Repository root path (absolute) |
UI Feedback
tuic.toast(title, opts?)
Show a native toast notification in the host app.
| Param | Type | Description |
|---|---|---|
title | string | Toast title (required) |
opts.message | string | Optional body text |
opts.level | "info" | "warn" | "error" | Severity (default: "info") |
opts.sound | boolean | Play 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).
| Param | Type | Description |
|---|---|---|
text | string | Text to copy |
Messaging
tuic.send(data)
Send structured data to the host. The host receives it via pluginRegistry.handlePanelMessage().
| Param | Type | Description |
|---|---|---|
data | any | JSON-serializable payload |
tuic.onMessage(callback)
Register a listener for messages pushed from the host.
| Param | Type | Description |
|---|---|---|
callback | (data: any) => void | Called 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-primary → bgPrimary).
var theme = tuic.theme;
// { bgPrimary: "#1e1e2e", fgPrimary: "#cdd6f4", accent: "#89b4fa", ... }
tuic.onThemeChange(callback)
Register a listener that fires when the host theme changes.
| Param | Type | Description |
|---|---|---|
callback | (theme: object) => void | Called 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 valueonRepoChangelistener registration- Theme delivery and
onThemeChange onMessagelistener registrationgetFile("README.md")reads file contentgetFile("../../../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
| File | Description |
|---|---|
src/components/PluginPanel/tuicSdk.ts | SDK script injected into iframes |
src/components/PluginPanel/PluginPanel.tsx | Host-side message handlers |
src/components/PluginPanel/resolveTuicPath.ts | Path resolution (relative + traversal guard) |
docs/examples/sdk-test.html | Interactive 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
- Create a directory:
~/.config/tuicommander/plugins/my-plugin/ - 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"] }
- 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() {},
};
- 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
- Discovery — Rust
list_user_pluginsscans~/.config/tuicommander/plugins/formanifest.jsonfiles - Validation — Frontend validates manifest fields and
minAppVersion - Import —
import("plugin://my-plugin/main.js")loads the module via the custom URI protocol - Module check — Default export must have
id,onload,onunload - Register —
pluginRegistry.register(plugin, capabilities)callsplugin.onload(host) - Active — Plugin receives PTY lines, structured events, and can use the PluginHost API
- Hot reload — File changes emit
plugin-changedevents; the plugin is unregistered and re-imported - Unload —
plugin.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, oronunloadlogs 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
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Must match the directory name |
name | string | yes | Human-readable display name |
version | string | yes | Plugin semver (e.g. "1.0.0") |
minAppVersion | string | yes | Minimum TUICommander version required |
main | string | yes | Entry point filename (e.g. "main.js") |
description | string | no | Short description |
author | string | no | Author name |
capabilities | string[] | no | Tier 3/4 capabilities needed (defaults to []) |
allowedUrls | string[] | no | URL patterns allowed for net:http (e.g. ["https://api.example.com/*"]) |
agentTypes | string[] | no | Agent types this plugin targets (e.g. ["claude"]). Omit or [] for universal plugins. |
binaries | string[] | no | CLI binaries this plugin may execute via exec:cli (e.g. ["rtk", "mdkb"]) |
Validation Rules
idmust match the directory name exactlyidmust not be emptymainmust not contain path separators or..- All
capabilitiesmust be known strings (see Capabilities section) minAppVersionmust 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:
onMatchmust be synchronous and fast (< 1ms) — it’s in the PTY hot pathpattern.lastIndexis 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.writePtysends 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.
| Parameter | Type | Default | Description |
|---|---|---|---|
sound | string | "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:
| Tier | Priority | Behavior |
|---|---|---|
| Low | < 10 | Shown only in the popover, not in rotation |
| Normal | 10–99 | Auto-rotates every 5s in the ticker area |
| Urgent | >= 100 | Pinned — 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:
-
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. -
Theme variables — all CSS custom properties from the app’s
:rootare 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 class | Description |
|---|---|
body | Themed background, font, color |
button, .btn | Default button with hover/active states |
button.primary, .btn-primary | Accent-colored button |
button.danger, .btn-danger | Error-colored button |
input, textarea, select | Themed form controls with focus ring |
.card | Bordered container with hover elevation |
table, th, td | Styled table with hover rows |
.badge | Inline label (combine with .badge-p1, .badge-error, .badge-success, .badge-accent, .badge-warning, .badge-muted) |
label, .hint | Form labels and help text |
.filter-bar | Flex row for search/filter UI |
.empty-state | Centered placeholder with .hint |
.toast, .toast.error, .toast.success | Fixed-position notification (add .show to display) |
h1–h4 | Themed headings |
code, a, hr, small | Themed inline elements |
::-webkit-scrollbar | Styled 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):
| Variable | Usage |
|---|---|
--bg-primary | Main canvas |
--bg-secondary | Sidebar-level surfaces |
--bg-tertiary | Inputs, elevated surfaces |
--bg-highlight | Hover states |
--fg-primary | Primary text |
--fg-secondary | Labels, secondary text |
--fg-muted | Tertiary text |
--accent | Links, primary actions |
--accent-hover | Hover on accent |
--success | Positive states |
--warning | Caution states |
--error | Error states |
--border | All borders |
--text-on-accent | Text on colored backgrounds |
--text-on-error | Text on error backgrounds |
--text-on-success | Text 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:
| Method | Description |
|---|---|
tuic.version | SDK 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:
| URL | Action |
|---|---|
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
disabledcallback 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
disabledcallback 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 inallowedUrls - Built-in plugins (no
capabilitiesarray) can fetch anyhttp://orhttps://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
binariesmanifest 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:
| Command | Args | Returns | Capability |
|---|---|---|---|
read_file | { path: string, file: string } | string | invoke:read_file |
list_markdown_files | { path: string } | Array<{ path, git_status }> | invoke:list_markdown_files |
read_plugin_data | { plugin_id: string, path: string } | string | none (always allowed) |
write_plugin_data | { plugin_id: string, path: string, content: string } | void | none (always allowed) |
delete_plugin_data | { plugin_id: string, path: string } | void | none (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"]
}
| Capability | Unlocks | Risk |
|---|---|---|
pty:write | host.writePty(), host.sendAgentInput() | Can send input to terminals |
ui:markdown | host.openMarkdownPanel(), host.openMarkdownFile() | Can open panels and files in the UI |
ui:sound | host.playNotificationSound(sound?) | Can play sounds (question, error, completion, warning, info) |
ui:panel | host.openPanel() | Can render arbitrary HTML in sandboxed iframe |
ui:ticker | host.setTicker(), host.clearTicker() | Can post messages to the shared status bar ticker |
credentials:read | host.readCredential() | Can read system credentials (consent dialog shown) |
net:http | host.httpFetch() | Can make HTTP requests (scoped to allowedUrls) |
invoke:read_file | host.invoke("read_file", ...) | Can read files on disk |
invoke:list_markdown_files | host.invoke("list_markdown_files", ...) | Can list directory contents |
fs:read | host.readFile(), host.readFileTail() | Can read files within $HOME (10 MB limit) |
fs:list | host.listDirectory() | Can list directory contents within $HOME |
fs:watch | host.watchPath() | Can watch filesystem paths within $HOME for changes |
fs:write | host.writeFile() | Can write files within $HOME (10 MB limit) |
fs:rename | host.renamePath() | Can rename/move files within $HOME |
exec:cli | host.execCli() | Can execute CLI binaries declared in manifest binaries field |
git:read | host.getGitBranches(), host.getRecentCommits(), host.getGitDiff() | Read-only access to git repository state |
ui:context-menu | host.registerTerminalAction() | Can add actions to the terminal right-click “Actions” submenu |
ui:sidebar | host.registerSidebarPanel() | Can register collapsible panel sections in the sidebar |
ui:file-icons | host.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 (
agentTypesomitted 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 method | Filtered by agentTypes |
|---|---|
registerOutputWatcher callbacks | Yes |
registerStructuredEventHandler callbacks | Yes |
| 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 name | Agent 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.mdstories: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:
- Emits a
plugin-changedevent with the plugin ID - Calls
pluginRegistry.unregister(id)(runsonunload, disposes all registrations) - Re-imports the module with a cache-busting query (
?t=timestamp) - 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
.ziparchives
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:
- From Settings: Click “Install from file…” in the Plugins tab
- From URL: Use
tuic://install-plugin?url=https://example.com/plugin.zip - From Rust:
invoke("install_plugin_from_zip", { path })orinvoke("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
Deep Link Scheme (tuic://)
TUICommander registers the tuic:// URL scheme for external integration:
| URL | Action |
|---|---|
tuic://install-plugin?url=https://... | Download ZIP, show confirmation, install |
tuic://open-repo?path=/path/to/repo | Switch to repo (must already be in sidebar) |
tuic://settings?tab=plugins | Open 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).
| Plugin | File | Section | Detects |
|---|---|---|---|
plan | planPlugin.ts | ACTIVE PLAN | plan-file structured events (repo-scoped) |
Note: Session prompt tracking is now a native Rust feature (via
input_line_buffer.rsand the Activity Dashboard). The formersessionPromptPluginbuilt-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):
| Class | Element |
|---|---|
activity-section-header | Section heading row |
activity-section-label | Section label text |
activity-dismiss-all | “Dismiss All” button |
activity-item | Individual item row |
activity-item-icon | Item icon container |
activity-item-body | Title + subtitle wrapper |
activity-item-title | Primary text |
activity-item-subtitle | Secondary text |
activity-item-dismiss | Dismiss button |
activity-last-item-btn | Shortcut button in toolbar |
activity-last-item-icon | Shortcut button icon |
activity-last-item-title | Shortcut 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:
| Example | Tier | Capabilities | Demonstrates |
|---|---|---|---|
hello-world | 1 | none | Output watcher, addItem |
auto-confirm | 1+3 | pty:write | Auto-responding to Y/N prompts |
ci-notifier | 1+3 | ui:sound, ui:markdown | Sound notifications, markdown panels |
repo-dashboard | 1+2 | none | Read-only state, dynamic markdown |
claude-status | 1 | none | Agent-scoped (agentTypes: ["claude"]), structured events |
telegram-notifier | 1+3 | net:http, ui:panel, ui:ticker | Telegram push notifications, per-event toggles, settings panel |
Distributable Plugins
Available from the plugin registry (submodule at plugins/). Installable via Settings > Plugins > Browse.
| Plugin | Tier | Capabilities | Description |
|---|---|---|---|
mdkb-dashboard | 2+3 | exec:cli, fs:read, ui:panel, ui:ticker | mdkb knowledge base dashboard |
rtk-dashboard | 3 | exec:cli, ui:panel, ui:context-menu | RTK token savings dashboard (binaries: ["rtk"]) |
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Plugin not loading | manifest.json missing or malformed | Check console for validation errors |
requires app version X.Y.Z | minAppVersion too high | Lower minAppVersion or update app |
not in the invoke whitelist | Calling non-whitelisted Tauri command | Only use commands listed in the whitelist table |
not declared in plugin ... manifest binaries | Binary not in manifest binaries field | Add the binary name to the binaries array in manifest.json |
requires capability "X" | Missing capability in manifest | Add the capability to manifest.json capabilities array |
| Module not found | main field doesn’t match filename | Ensure "main": "main.js" matches your actual file |
| Changes not reflecting | Hot reload cache | Save the file again, or restart the app |
default export error | Module 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:
.dmgand.app - Windows:
.nsis(.exesetup installer) - Linux:
.deband.AppImage
Note: The
.msibundle may fail on Windows due to WiX tooling issues. Use--bundles nsisto produce a working.exeinstaller: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
| File | Purpose |
|---|---|
src/App.tsx | Central orchestrator (829 lines) |
src-tauri/src/lib.rs | Rust app setup, command registration |
src-tauri/src/pty.rs | PTY session management |
src/hooks/useAppInit.ts | App initialization |
src/stores/terminals.ts | Terminal state |
src/stores/repositories.ts | Repository state |
SPEC.md | Feature specification |
IDEAS.md | Feature 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_PATHthere 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
| Symptom | Cause | Fix |
|---|---|---|
whisper-rs-sys build fails with “couldn’t find libclang” | LLVM not installed or LIBCLANG_PATH not set | Install LLVM 18 and set LIBCLANG_PATH |
whisper-rs-sys compile error: attempt to compute 1_usize - 296_usize | LLVM 19+ generates broken bindings for this crate | Use LLVM 18 specifically |
WiX .msi bundle fails | WiX light.exe tooling issue | Use --bundles nsis instead |
| App window opens but shows a black screen | Navigation 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 found | Custom local build isn’t listed in official release manifest | Harmless — 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
| Tool | What it profiles | Install |
|---|---|---|
| samply | Rust CPU time (flamegraphs) | cargo install samply |
| tokio-console | Async task scheduling, lock contention | cargo install tokio-console |
| hyperfine | Command-line benchmarking | brew install hyperfine |
| Chrome DevTools | JS rendering, memory, layout | Built into Tauri webview |
| Solid DevTools | SolidJS signal/memo reactivity graph | Browser 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::Commandin hot paths = subprocess forksserde_json::to_value/serde_json::to_string= serialization overheadparking_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,branchesrecent_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
- Open DevTools in TUICommander:
Cmd+Shift+I - Go to Performance tab
- Click Record
- Exercise the scenario for 10-30 seconds
- Stop recording
What to look for:
- Long Tasks (>50ms red bars) = jank
- Layout/Recalculate Style = CSS forcing reflow
requestAnimationFramegaps = dropped frames- Frequent minor GC = allocation pressure
Memory Profiling
Run scripts/perf/snapshot-memory.sh for detailed scenario instructions. Key scenarios:
- Terminal memory — open/close 5 terminals, compare heap snapshots
- Panel leak check — open/close Settings/Activity/Git panels 10x
- 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
createMemore-evaluates - Find effects with unexpectedly high execution counts
- Trace which signal changes trigger cascading updates
Key areas to watch:
terminalsStoreupdates propagating to StatusBar/TabBar/SmartButtonStripdebouncedBusysignal reactivity scopegithubStorepolling 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
- Open 5 terminal tabs
- Run an AI agent in 2 of them
- Record CPU + memory for 2 minutes
- Check: is CPU usage stable? Is memory growing?
Scenario 3: Git-Heavy Workflow
- Open a large repo (>1000 commits, >50 branches)
- Open the Git panel
- Switch branches
- Run
bench-ipc.shagainst 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:
| Layer | Key files | What to measure |
|---|---|---|
| Tauri commands | src-tauri/src/git.rs | spawn_blocking overhead, subprocess latency |
| PTY pipeline | src-tauri/src/pty.rs | Read buffer throughput, event emission rate |
| IPC serialization | src-tauri/src/pty.rs, git.rs | JSON payload sizes, serde time |
| State management | src/stores/terminals.ts | Signal propagation scope, batch effectiveness |
| Rendering | src/components/Terminal/Terminal.tsx | xterm.js write batching, WebGL atlas rebuilds |
| Polling | src/hooks/useAgentPolling.ts, src/stores/github.ts | Interval frequency, IPC calls per tick |
| Bundle | vite.config.ts | Chunk sizes, initial parse/eval time |