Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

Tabby — Administrator & Developer Guide

Generated May 2026 from Eugeny/tabby at d8fdecd

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

Tabby is a cross-platform terminal emulator built on Electron 38, Angular 15, and xterm.js v6. It replaces monolithic terminal applications with a modular, plugin-based architecture where every major feature — local terminals, SSH, serial, telnet, settings UI, link detection, color schemes — is a separate Angular module loaded at runtime through dependency injection. This guide covers the full architecture, every configuration surface, deployment packaging, security model, performance tuning, and the internal developer APIs needed to extend or maintain the project.

For usage tutorials, keyboard shortcuts, and day-to-day workflow guidance, see the companion User Guide.


1. Architecture Overview

1.1 High-Level System Diagram

┌─────────────────────────────────────────────────────────────────────┐
│                        Electron 38 Shell                            │
│  ┌──────────────────────┐    ┌────────────────────────────────────┐ │
│  │    Main Process       │    │        Renderer Process            │ │
│  │                       │    │                                    │ │
│  │  app/lib/index.ts     │    │   Angular 15 Application           │ │
│  │  ├── Application      │◄──►│   ├── AppService (tabs)            │ │
│  │  │   ├── Window[]     │IPC │   ├── ConfigService (YAML)         │ │
│  │  │   ├── PTYManager   │    │   ├── ProfilesService              │ │
│  │  │   ├── PluginManager│    │   ├── HotkeysService               │ │
│  │  │   ├── DockingServ. │    │   ├── VaultService                 │ │
│  │  │   ├── Updater      │    │   ├── ThemesService                │ │
│  │  │   └── Tray         │    │   ├── CommandService               │ │
│  │  │                    │    │   └── TabRecoveryService            │ │
│  │  ├── config.ts (I/O)  │    │                                    │ │
│  │  ├── pty.ts (node-pty)│    │   Plugin Modules (Angular DI)      │ │
│  │  ├── cli.ts (yargs)   │    │   ├── tabby-terminal (xterm.js)    │ │
│  │  ├── sentry.ts        │    │   ├── tabby-local (shells)         │ │
│  │  └── window.ts        │    │   ├── tabby-ssh (russh)            │ │
│  │                       │    │   ├── tabby-serial (serialport)    │ │
│  └──────────────────────┘    │   ├── tabby-telnet                  │ │
│                               │   ├── tabby-settings               │ │
│                               │   ├── tabby-plugin-manager         │ │
│                               │   ├── tabby-linkifier              │ │
│                               │   ├── tabby-community-color-schemes│ │
│                               │   └── tabby-auto-sudo-password     │ │
│                               └────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
         │                              │
         ▼                              ▼
    ┌──────────┐                ┌──────────────┐
    │ node-pty  │                │  Native Addons │
    │ (PTY mgr) │                │  russh (SSH)   │
    │           │                │  serialport    │
    │           │                │  keytar        │
    └──────────┘                └──────────────┘

1.2 Process Model

Tabby follows Electron's dual-process architecture:

Main process (app/lib/): Manages the application lifecycle, BrowserWindow creation, PTY spawning, native plugin management, tray icon, global hotkeys, docking, auto-updates, and Sentry error reporting. Config file I/O (YAML read/write with atomic saves) happens here. The main process exposes services to the renderer via Electron IPC.

Renderer process (app/src/ + all tabby-* plugins): Runs the Angular 15 application. All UI rendering, tab management, terminal emulation (xterm.js v6), SSH session management, and plugin UI happens here. The renderer communicates with the main process through a well-defined IPC boundary for operations requiring Node.js APIs (file access, PTY control, native module calls).

Note

Tabby enforces single-instance mode. When a second instance is launched, it forwards its CLI arguments to the running instance via Electron's app.requestSingleInstanceLock() mechanism and exits. The running instance receives the arguments and processes them (opening new tabs, running commands, etc.).

1.3 Plugin System Architecture

The plugin system is the central architectural decision in Tabby. Every feature is a plugin — there is no monolithic core that handles terminals, SSH, or settings directly. Even the "core" (tabby-core) is structured as a plugin, albeit one that provides the foundational services other plugins depend on.

Plugin Loading Sequence

  1. Discovery: On startup, pluginManager.ts scans the built-in plugin directories and the user plugin directory ({userData}/plugins/)
  2. Module Registration: Each plugin exports an Angular NgModule that declares its components and registers providers
  3. Dependency Injection: Angular's multi-provider system allows plugins to contribute to shared extension points without knowing about each other
  4. Service Initialization: Services decorated with @Injectable() are instantiated lazily by Angular's DI container when first injected

Built-in Plugins

Plugin Package Responsibility
tabby-core Core framework Config management, hotkeys, tab lifecycle, profiles, command palette, vault, locale, themes, selectors
tabby-electron Electron bindings Platform-specific services, shell providers, docking, touchbar, auto-updater, winston logging, file I/O
tabby-local Local terminals Shell detection across platforms, PTY session management, terminal tab components
tabby-terminal Terminal emulation xterm.js v6 frontend, color scheme management, search, middleware pipeline, terminal decorators
tabby-ssh SSH connections SSH sessions via russh, SFTP, port forwarding (local/remote/dynamic), key management, multiplexing
tabby-serial Serial ports Serial connections via the serialport library, baud rate management
tabby-telnet Telnet/socket Telnet protocol and raw TCP socket connections
tabby-settings Settings UI Settings tab panels, profile editor, hotkey editor, vault configuration, config sync
tabby-plugin-manager Plugin manager npm-based third-party plugin installation, removal, and updates using a bundled npm binary
tabby-linkifier Link detection URL, IP address, and file path detection in terminal output with click handlers
tabby-community-color-schemes Color schemes 150+ community-contributed terminal color schemes
tabby-auto-sudo-password Auto sudo Detects sudo password prompts and auto-pastes a saved password
tabby-web Web platform Platform service bindings for browser-based deployment
tabby-web-demo Demo mode Browser-based demo using v86 x86 emulator
tabby-uac UAC helper Windows UAC elevation via a C++ native helper

Extension Point System (Angular Multi-Providers)

Plugins register functionality through Angular's multi-provider tokens. Each token defines a contract; any plugin can provide implementations:

Extension Point Interface Purpose
ConfigProvider Contributes default config blocks Merged via deepmerge at startup
HotkeyProvider Registers keyboard shortcuts Collected by HotkeysService
ProfileProvider Defines connection profile types SSH, local, serial, telnet profiles
QuickConnectProfileProvider Parses quick-connect strings user@host:port to SSH profile
TabRecoveryProvider Persists/restores tab state Called every 30s + on change
TabContextMenuItemProvider Adds right-click menu items Per-tab context menus
ToolbarButtonProvider Adds toolbar buttons Main toolbar customization
CommandProvider Registers command palette entries Fuzzy-searchable commands
SettingsTabProvider Adds settings panels Plugin-specific settings UI
TerminalColorSchemeProvider Registers color schemes Built-in + community + custom
TerminalDecorator Decorates terminal instances Overlays, modifications
CLIHandler Handles CLI subcommands Extend tabby CLI
ShellProvider Detects available shells Platform-specific shell discovery
FileProvider File access abstraction Platform-independent file operations
SFTPContextMenuItemProvider SFTP right-click actions File manager context menus
SSHProfileImporter Imports SSH profiles OpenSSH config, static files
AutoPrivateKeyLocator Discovers SSH keys Auto-find keys on disk
SessionMiddleware Terminal I/O pipeline Transform input/output streams

1.4 Data Flow

Terminal Session Lifecycle

User action (click profile / CLI)
    │
    ▼
ProfilesService.openProfile()
    │
    ▼
TabsService.open(TerminalTabComponent)
    │
    ▼
SessionFactory creates session:
    ├── LocalSession   → PTYManager.spawn() → node-pty
    ├── SSHSession     → russh native → remote PTY
    ├── SerialSession  → serialport   → hardware
    └── TelnetSession  → net.Socket   → remote host
    │
    ▼
SessionMiddlewareStack attaches:
    InputProcessor → LoginScriptProcessor → OSCProcessor
    → TerminalStreamProcessor → UTF8SplitterMiddleware
    │
    ▼
xterm.js Terminal instance renders output
    │
    ▼
User input → middleware pipeline (reverse) → session.write()

Config Data Flow

config.yaml (disk)
    │
    ▼  atomically.readFile() [main process]
    │
ConfigService.readRawConfig()
    │
    ▼  js-yaml.load()
    │
Raw config object
    │
    ▼  deepmerge(allDefaults, rawConfig)
    │
ConfigProxy (reactive, Observable)
    │
    ├──► changed$ Observable → subscribers react
    │
    ▼  On save: diff against defaults, strip default values
    │
Minimal diff written to config.yaml via atomically.writeFile()

1.5 IPC Boundary

The main process and renderer communicate through Electron IPC channels. Key channels include:

Channel Direction Purpose
pty:spawn Renderer to Main Create new PTY process
pty:write Renderer to Main Send input to PTY
pty:resize Renderer to Main Resize PTY dimensions
pty:kill Renderer to Main Terminate PTY process
pty:data Main to Renderer PTY output data
pty:exit Main to Renderer PTY process exited
config:read Renderer to Main Read config from disk
config:write Renderer to Main Write config to disk
app:toggle-window Main to Renderer Docking toggle
app:update-available Main to Renderer Auto-update notification

1.6 Dependency Graph

The plugin dependency graph flows strictly downward. No circular dependencies exist between plugin packages:

tabby-core (foundation — no plugin dependencies)
    │
    ├── tabby-electron (platform bindings)
    │       │
    │       ├── tabby-local (depends on core + electron)
    │       │
    │       └── tabby-plugin-manager (depends on core + electron)
    │
    ├── tabby-terminal (depends on core)
    │       │
    │       ├── tabby-ssh (depends on core + terminal)
    │       │
    │       ├── tabby-serial (depends on core + terminal)
    │       │
    │       ├── tabby-telnet (depends on core + terminal)
    │       │
    │       ├── tabby-linkifier (depends on core + terminal)
    │       │
    │       ├── tabby-community-color-schemes (depends on core + terminal)
    │       │
    │       └── tabby-auto-sudo-password (depends on core + terminal)
    │
    ├── tabby-settings (depends on core)
    │
    ├── tabby-web (depends on core — alternative to tabby-electron)
    │
    └── tabby-web-demo (depends on core + web)

2. System Requirements

2.1 Supported Platforms

Platform Versions Architecture Package Formats
Windows 10, 11 x64, ARM64 MSIX, NSIS installer, portable zip
macOS 12+ (Monterey) x64, ARM64 (Apple Silicon) DMG, zip
Linux Ubuntu 22.04+, Fedora 38+, Arch x64, ARM64 AppImage, deb, rpm, pacman, snap

2.2 Runtime Dependencies

Component Version Purpose
Electron 38 Desktop shell, Chromium renderer
Node.js 22+ (bundled with Electron) Main process runtime
OpenSSL 3.x (bundled) TLS for SSH, HTTPS

No external runtime dependencies are required. Electron bundles Node.js and Chromium. Native modules (node-pty, russh, serialport, keytar) are compiled and bundled during the build process.

2.3 Build Dependencies

Tool Version Purpose
Node.js 22.x Build toolchain
Yarn 1.x (Classic) Package manager, workspace management
Python 3.x Native module compilation (node-gyp)
C/C++ compiler Platform-specific Native addons (node-pty, russh, serialport, keytar)
Rust toolchain Latest stable russh SSH library compilation

Platform-specific build requirements:

Windows:

  • Visual Studio Build Tools 2019 or 2022 with the "Desktop development with C++" workload
  • Windows 10 SDK or later
  • Rust toolchain (rustup + stable)
  • Python 3.x for node-gyp

macOS:

  • Xcode Command Line Tools (xcode-select --install)
  • Rust toolchain (rustup + stable)
  • Python 3.x for node-gyp

Linux:

  • build-essential (gcc, g++, make)
  • libx11-dev, libxkbfile-dev (X11 keyboard support)
  • libsecret-1-dev (keytar — OS credential store)
  • rpm and dpkg (for building distribution packages)
  • Rust toolchain (rustup + stable)
  • Python 3.x for node-gyp

2.4 Hardware Recommendations

Resource Minimum Recommended Notes
RAM 256 MB 512 MB+ Per window. Each tab adds ~10-25 MB for scrollback
GPU OpenGL ES 2.0 Dedicated GPU WebGL terminal renderer; falls back to canvas
Disk 200 MB 500 MB+ Base install; plugins add ~5-20 MB each
CPU Dual-core Quad-core+ Multiple PTY sessions are CPU-bound on the main process
Display 1024x768 1920x1080+ Tabby renders at native resolution

2.5 Network Requirements

Tabby itself has no mandatory network requirements for local terminal use. Network access is needed for:

Feature Destination Protocol Port
SSH connections User-configured hosts TCP 22 (default)
Telnet connections User-configured hosts TCP 23 (default)
Plugin installation registry.npmjs.org HTTPS 443
Auto-updates github.com (releases) HTTPS 443
Error reporting (Sentry) Sentry ingest endpoint HTTPS 443
Crowdin translations crowdin.com HTTPS 443

3. Configuration Reference

3.1 Config File Location

Tabby stores its configuration as a single YAML file. The location depends on platform and environment:

Platform Default Path
Windows %APPDATA%\tabby\config.yaml
macOS ~/Library/Application Support/tabby/config.yaml
Linux ~/.config/tabby/config.yaml
Override $TABBY_CONFIG_DIRECTORY/config.yaml
Portable <exe_dir>/data/config.yaml

A backup is maintained at config.yaml.backup in the same directory. The backup is written before every config save operation.

Important

Tabby stores only the diff from defaults in config.yaml. Values that match the default are stripped on save. This means a fresh config.yaml may be nearly empty — even a single line with just version: 7. To see the full effective configuration (defaults merged with overrides), inspect ConfigService.store at runtime via the developer console (Ctrl+Shift+I then Console tab). This diff-only storage is a deliberate design choice: it ensures that when a new version of Tabby ships with updated defaults, users automatically get the new defaults for any setting they have not explicitly overridden.

3.2 Config File Format

The config file uses YAML syntax parsed by js-yaml. Writes are atomic via the atomically library (write to temp file, then rename). On every save, the current config is backed up first.

# Example config.yaml showing commonly customized values
version: 7

appearance:
  theme: Standard
  colorScheme: Dracula
  font: "MesloLGS NF"
  fontSize: 14
  fontWeight: 400
  fontWeightBold: 700
  ligatures: true
  opacity: 1
  vibrancy: false
  frame: full          # full | thin | none
  dock: off             # off | top | bottom | left | right
  dockFill: 0.5
  dockSpace: 1
  dockScreen: current
  dockAlwaysOnTop: true
  tabsOnTop: false
  tabsLocation: top     # top | bottom | left | right
  cycleTabs: true
  flexTabs: true

terminal:
  frontend: xterm-webgl     # xterm-webgl | xterm-canvas
  scrollbackLines: 25000
  cursorBlink: true
  cursorStyle: block         # block | underline | bar
  bell: "off"               # off | visual | audible
  bracketedPaste: true
  rightClick: menu           # menu | paste | clipboard
  pasteOnMiddleClick: true
  copyOnSelect: false
  wordSeparator: " ()[]{}'\"\\/"
  drawBoldTextInBrightColors: true
  sixel: true
  autoOpen: false
  warnOnMultilinePaste: false
  altIsMeta: false           # macOS: use Option as Meta
  scrollOnInput: true
  unicode: true
  detectProgress: true
  hideTabIndex: false

ssh:
  warnOnClose: true
  winSCPPath: null
  agentAccounts: []
  knownHosts: []

serial:
  warnOnClose: true

hotkeys: {}                 # See §3.6

profiles: []                # See §3.4

groups: []                  # Profile groups

vault: null                 # Encrypted vault blob

recoverTabs: true
warnOnClose: true
enableAnalytics: true
enableAutomaticUpdates: true

providerBlacklist: []       # Disabled extension points
commandBlacklist: []        # Hidden commands

3.3 Config Proxy System

ConfigService exposes configuration through a ConfigProxy — a JavaScript Proxy object that intercepts property access and merges defaults with stored values in real-time.

Key behaviors:

  1. Default merging: Accessing config.store.terminal.scrollbackLines returns the stored value if set, otherwise the default from all ConfigProvider contributions
  2. Diff-only storage: When a value is set back to its default, it is deleted from the store (only deviations are persisted)
  3. Change notification: Every mutation triggers ConfigService.changed$ (an RxJS Observable), allowing all subscribers to react
  4. Ready signal: ConfigService.ready$ is an AsyncSubject that completes once config is fully loaded and all providers have contributed defaults
  5. Platform-specific defaults: ConfigProvider implementations can return different defaults per platform (Windows, macOS, Linux, Web)
  6. Array-replace merge strategy: When merging defaults with stored config, arrays are replaced entirely (not appended). This prevents duplicate entries when defaults change between versions.
// How config providers contribute defaults (simplified)
@Injectable()
export class MyPluginConfigProvider extends ConfigProvider {
    defaults = {
        myPlugin: {
            enabled: true,
            maxRetries: 3,
        },
    }

    platformDefaults = {
        [Platform.Windows]: {
            myPlugin: {
                shellPath: 'C:\\Windows\\System32\\cmd.exe',
            },
        },
        [Platform.macOS]: {
            myPlugin: {
                shellPath: '/bin/zsh',
            },
        },
    }
}

3.4 Profile System

Profiles define connection parameters for terminals. Each profile type (local, SSH, serial, telnet) has its own schema with type-specific defaults.

Profile Structure

profiles:
  - id: "uuid-here"
    type: local            # local | ssh | serial | telnet
    name: "My Profile"
    group: "group-uuid"    # Optional group membership
    icon: "fas fa-terminal" # FontAwesome icon
    color: "#ff0000"        # Tab color
    weight: 0               # Sort order
    isBuiltin: false
    options:
      # Type-specific options (see below)

Profile Groups

Profiles can be organized into groups for the UI:

groups:
  - id: "group-uuid"
    name: "Production Servers"
    icon: "fas fa-server"
    color: "#e74c3c"

SSH Profile Options (Complete Reference)

options:
  host: "server.example.com"
  port: 22
  user: "root"
  auth: null                    # null = auto-detect
  privateKeys: []               # Paths to private key files
  keepaliveInterval: 5000       # ms between keepalive packets
  keepaliveCountMax: 10         # max missed keepalives before disconnect
  readyTimeout: 30000           # ms to wait for connection
  x11: false                    # X11 forwarding
  skipBanner: false             # Skip SSH banner display
  jumpHost: null                # UUID of another SSH profile (ProxyJump)
  agentForward: false           # Forward local SSH agent
  reuseSession: true            # SSH multiplexing
  socksProxyHost: null           # SOCKS proxy hostname
  socksProxyPort: null           # SOCKS proxy port
  httpProxyHost: null            # HTTP proxy hostname
  httpProxyPort: null            # HTTP proxy port
  proxyCommand: null             # ProxyCommand equivalent
  forwardedPorts: []            # See §3.5
  scripts: []                   # Login scripts (expect/send)
  algorithms:
    kex: [...]                  # Key exchange (see §7.3)
    serverHostKey: [...]
    cipher: [...]
    hmac: [...]

Local Profile Options (Complete Reference)

options:
  command: ""                   # Shell executable (empty = default shell)
  args: []                      # Command-line arguments
  cwd: ""                       # Working directory (empty = home)
  env: {}                       # Environment variable overrides

Serial Profile Options (Complete Reference)

options:
  port: "COM3"                  # Windows: COM1-COM256; Linux: /dev/ttyUSB0, /dev/ttyS0
  baudRate: 115200
  dataBits: 8                   # 5, 6, 7, 8
  stopBits: 1                   # 1, 1.5, 2
  parity: "none"                # none, even, odd, mark, space
  rtscts: false                 # RTS/CTS hardware flow control
  xon: false                    # XON/XOFF software flow control
  xany: false                   # XANY flow control
  slowSend: false               # Character-by-character send with delay
  inputMode: "readline"         # readline | local-echo
  outputMode: "CRLF"           # CR | LF | CRLF
  scripts: []                   # Login scripts

Supported baud rates: 110, 150, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 1500000

Telnet Profile Options (Complete Reference)

options:
  host: "server.example.com"
  port: 23                      # 23 for Telnet, 1234 for raw socket
  inputMode: "readline"         # readline | local-echo
  outputNewlines: "CRLF"       # CR | LF | CRLF
  scripts: []                   # Login scripts

Built-in Profiles

Tabby automatically generates built-in profiles from detected shells (via ShellProvider implementations) and imported SSH configurations (via SSHProfileImporter). These have isBuiltin: true and do not appear in the config file. They can be overridden by creating a user profile with the same type and equivalent options.

Recent Profiles

The most recently used profiles are tracked and shown in the quick-access UI:

# Controlled by internal settings
# Default: last 3 profiles

The count of recent profiles is configurable (default 3).

3.5 Port Forwarding Configuration

SSH profiles support three types of port forwarding:

forwardedPorts:
  # Local forward: listen on local:8080, forward to remote-host:80
  - type: local
    host: "localhost"
    port: 8080
    targetAddress: "remote-host"
    targetPort: 80

  # Remote forward: listen on remote:9090, forward to local:3000
  - type: remote
    host: "0.0.0.0"
    port: 9090
    targetAddress: "localhost"
    targetPort: 3000

  # Dynamic (SOCKS proxy): listen on local:1080
  - type: dynamic
    host: "localhost"
    port: 1080

Port forwards are established automatically when the SSH connection opens. They persist for the lifetime of the SSH session.

3.6 Hotkey Configuration

Hotkeys are registered by HotkeyProvider implementations across all plugins. The user can override any hotkey in the config:

hotkeys:
  new-tab:
    - "Ctrl+Shift+T"
  close-tab:
    - "Ctrl+Shift+W"
  toggle-window:
    - "Ctrl+Space"
  copy:
    - "Ctrl+Shift+C"
  paste:
    - "Ctrl+Shift+V"
  split-right:
    - "Ctrl+Shift+E"
  split-bottom:
    - "Ctrl+Shift+D"
  focus-next-pane:
    - "Ctrl+Tab"
  command-palette:
    - "Ctrl+Shift+P"
  search:
    - "Ctrl+Shift+F"
  zoom-in:
    - "Ctrl+="
  zoom-out:
    - "Ctrl+-"
  reset-zoom:
    - "Ctrl+0"

Hotkeys use Electron's accelerator syntax. Multiple bindings per action are supported (array values). Setting an empty array disables the hotkey entirely.

Global vs. local hotkeys: The toggle-window hotkey is a global hotkey (works even when Tabby is not focused). All other hotkeys are local (only active when Tabby has focus).

3.7 Vault Configuration

The vault stores encrypted secrets (SSH passwords, key passphrases, auto-sudo passwords). It uses a master passphrase to derive an encryption key.

vault:
  # This is the encrypted blob — do not edit manually
  enabled: true
  # Encrypted content stored as base64

Vault operations:

  • Lock: Clears the decrypted vault from memory; requires passphrase to unlock
  • Unlock: Decrypts the vault blob with the master passphrase
  • Not configured: vault: null — secrets stored in plaintext in profile options
  • Change passphrase: Decrypts with old passphrase, re-encrypts with new one

Tip

The vault encrypts all values stored via PasswordStorageService. When the vault is locked, SSH connections requiring stored passwords will prompt for the vault passphrase first. If you use Tabby in an environment where you frequently lock/unlock your workstation, consider setting a shorter vault auto-lock timeout.

3.8 Environment Variables

Variable Purpose Default
TABBY_CONFIG_DIRECTORY Override config file directory Platform-specific (see §3.1)
TABBY_PLUGINS Additional plugin search paths (colon/semicolon-separated) None
TABBY_DEV Enable development mode (disables Angular prod mode, enables debugging) Not set
TABBY_FORCE_ANGULAR_PROD Force Angular production mode even in dev builds Not set
PORTABLE_EXECUTABLE_FILE Detected automatically for portable builds; triggers portable mode Not set
NODE_PATH Additional Node.js module resolution paths Standard

3.9 Portable Mode

Tabby supports a portable installation mode where all data (config, plugins, logs) is stored next to the executable rather than in user-specific directories.

Detection: The main process (portable.ts) checks for a data/ directory adjacent to the executable. If found, userData and appData paths are redirected to data/.

Setup:

  1. Download the portable ZIP distribution
  2. Extract to any directory (e.g., a USB drive)
  3. Create a data/ subdirectory next to the executable
  4. Launch Tabby — all state will be stored in data/

This is particularly useful for USB-drive deployments, environments where per-user directories are restricted, or when you want a completely self-contained installation that leaves no traces on the host system.

3.10 Config Sync

Tabby supports configuration synchronization through the settings UI (tabby-settings). The sync mechanism allows exporting and importing the config file, enabling manual sync across machines via cloud storage, version control, or direct file copy.

3.11 Provider and Command Blacklists

Two blacklist mechanisms allow administrators to disable features:

# Disable specific extension points (prevents plugins from registering)
providerBlacklist:
  - "tabby-serial:SerialProfileProvider"
  - "tabby-telnet:TelnetProfileProvider"

# Hide specific commands from the command palette
commandBlacklist:
  - "tabby-plugin-manager:install"

These are powerful tools for managed deployments where certain features should be unavailable to end users.

3.12 Legacy Terminus Migration

Tabby was originally named Terminus. On first launch, config.ts in the main process checks for a Terminus config directory and automatically migrates:

  • Config file (config.yaml)
  • Plugin directory
  • Log files

The migration is one-time and non-destructive (copies, does not move).


4. Deployment & Scaling

4.1 Build System

Tabby uses a monorepo with Yarn workspaces. Each plugin is a separate npm package with its own webpack.config.mjs and package.json.

Repository Structure

tabby/
├── app/                      # Electron main process + renderer entry
│   ├── lib/                  # Main process source (TypeScript)
│   │   ├── index.ts          # Entry point
│   │   ├── app.ts            # Application class
│   │   ├── window.ts         # Window management
│   │   ├── cli.ts            # CLI parser (yargs)
│   │   ├── config.ts         # Config I/O (YAML, atomic writes)
│   │   ├── pty.ts            # PTY manager (node-pty, flow control)
│   │   ├── pluginManager.ts  # Plugin install/uninstall (npm)
│   │   ├── portable.ts       # Portable mode detection
│   │   ├── sentry.ts         # Error reporting setup
│   │   ├── lru.ts            # Path resolution cache
│   │   └── utfSplitter.ts    # UTF-8 stream splitting
│   ├── src/                  # Renderer entry (Angular bootstrap)
│   └── patches/              # patch-package patches for app deps
├── tabby-core/               # Core framework plugin
│   └── src/
│       ├── api/              # Public API types (extension points)
│       └── services/         # Core services (config, app, profiles, etc.)
├── tabby-electron/           # Electron platform plugin
│   └── src/
│       ├── services/         # Electron-specific services
│       └── shells/           # Shell provider implementations (13 providers)
├── tabby-local/              # Local terminal plugin
│   └── src/
│       ├── services/         # Terminal and session services
│       └── cli.ts            # Local CLI handler
├── tabby-terminal/           # Terminal emulation plugin
│   └── src/
│       ├── api/              # Terminal API types
│       ├── middleware/        # I/O middleware pipeline (5 processors)
│       └── services/         # Terminal services
├── tabby-ssh/                # SSH plugin
│   └── src/
│       ├── api/              # SSH API types
│       └── services/         # SSH, SFTP, multiplexer, known hosts
├── tabby-serial/             # Serial plugin
│   └── src/services/         # Serial port services
├── tabby-telnet/             # Telnet plugin
│   └── src/                  # Telnet session and provider
├── tabby-settings/           # Settings UI plugin
│   └── src/services/         # Settings services
├── tabby-plugin-manager/     # Plugin manager plugin
│   └── src/services/         # Plugin manager service
├── tabby-linkifier/          # Link detection plugin
│   └── src/                  # URL/IP/path detectors
├── tabby-community-color-schemes/
│   ├── schemes/              # 150+ color scheme YAML files
│   └── src/                  # Color scheme provider
├── tabby-auto-sudo-password/ # Auto-sudo plugin
│   └── src/                  # Prompt detection + auto-paste
├── tabby-web/                # Web platform plugin
│   └── src/services/         # Web-specific platform services
├── tabby-web-demo/           # Demo plugin
│   ├── data/                 # v86 emulator data
│   └── src/                  # Demo setup
├── tabby-uac/                # Windows UAC helper
│   └── UAC/                  # C++ native module source
├── locale/                   # i18n translation files (24+ .po files)
├── patches/                  # Root-level dependency patches
├── scripts/                  # Build scripts (per-platform)
├── extras/                   # Extras
│   ├── clink/                # Clink integration scripts
│   └── automator-workflows/  # macOS Automator workflows
├── snap/                     # Snapcraft packaging
├── electron-builder.yml      # Electron Builder packaging config
├── package.json              # Root workspace definition
├── tsconfig.json             # TypeScript configuration
└── .eslintrc.yml             # ESLint configuration

Build Commands

# Install dependencies (must use yarn, not npm — workspace resolution)
yarn install

# Build all plugins + main app
yarn run build

# Watch mode for development (incremental rebuilds)
yarn run watch

# Start in development mode
yarn run start

# Lint all code
yarn run lint

# Generate TypeDoc API documentation
yarn run docs

# i18n operations
yarn run i18n:extract    # Extract translatable strings
yarn run i18n:compile    # Compile .po files to runtime format

Webpack Configuration

Each plugin has its own webpack.config.mjs that:

  1. Compiles TypeScript via ts-loader
  2. Processes Angular templates via @angular/compiler-cli
  3. Processes SCSS/CSS styles
  4. Externalizes shared dependencies (@angular/*, tabby-core, rxjs, electron, etc.) to avoid duplication across plugin bundles
  5. Outputs a single bundle file to the plugin's dist/ directory

The main app has two webpack configs:

  • app/webpack.main.config.mjs — Electron main process (Node.js target)
  • app/webpack.config.mjs — Renderer process (web target with Node.js integration)

4.2 Packaging

Tabby uses electron-builder for creating distributable packages. Platform-specific build scripts handle native module compilation, code signing, and packaging.

Windows Packaging

# Build NSIS installer + portable + MSIX
node scripts/build-windows.mjs

# Outputs (in dist/):
# tabby-setup-{version}.exe     (NSIS installer)
# tabby-{version}-portable.zip  (Portable — no install needed)
# tabby-{version}.msix          (Microsoft Store / sideload)

Code signing: Windows builds can be code-signed with an EV certificate for SmartScreen trust. Configure signing in electron-builder.yml under win.signingHashAlgorithms and win.certificateSubjectName.

macOS Packaging

# Build DMG + zip for both architectures
node scripts/build-macos.mjs

# Outputs (in dist/):
# tabby-{version}-macos-x64.dmg
# tabby-{version}-macos-arm64.dmg
# tabby-{version}-macos-x64.zip
# tabby-{version}-macos-arm64.zip

Notarization: macOS builds require notarization for Gatekeeper compatibility. Configure Apple ID credentials in electron-builder.yml under mac.notarize.

Linux Packaging

# Build all Linux package formats
node scripts/build-linux.mjs

# Outputs (in dist/):
# tabby-{version}-linux-x64.AppImage
# tabby-{version}-linux-x64.deb
# tabby-{version}-linux-x64.rpm
# tabby-{version}-linux-x64.pacman
# tabby-{version}-linux-x64.snap
# tabby-{version}-linux-arm64.AppImage  (if ARM build host)
# tabby-{version}-linux-arm64.deb

Native Module Rebuilding

Native modules (node-pty, russh, serialport, keytar) must be compiled against Electron's Node.js ABI. The build system uses @electron/rebuild:

# Rebuild all native modules for current Electron version
npx @electron/rebuild

Note

The Rust-based russh module requires a working Rust toolchain during build. Ensure rustc and cargo are in your PATH. The first build of russh can take 3-5 minutes as it compiles the entire Rust SSH implementation from source. Subsequent builds use Cargo's incremental compilation cache.

patch-package

The project uses patch-package to apply fixes to dependencies that have not been released upstream. Patches are stored in:

  • patches/ — Root-level dependency patches
  • app/patches/ — App-specific dependency patches
  • tabby-terminal/patches/ — Terminal plugin dependency patches

These patches are applied automatically during yarn install via the postinstall script.

4.3 Plugin Distribution

Third-party plugins are distributed as npm packages. The built-in tabby-plugin-manager provides install/uninstall/update functionality.

Plugin installation paths:

  • User plugins: {userData}/plugins/node_modules/
  • The plugin manager bundles its own npm binary to avoid requiring a system-wide npm installation

Plugin naming convention:

  • npm packages must be named tabby-* or terminus-* (legacy name)
  • The plugin manager searches the npm registry for packages with the tabby-plugin keyword

Plugin development workflow:

# 1. Create plugin directory
mkdir tabby-my-plugin && cd tabby-my-plugin

# 2. Initialize package.json
npm init
# Set "keywords": ["tabby-plugin"]
# Set "peerDependencies": { "tabby-core": "*" }

# 3. Create webpack.config.mjs (copy from existing plugin)
# 4. Create src/index.ts with NgModule export

# 5. Build
yarn build

# 6. Link for local development
ln -s $(pwd) ~/.config/tabby/plugins/node_modules/tabby-my-plugin

# 7. Restart Tabby — plugin is loaded automatically

# 8. Publish to npm
npm publish

4.4 Snap Packaging

The snap/ directory contains the Snapcraft configuration:

# Key snap configuration points
name: tabby
grade: stable
confinement: classic    # Required — terminal emulators need unrestricted access

Classic confinement is mandatory because Tabby needs unrestricted access to the filesystem (PTY device files), network stack, and process spawning. Strict confinement would prevent core terminal functionality.

4.5 Auto-Updates

Tabby uses electron-updater for automatic updates:

  1. On startup (if enableAutomaticUpdates: true), the updater checks GitHub Releases for newer versions
  2. If an update is available, a notification is shown to the user via ngx-toastr
  3. The update is downloaded in the background
  4. On next restart, the update is applied

Update channel: By default, only stable releases are offered. Pre-release/beta channels can be configured in electron-builder.yml.

Disabling auto-updates:

enableAutomaticUpdates: false

This is recommended for managed deployments where IT controls software versions.

4.6 Mass Deployment Considerations

For deploying Tabby across an organization:

1. Configuration management:

  • Use TABBY_CONFIG_DIRECTORY environment variable to point to a shared or managed config location
  • Deploy a pre-configured config.yaml to each user's config directory
  • Use Group Policy (Windows) or configuration management tools to manage the config file

2. Plugin control:

  • Pre-install required plugins in the user plugins directory
  • Use providerBlacklist to disable unwanted features (serial, telnet, plugin manager, etc.)
  • Block npm registry access if third-party plugins are not desired

3. Update control:

  • Set enableAutomaticUpdates: false in the managed config
  • Manage updates through your software distribution system (SCCM, Intune, Munki, etc.)

4. Portable mode:

  • Deploy the portable version with a pre-configured data/ directory for zero-install environments
  • Useful for jump boxes, shared workstations, or locked-down desktops

5. Telemetry control:

  • Set enableAnalytics: false to disable Sentry error reporting
  • No other telemetry is collected

5. Operations

5.1 Logging

Main Process Logging (Winston)

The Electron main process uses Winston with two transports:

Transport Destination Format
File {userData}/log.txt Timestamped JSON lines
Console stdout/stderr Colorized text

Log file location by platform:

Platform Path
Windows %APPDATA%\tabby\log.txt
macOS ~/Library/Application Support/tabby/log.txt
Linux ~/.config/tabby/log.txt
Portable <exe_dir>/data/log.txt

The log file is useful for diagnosing startup failures, PTY spawn errors, and native module loading issues — problems that occur before the renderer (and its developer console) is available.

Renderer Process Logging

The renderer process logs to the Chromium developer console. Access via:

  • Ctrl+Shift+I (Windows/Linux) or Cmd+Option+I (macOS)
  • Navigate to the Console tab

Renderer logs include Angular lifecycle events, config changes, plugin loading, session state, and xterm.js events.

Sentry Error Reporting

Tabby integrates Sentry for automated crash reporting (app/lib/sentry.ts). This captures:

  • Unhandled exceptions in the main process
  • Unhandled promise rejections
  • Renderer process crashes

Error reporting is controlled by the enableAnalytics config option:

# Disable all telemetry and error reporting
enableAnalytics: false

Tip

When diagnosing startup failures, check the main process log file directly. If the renderer fails to load, the main process log will contain the error (the developer console is not available when the renderer crashes during bootstrap). On Linux with snap, also check journalctl --user -u snap.tabby.tabby for additional output.

5.2 Tab Recovery

Tabby automatically saves tab state for crash recovery:

  • Auto-save interval: Every 30 seconds
  • Debounced save: 1 second after any tab change (open, close, reorder, split)
  • History depth: Last 5 closed tabs are recoverable via "Reopen Closed Tab"
  • Storage: In the config file under the recovery key

Recovery data includes:

  • Tab type (local, SSH, serial, telnet, split)
  • Profile reference (UUID) and full options
  • Working directory (for local terminals)
  • Connection parameters (for SSH/serial/telnet)
  • Split layout geometry (nested horizontal/vertical splits)
  • Tab order and active tab indicator

On startup, if recoverTabs: true (default), Tabby restores the previous session's tabs. The recovery process:

  1. Reads serialized tab state from config
  2. Matches each serialized tab to a TabRecoveryProvider
  3. The provider reconstructs the tab component and reconnects if appropriate
  4. Failed recoveries are silently skipped (the tab is not restored)

5.3 CLI Reference

Tabby provides a CLI interface parsed by yargs (app/lib/cli.ts). Plugins can extend the CLI via CLIHandler providers.

# Open Tabby (or focus existing instance)
tabby

# Open a new tab with a specific profile
tabby open --profile "My SSH Server"

# Open a specific directory in a local terminal
tabby open --directory /path/to/dir

# Run a command in a new tab
tabby run -- htop

# Connect via SSH using quick-connect syntax
tabby connect ssh://user@host:port

# List available profiles
tabby profiles

# Open settings
tabby settings

# Show version
tabby --version

# Show help
tabby --help

Tip

The CLI respects single-instance mode. If Tabby is already running, CLI commands are forwarded to the running instance via IPC — a new window is not created. This enables scripting workflows: shell scripts or other tools can open SSH connections in Tabby with tabby connect ssh://user@host.

5.4 Window Management

Docking (Quake-Style Dropdown)

Tabby supports docking the window to a screen edge, creating a dropdown terminal similar to Quake console or Guake:

appearance:
  dock: top           # off | top | bottom | left | right
  dockFill: 0.5       # Width/height ratio (0-1), how much of the edge to fill
  dockSpace: 1        # Screen utilization ratio (0-1), margin from edge
  dockAlwaysOnTop: true
  dockScreen: current # current | <screen-name>

The docking service (tabby-electron/src/services/docking.service.ts) handles:

  • Window repositioning and resizing based on dock position and fill/space ratios
  • Screen edge snapping with pixel-perfect alignment
  • Auto-reposition when displays change (connect/disconnect monitors, resolution change)
  • Global hotkey toggle (toggle-window hotkey, default Ctrl+Space)
  • Show/hide animation

Multi-monitor behavior: When dockScreen is current, the window docks to the screen where the mouse cursor is located at the time of the toggle. Setting it to a specific screen name pins the docked window to that display.

Frameless Window Mode

appearance:
  frame: none         # full | thin | none

When frame is none, Tabby renders its own title bar with window controls:

  • macOS: Traffic light buttons (close/minimize/maximize) are repositioned within the custom title bar area using trafficLightPosition
  • Windows: Window Controls Overlay (WCO) is used when supported, providing native-feeling minimize/maximize/close buttons in the custom title bar
  • Linux: Custom drawn buttons matching the desktop environment's theme when possible

Vibrancy/Transparency

appearance:
  vibrancy: true
  opacity: 0.9        # 0-1

Platform-specific behavior:

  • Windows: Glasstron provides Aero-style blur effects (acrylic/mica on Windows 11)
  • macOS: Native NSVisualEffectView vibrancy
  • Linux: Compositor-dependent — works with KDE/Kwin, may not work with all window managers

5.5 Tray Icon

The system tray icon is available on Windows and macOS (not Linux, due to inconsistent tray support across Linux desktop environments). The tray provides:

  • Left-click: Toggle window visibility
  • Right-click: Context menu with:
    • Profile quick-launch submenu
    • Show/hide toggle
    • Quit action

When the window is closed (not quit), it minimizes to the tray instead of exiting. This allows the global hotkey to work even when no window is visible.

5.6 GPU Configuration

Tabby's terminal renderer can use WebGL for hardware-accelerated rendering:

terminal:
  frontend: xterm-webgl     # WebGL (GPU-accelerated) — default
  # frontend: xterm-canvas  # Canvas 2D (CPU, fallback)

The Application class (app/lib/app.ts) manages GPU settings at the Electron level. GPU acceleration can be disabled entirely via Electron command-line switches for systems with problematic drivers or when running in virtual machines:

# Launch with GPU disabled
tabby --disable-gpu

If WebGL rendering causes visual artifacts, black terminal areas, or crashes, switch to xterm-canvas in the settings.

Note

The WebGL renderer maintains a glyph texture atlas in GPU memory. With many open tabs using different fonts or font sizes, this atlas can grow. Switching to xterm-canvas eliminates GPU memory usage entirely at the cost of rendering performance.

5.7 Multi-Window Support

Tabby supports multiple windows, each managed as a separate Window instance by the Application class. All windows:

  • Share the same configuration (ConfigService is singleton)
  • Share the same plugin set
  • Have independent tab state and layouts
  • Can be individually docked, resized, and positioned
  • Are independently recoverable (tab recovery saves per-window state)

5.8 Internationalization

Tabby supports 24+ languages via @ngx-translate/core with ICU MessageFormat compilation:

Language Code Status
English en-US Complete (source language)
Chinese (Simplified) zh-CN Community-maintained
Chinese (Traditional) zh-TW Community-maintained
Japanese ja-JP Community-maintained
Korean ko-KR Community-maintained
German de-DE Community-maintained
French fr-FR Community-maintained
Russian ru-RU Community-maintained
Spanish es-ES Community-maintained
Portuguese (Brazil) pt-BR Community-maintained
Turkish tr-TR Community-maintained
Arabic ar-SA Community-maintained
Italian it-IT Community-maintained
Polish pl-PL Community-maintained
Ukrainian uk-UA Community-maintained
Czech cs-CZ Community-maintained
Dutch nl-NL Community-maintained
Croatian hr-HR Community-maintained
Indonesian id-ID Community-maintained
And others... ... ...

Translation files are .po format in the locale/ directory. The Crowdin integration manages translation workflows: string extraction, translator assignment, review, and download of completed translations.

Adding a new locale:

# Extract translatable strings from source
yarn run i18n:extract

# Add new .po file to locale/ directory
# Upload to Crowdin or translate manually

# Compile translations to runtime format
yarn run i18n:compile

6. Performance

6.1 PTY Flow Control

The PTY manager (app/lib/pty.ts) implements back-pressure-based flow control to prevent the renderer from being overwhelmed by fast-producing processes (e.g., cat /dev/urandom, large find outputs, or compile logs):

PTY output → node-pty → Buffer → Flow control gate → IPC → Renderer → xterm.js

Flow control parameters:

Parameter Value Purpose
maxChunk 100 KB Maximum bytes sent per IPC message
maxDelta 500 KB Maximum buffered bytes before pausing PTY reads

Flow control algorithm:

On PTY data received:
  1. Append data to internal buffer
  2. Update bufferSize counter
  3. If bufferSize > maxDelta (500KB):
       Pause PTY reading (back-pressure signal)
  4. Extract up to maxChunk (100KB) from buffer
  5. Send chunk to renderer via IPC
  6. Wait for renderer acknowledgment

On renderer acknowledgment:
  1. Decrement bufferSize by acknowledged bytes
  2. If bufferSize < maxDelta AND PTY is paused:
       Resume PTY reading

This prevents memory exhaustion when a process outputs data faster than the terminal can render it. Without flow control, a fast cat /dev/urandom would fill the IPC queue and consume unbounded memory.

6.2 Terminal Rendering Performance

WebGL Renderer (xterm-webgl)

The default frontend uses GPU-accelerated rendering via WebGL 2.0:

  • Glyph atlas: Characters are rendered to a texture atlas once, then reused as textured quads for subsequent frames. This dramatically reduces the number of draw calls — instead of drawing each character individually, the renderer batches all cells into a small number of GPU draw calls
  • Batched rendering: Cells with the same foreground/background colors are drawn in a single GPU pass
  • Sixel/image support: Images embedded in terminal output (via Sixel protocol) are rendered as GPU textures, offloading image scaling to the GPU
  • Selection rendering: Text selection is rendered as a separate overlay pass

Performance characteristics:

  • 60 FPS rendering of terminal output on most hardware
  • Handles 100,000+ lines of scrollback without rendering lag
  • GPU memory usage proportional to font atlas size + scrollback

Canvas Fallback (xterm-canvas)

The Canvas 2D API fallback:

  • CPU-rendered: All text is drawn using Canvas 2D fillText calls
  • No GPU memory: Suitable for VMs, remote desktop sessions, or systems without GPU
  • Universal compatibility: Works on any system with a Canvas implementation
  • Performance: Noticeably slower than WebGL for rapid output, but adequate for interactive use
terminal:
  frontend: xterm-webgl       # GPU-accelerated (default, fastest)
  scrollbackLines: 25000      # Reduce for lower memory; increase for more history
  ligatures: false             # Disable font ligatures for faster text measurement
  sixel: false                 # Disable image protocol if unused
  unicode: true                # Unicode 11 width calculation (slight overhead)
  drawBoldTextInBrightColors: true  # No perf impact, aesthetic choice

Note

Each terminal tab maintains its own xterm.js instance with its own scrollback buffer. With scrollbackLines: 25000 and a 200-column terminal, each tab's scrollback buffer can consume ~10-25 MB of memory. With 20 tabs open, that is 200-500 MB for scrollback alone. If you routinely run many concurrent sessions, consider reducing scrollbackLines to 5000-10000.

6.3 Path Resolution Cache

The main process uses an LRU cache (app/lib/lru.ts) for fs.realpathSync results. This is a targeted optimization for:

  • Plugin loading: Plugin directories are resolved repeatedly during startup
  • Shell detection: Shell executable paths are resolved for each ShellProvider
  • Native module loading: require calls resolve paths through the filesystem

On Windows, realpathSync is particularly slow on NTFS junctions and symlinks. The LRU cache avoids redundant filesystem calls, reducing startup time by 100-300ms on Windows systems with antivirus software that hooks filesystem operations.

6.4 SSH Multiplexing

SSH multiplexing (SSHMultiplexerService) reuses existing SSH connections for new sessions to the same host:

# Per-profile setting
options:
  reuseSession: true    # Default: true

How it works:

  1. When opening an SSH connection, SSHMultiplexerService checks if an active transport exists for the same host:port:user combination
  2. If found, a new SSH channel is opened on the existing transport (no TCP handshake, no key exchange, no authentication)
  3. If not found, a new SSH connection is established and registered in the multiplexer pool
  4. When the last channel on a transport closes, the transport is kept alive for a grace period before disconnection

Performance impact:

  • Second connection to the same host: ~50ms (channel open) vs. ~2-5 seconds (full connection)
  • Reduced network overhead: one TCP connection handles multiple sessions
  • Reduced server-side resource usage: one SSH session with multiple channels vs. multiple sessions

6.5 UTF-8 Stream Splitting

The UTF8SplitterMiddleware and utfSplitter.ts handle splitting byte streams at valid UTF-8 code point boundaries. Without this:

  • A 3-byte UTF-8 character (e.g., CJK characters) could be split across two IPC messages
  • xterm.js would receive an invalid byte sequence and render a replacement character
  • The next message would start with trailing bytes that do not form a valid sequence

The splitter buffers incomplete sequences (1-3 trailing bytes) and emits them once the full code point arrives in the next data event. This is especially important for SSH sessions over high-latency connections where TCP segmentation does not respect character boundaries.

6.6 Memory Management Strategies

Consumer Typical Usage Mitigation
Scrollback buffers 10-25 MB per tab Reduce scrollbackLines
WebGL glyph atlas 4-32 MB (GPU) Switch to xterm-canvas
Sixel images Variable per image Disable sixel: false
SSH transports 1-5 MB per connection Multiplexing reduces count
Plugin modules 2-10 MB per plugin Uninstall unused plugins
Angular component tree 5-20 MB baseline Close unused split panes
Electron/Chromium overhead 80-150 MB baseline Irreducible

Monitoring: Use Electron's built-in process manager:

  • Windows/Linux: Window menu → look for process information, or use Shift+Esc in some builds
  • Developer Tools → Performance tab → Memory snapshots
  • chrome://tracing in Developer Tools for detailed GPU/rendering analysis

6.7 Startup Performance

Tabby's startup involves:

  1. Electron initialization: ~500ms (platform-dependent)
  2. Config file read: ~10ms (YAML parse)
  3. Plugin discovery: ~50-200ms (filesystem scan)
  4. Angular bootstrap: ~200-500ms (module compilation, DI setup)
  5. Shell detection: ~100-500ms (parallel filesystem probes)
  6. Tab recovery: ~50-200ms per recovered tab
  7. xterm.js initialization: ~30-50ms per terminal tab

Total cold start: typically 1-3 seconds. Hot start (from tray): <100ms (window show only).

Optimization techniques applied:

  • LRU cache for path resolution (§6.3)
  • Lazy instantiation of services via Angular DI
  • Parallel shell detection across all providers
  • WebGL glyph atlas reuse across tabs

7. Security

7.1 Threat Model

Tabby is a terminal emulator that executes arbitrary commands with the user's privileges. The security surface includes:

  1. Local process execution: Any command the user can run, Tabby can run. Tabby does not sandbox spawned processes.
  2. SSH key management: Private keys are read from disk for authentication. Key paths are stored in config.
  3. Credential storage: Passwords may be stored in the vault (encrypted), OS keychain (keytar), or plaintext in config.
  4. Network connections: SSH, telnet, and raw sockets connect to arbitrary remote hosts.
  5. Plugin system: Third-party plugins run with full Node.js + Electron privileges. No sandboxing.
  6. Auto-update: Update packages are downloaded from GitHub and executed. Signature verification depends on platform.
  7. Config file: Contains connection details, key paths, and potentially plaintext secrets.
  8. Clipboard access: Terminal content can be copied; paste operations inject text into the active session.
  9. IPC boundary: Main/renderer communication could be intercepted by a compromised renderer.

7.2 Credential Storage

Tabby provides three tiers of credential storage, in order of security:

Method Security Level Storage Location Encryption
Vault High config.yaml (encrypted blob) AES with passphrase-derived key
Keytar (OS keychain) High OS credential store OS-managed
Plaintext in config None config.yaml None

Vault architecture (VaultService in tabby-core):

  • All passwords are encrypted with a master passphrase
  • The encrypted blob is stored as a base64 string in config.yaml
  • When locked, the decryption key is cleared from memory
  • Operations requiring stored passwords prompt for the vault passphrase
  • Changing the master passphrase re-encrypts all stored secrets

Keytar integration (PasswordStorageService in tabby-ssh):

  • Uses the keytar native module for OS-level credential storage
  • Windows: Windows Credential Manager (encrypted by DPAPI)
  • macOS: macOS Keychain (encrypted, access-controlled)
  • Linux: libsecret (GNOME Keyring or KDE Wallet)

Warning

If the vault is not configured (vault: null), SSH passwords and key passphrases stored in profiles are kept in plaintext in config.yaml. Any process with read access to the config file can extract these credentials. Always configure the vault for environments where credential security is important. Run chmod 600 config.yaml (or equivalent restrictive ACLs on Windows) as a baseline defense.

7.3 SSH Cryptographic Algorithms

Tabby's SSH implementation (via the russh Rust library, version 0.1.37) supports modern cryptographic algorithms including post-quantum options:

Key Exchange (KEX) — Default Order

Algorithm Security Level Notes
mlkem768x25519-sha256 Post-quantum ML-KEM 768 (Kyber) hybridized with X25519. Provides quantum resistance while maintaining classical security.
curve25519-sha256 High Modern elliptic curve Diffie-Hellman. Preferred classical algorithm.
curve25519-sha256@libssh.org High Legacy identifier for the same algorithm (interop with older implementations)
diffie-hellman-group16-sha512 High 4096-bit modular DH with SHA-512
diffie-hellman-group14-sha256 Medium 2048-bit modular DH with SHA-256. Included for compatibility.
ext-info-c / ext-info-s N/A RFC 8308 extension negotiation (client/server)
kex-strict-c-v00@openssh.com N/A Terrapin attack mitigation (client strict mode)
kex-strict-s-v00@openssh.com N/A Terrapin attack mitigation (server strict mode)

Host Key Algorithms — Default Order

Algorithm Key Size Notes
ssh-ed25519 256-bit Preferred. Small, fast, no known weaknesses.
ecdsa-sha2-nistp256 256-bit NIST P-256 curve
ecdsa-sha2-nistp521 521-bit NIST P-521 curve
rsa-sha2-256 2048+ bit RSA with SHA-256 digest
rsa-sha2-512 2048+ bit RSA with SHA-512 digest
ssh-rsa 2048+ bit Legacy RSA with SHA-1 digest. Least preferred due to SHA-1 deprecation.

Ciphers — Default Order

Algorithm Block/Key Size Notes
chacha20-poly1305@openssh.com 256-bit key Preferred. AEAD cipher, constant-time, no AES-NI dependency.
aes256-gcm@openssh.com 256-bit key AEAD cipher. Fast on hardware with AES-NI.
aes256-ctr 256-bit key AES-256 in counter mode (requires separate HMAC).
aes192-ctr 192-bit key AES-192 in counter mode.
aes128-ctr 128-bit key AES-128 in counter mode. Smallest key size offered.

HMAC — Default Order

Algorithm Digest Size Notes
hmac-sha2-512-etm@openssh.com 512-bit Encrypt-then-MAC (preferred pattern) with SHA-512
hmac-sha2-256-etm@openssh.com 256-bit Encrypt-then-MAC with SHA-256
hmac-sha2-512 512-bit MAC-then-encrypt with SHA-512
hmac-sha2-256 256-bit MAC-then-encrypt with SHA-256
hmac-sha1-etm@openssh.com 160-bit Legacy ETM variant. SHA-1 is weak but no practical attack on HMAC-SHA1.
hmac-sha1 160-bit Legacy. Least preferred.

Note on compression: SSH compression is none by default. Tabby does not enable zlib compression by default because modern connections are typically fast enough that the CPU overhead of compression exceeds the bandwidth savings.

Algorithm Customization

Per-profile algorithm overrides allow restricting or reordering the negotiated algorithms:

profiles:
  - type: ssh
    options:
      algorithms:
        kex:
          - curve25519-sha256
          - diffie-hellman-group16-sha512
          - kex-strict-c-v00@openssh.com
        cipher:
          - aes256-gcm@openssh.com
          - chacha20-poly1305@openssh.com
        hmac:
          - hmac-sha2-512-etm@openssh.com
        serverHostKey:
          - ssh-ed25519

Caution

Removing kex-strict-c-v00@openssh.com and kex-strict-s-v00@openssh.com from the KEX list disables the Terrapin attack mitigation (CVE-2023-48795). These entries should always be included unless you are connecting to legacy servers that do not support strict KEX and you understand the risk. The Terrapin attack allows a man-in-the-middle to inject packets at the beginning of the SSH connection.

7.4 SSH Known Hosts

SSHKnownHostsService manages host key verification using the Trust On First Use (TOFU) model:

  1. First connection: The user is prompted to accept the host key. If accepted, it is stored in ssh.knownHosts in the config.
  2. Subsequent connections: The stored key is compared against the presented key. A match allows connection; a mismatch triggers a prominent warning.
  3. OpenSSH interop: ~/.ssh/known_hosts is also checked, providing compatibility with OpenSSH's known hosts database.
ssh:
  knownHosts:
    - host: "server.example.com"
      port: 22
      type: "ssh-ed25519"
      key: "AAAAC3NzaC1lZDI1NTE5AAAA..."

7.5 SSH Agent Forwarding

Agent forwarding (agentForward: true) allows the remote server to use the local SSH agent for authentication to third-party servers:

options:
  agentForward: true
  agent: auto         # auto | <named-pipe-path> | <socket-path>

The auto agent setting detects the local SSH agent:

  • Windows: Checks for Pageant named pipe, then OpenSSH agent pipe (\\.\pipe\openssh-ssh-agent)
  • macOS/Linux: Uses SSH_AUTH_SOCK environment variable

Warning

SSH agent forwarding exposes your local SSH keys to the remote server's administrator. A malicious server administrator could use your forwarded agent to authenticate to other servers as you. Only enable agent forwarding to trusted servers. Consider using ProxyJump (jumpHost) instead, which does not expose your agent.

7.6 SSH Jump Hosts (ProxyJump)

Jump host support allows chaining SSH connections through an intermediate bastion host:

profiles:
  - name: "Bastion"
    type: ssh
    id: "bastion-uuid"
    options:
      host: "bastion.example.com"
      user: "jumpuser"

  - name: "Internal Server"
    type: ssh
    options:
      host: "10.0.0.5"
      user: "admin"
      jumpHost: "bastion-uuid"    # Reference to bastion profile by UUID

The connection flow:

  1. Tabby connects to the bastion host
  2. Through the bastion, it opens a direct-tcpip channel to the internal server
  3. The SSH handshake for the internal server happens through this tunneled connection
  4. The internal server never sees the client's real IP — only the bastion's IP

Multiple jump hosts can be chained (A → B → C) by having the intermediate profile also reference a jump host.

7.7 X11 Forwarding

options:
  x11: true

When enabled, Tabby sets up X11 forwarding channels through the SSH connection. An X11Socket class manages the local X11 display connection. This allows running graphical applications on the remote server with display on the local machine.

Requirements:

  • An X11 server running locally (X.Org, XQuartz on macOS, or Xming/VcXsrv on Windows)
  • DISPLAY environment variable set appropriately

7.8 Proxy Support

SSH connections support SOCKS and HTTP proxies:

options:
  # SOCKS proxy (e.g., via SSH dynamic forwarding or Tor)
  socksProxyHost: "127.0.0.1"
  socksProxyPort: 1080

  # HTTP CONNECT proxy (e.g., corporate proxy)
  httpProxyHost: "proxy.corp.example.com"
  httpProxyPort: 8080

  # ProxyCommand (arbitrary command for connection)
  proxyCommand: "nc -X 5 -x proxy:1080 %h %p"

7.9 Plugin Security

Third-party plugins run with the same privileges as Tabby itself: full Node.js APIs (filesystem, network, child process), full Electron APIs (IPC, clipboard, shell), and access to all Angular services (config, vault, credentials).

There is no plugin sandboxing or permission system.

Mitigations:

  • Plugin installation requires explicit user action through the plugin manager UI
  • Plugins are sourced from the npm registry, which provides package signing and some supply-chain security
  • The providerBlacklist config option can disable specific extension points
  • Administrators can block plugin installation by blacklisting tabby-plugin-manager providers or restricting npm registry access

Warning

Installing third-party plugins grants those plugins full access to your filesystem, network, and credentials (including vault contents if unlocked). Only install plugins from trusted sources. Review plugin source code when possible, especially plugins that interact with SSH sessions or credentials.

7.10 Auto-Sudo Security

The tabby-auto-sudo-password plugin detects sudo password prompts in terminal output via pattern matching and automatically sends the stored password.

Security considerations:

  • The password is stored in the vault (encrypted) or keytar (OS-managed)
  • A malicious program could output text matching the sudo prompt pattern, causing the password to be sent to it
  • The plugin should be used only on trusted systems where you control what runs in the terminal
  • Consider disabling this plugin on shared or multi-user systems

7.11 Config File Security

The config file (config.yaml) may contain sensitive information:

  • SSH connection details (hosts, ports, usernames)
  • Port forwarding rules (revealing internal network topology)
  • SSH key file paths (revealing key locations)
  • Passwords and passphrases (if vault is not configured)
  • Known host keys (fingerprinting your infrastructure)

Hardening recommendations:

  1. Enable the vault for credential encryption — this is the single most important step
  2. Restrict file permissions: chmod 600 config.yaml (Linux/macOS) or equivalent restrictive ACLs on Windows
  3. Exclude from backups that are not themselves encrypted
  4. Use TABBY_CONFIG_DIRECTORY to place config in an encrypted volume (e.g., LUKS, FileVault, BitLocker) if the system disk is not encrypted
  5. Audit config.yaml periodically for plaintext credentials that may have been stored before vault configuration

7.12 Electron Security Baseline

Tabby's Electron configuration:

  • Node integration: Enabled in the renderer (required for plugin system)
  • Context isolation: The renderer has access to Node.js APIs (necessary for plugin architecture)
  • Content Security Policy: Configured to allow tabby:// protocol and inline scripts required by Angular
  • Remote content: The terminal does not render remote web content — all UI is local Angular components

The lack of full context isolation is a deliberate trade-off: the plugin system requires renderer-side Node.js access for native module loading (russh, serialport, keytar). This means a compromised renderer could access local filesystem and network. The mitigation is that the renderer only processes locally-generated Angular UI — it does not load arbitrary web content.


8. Developer Internals

8.1 Module Reference

8.1.1 app/lib/ — Main Process

index.ts — Entry Point

The application entry point. Execution sequence:

  1. Sets up crash reporting (Sentry) early, before any async operations
  2. Loads the config file from disk via config.ts
  3. Creates the Application instance
  4. Registers the tabby:// custom protocol for loading plugin resources
  5. Handles second-instance detection: if another instance is already running, forwards CLI args and exits
  6. Configures Electron security settings (CSP headers, webPreferences)
  7. On app.ready, creates the first window and processes any CLI arguments
// Simplified startup sequence (pseudocode from source)
setupSentry()
const config = loadConfig()
const application = new Application(config)

// Custom protocol for plugin resource loading
protocol.registerFileProtocol('tabby', (request, callback) => {
    // Maps tabby://plugin-name/resource/path → actual filesystem path
    const pluginPath = resolvePluginPath(request.url)
    callback({ path: pluginPath })
})

// Single instance enforcement
if (!app.requestSingleInstanceLock()) {
    app.quit()  // Another instance is already running
}

app.on('second-instance', (event, argv) => {
    application.handleSecondInstance(argv)
})

app.ts — Application Class

The central orchestrator for the main process. This is the largest and most important class in the main process.

Method/Property Purpose
constructor(config) Initializes tray, global hotkeys, plugin manager IPC handlers, GPU settings
newWindow() Creates a new Window instance and adds it to the window list
toggleWindow() Shows/hides the focused window (used by docking toggle hotkey)
setupTray() Creates system tray icon with context menu (Windows/macOS only)
setupGlobalHotkey() Registers the global toggle-window hotkey via Electron globalShortcut
handleSecondInstance(argv) Receives CLI args from a second instance, forwards to active window
windows: Window[] Active window list
pluginManager Plugin install/uninstall IPC bridge to renderer

Platform-specific handling:

  • Wayland detection: Checks XDG_SESSION_TYPE and WAYLAND_DISPLAY for Wayland sessions. On Wayland, global hotkey registration may behave differently depending on the compositor.
  • KDE Plasma detection: KDE Plasma has its own global shortcut system. The Application class detects KDE and warns the user if global hotkey registration fails.
  • GPU settings: Applies Electron command-line switches (--disable-gpu, --disable-software-rasterizer, etc.) based on config and platform.

window.ts — Window Class

Manages a single BrowserWindow instance. Each window is independent with its own tab set.

Method/Property Purpose
constructor(config) Creates BrowserWindow with webPreferences, sets up event handlers
present() Shows and focuses the window, bringing it to the foreground
hide() Hides the window (to tray on Windows/macOS, or minimizes on Linux)
toggleVisibility() Show/hide toggle for docking hotkey
setBounds(bounds) Sets window position and size
setDocking(position) Applies docking configuration (repositions to screen edge)
setVibrancy(enabled) Enables/disables window transparency and blur effects
isVisible Current visibility state
focused Whether window has keyboard focus

Frameless window internals:

  • frame: false removes the OS title bar
  • titleBarStyle: 'hidden' on macOS preserves traffic lights but hides the title
  • Window Controls Overlay (WCO) on Windows provides native minimize/maximize/close buttons within the custom title bar region
  • Glasstron (glasstron npm package) on Windows provides Aero blur/acrylic/mica effects behind the window

Dark mode handling: Subscribes to Electron's nativeTheme.on('updated') event to react to OS dark/light mode changes, updating the window's theme accordingly.

config.ts — Config File I/O

Handles reading and writing the YAML config file in the main process.

Function Purpose
loadConfig() Reads config.yaml via fs.readFileSync, parses with js-yaml, returns plain object
saveConfig(data) 1. Backs up current config to .backup 2. Writes new config atomically via atomically
migrateConfig(data) Detects Terminus (old project name) config directory and migrates on first launch

Atomic write sequence (via atomically library):

  1. Write data to a temporary file in the same directory
  2. fsync the temporary file to ensure data reaches disk
  3. Atomically rename temporary file to config.yaml
  4. If rename fails (e.g., cross-device), falls back to write-in-place

Error handling: If config.yaml is corrupted (invalid YAML), the loader falls back to the .backup file. If both are corrupted, a fresh default config is generated.

pty.ts — PTY Manager

Manages pseudo-terminal processes via node-pty. This is performance-critical code.

Class Purpose
PTYManager Spawns and tracks all PTY instances; handles IPC channels from renderer
PTY Individual PTY wrapper with flow control, UTF-8 splitting, and lifecycle management

Flow control implementation (detailed):

class PTY {
    private buffer: Buffer[] = []
    private bufferSize = 0
    private paused = false
    private static MAX_CHUNK = 100 * 1024    // 100 KB per IPC message
    private static MAX_DELTA = 500 * 1024    // 500 KB buffer before pause

    // Called when node-pty emits data
    onData(data: Buffer) {
        this.buffer.push(data)
        this.bufferSize += data.length

        if (this.bufferSize > PTY.MAX_DELTA) {
            this.pty.pause()       // Back-pressure: stop reading from PTY fd
            this.paused = true
        }

        this.flush()               // Send up to MAX_CHUNK to renderer
    }

    private flush() {
        while (this.buffer.length > 0) {
            const chunk = this.extractChunk(PTY.MAX_CHUNK)
            this.sendToRenderer(chunk)
        }
    }

    // Called when renderer acknowledges receipt
    onAck(bytes: number) {
        this.bufferSize -= bytes
        if (this.paused && this.bufferSize < PTY.MAX_DELTA) {
            this.pty.resume()
            this.paused = false
        }
    }
}

UTF-8 splitting: Before sending data to the renderer, utfSplitter.ts ensures the chunk does not end in the middle of a multi-byte UTF-8 sequence. If it does, the trailing bytes are held back and prepended to the next chunk.

cli.ts — CLI Parser

Uses yargs to parse command-line arguments. Defines the top-level commands:

Command Options Purpose
open --profile, --directory Opens a new tab with the specified profile or directory
run -- <command> Runs a command in a new tab
connect <url> Connects using a URL (ssh://, telnet://, serial://)
profiles (none) Lists available profiles
settings (none) Opens the settings UI

Plugins extend the CLI through CLIHandler providers in the renderer process. The main process forwards unrecognized commands to the renderer for plugin handling.

pluginManager.ts — Plugin Manager (Main Process)

Manages npm-based plugin installation in the main process.

Method Purpose
install(packageName) Runs bundled npm binary to install a plugin to {userData}/plugins/
uninstall(packageName) Removes a plugin package from the plugins directory
list() Reads {userData}/plugins/node_modules/ to list installed plugins

The plugin manager bundles its own npm binary (not the system npm) to ensure consistent behavior regardless of the user's Node.js/npm installation. IPC is used to communicate between the renderer (UI) and main process (npm operations).

portable.ts — Portable Mode Detection

Detection logic:

  1. Check for PORTABLE_EXECUTABLE_FILE environment variable
  2. Check for a data/ directory adjacent to the executable (process.execPath)
  3. If either is found, redirect app.getPath('userData') and app.getPath('appData') to data/

sentry.ts — Error Reporting

Sets up the Sentry SDK for the main process:

  • Captures unhandled exceptions via process.on('uncaughtException')
  • Captures unhandled promise rejections via process.on('unhandledRejection')
  • Tags events with app version, platform, and architecture
  • Respects enableAnalytics: false — Sentry is not initialized when disabled

lru.ts — LRU Cache

A simple least-recently-used cache with a fixed size. Used to memoize fs.realpathSync calls:

class LRUCache<K, V> {
    private cache = new Map<K, V>()
    private maxSize: number

    get(key: K): V | undefined
    set(key: K, value: V): void
    // Evicts least recently accessed entry when maxSize is exceeded
}

utfSplitter.ts — UTF-8 Byte Stream Splitter

Analyzes the trailing bytes of a Buffer to determine if they form an incomplete UTF-8 sequence:

  • 0xxxxxxx — single byte, always complete
  • 110xxxxx — 2-byte sequence, needs 1 continuation
  • 1110xxxx — 3-byte sequence, needs 2 continuations
  • 11110xxx — 4-byte sequence, needs 3 continuations
  • 10xxxxxx — continuation byte

If the buffer ends with an incomplete sequence, those bytes are held back and prepended to the next data event.

8.1.2 tabby-core/src/ — Core Framework

The foundation that all other plugins depend on. Contains no platform-specific code — everything here works on Electron and web platforms.

services/config.service.ts — ConfigService

The most critical service in the entire application. Manages all configuration state.

Member Type Purpose
store ConfigProxy Reactive config object with default merging (read/write)
changed$ ReplaySubject<void> Emits on every config mutation
ready$ AsyncSubject<void> Completes once all config providers have contributed defaults
save() async method Serializes diff-only config to YAML and writes to disk
requestRestart() method Prompts user to restart the application
enabledServices Set<string> Services currently enabled (not in providerBlacklist)
restoreDefaults() method Resets all config to defaults

ConfigProxy — reactive config access:

The ConfigProxy class wraps the raw config object with a JavaScript Proxy. This is the mechanism that makes config access "just work" — you read/write properties normally, and the proxy handles default merging, change notification, and diff-only storage:

// Reading a value:
const lines = config.store.terminal.scrollbackLines
// → If stored: returns stored value
// → If not stored: returns default from ConfigProvider contributions
// → If nested object: returns another ConfigProxy (recursive)

// Writing a value:
config.store.terminal.scrollbackLines = 50000
// → If value equals default: deletes from store (diff-only)
// → If value differs from default: stores in config
// → Triggers changed$.next()

// The deepmerge strategy for arrays is "replace" (not "append"):
// This prevents duplicate array entries when defaults change between versions

Config version migrations: The version field in config.yaml tracks the config schema version. When Tabby loads a config with an older version, migration functions update the structure to the current format.

services/app.service.ts — AppService

Manages the tab lifecycle, window state, and application-level operations.

Member Purpose
tabs: BaseTabComponent[] All open tabs in the current window
activeTab: BaseTabComponent Currently focused tab
openNewTab(type, inputs) Opens a new tab of the specified Angular component type
selectTab(tab) Focuses a tab, deselecting the previous one
closeTab(tab) Closes a tab, destroys its component, triggers tabClosed$
splitTab(tab, direction) Wraps a tab in a SplitTabComponent and adds a new pane
duplicateTab(tab) Creates a new tab with the same profile and options
explodeTab(splitTab, child) Promotes a split pane child to a standalone tab
combineTabsIntoSplit(tabs) Combines multiple standalone tabs into one split view
tabsChanged$ Emits when tabs are added, removed, or reordered
tabOpened$ Emits when a new tab opens (with the tab as payload)
tabClosed$ Emits when a tab closes (with the tab as payload)
activeTabChanged$ Emits on focus change (with the new active tab)
ready$ Completes when the Angular UI is fully initialized
hostWindow Reference to the platform window service

SplitTabComponent architecture:

SplitTabComponent implements a recursive split container:

SplitTabComponent (root)
├── SplitContainer (horizontal)
│   ├── TerminalTabComponent (left, 50%)
│   └── SplitContainer (vertical)
│       ├── SSHTerminalTabComponent (top-right, 25%)
│       └── TerminalTabComponent (bottom-right, 25%)

Split operations:

  • Split right: Creates a horizontal container with the current tab on the left and a new tab on the right
  • Split bottom: Creates a vertical container with the current tab on top and a new tab on the bottom
  • Resize: Drag the divider between panes to adjust ratios
  • Focus navigation: Cycle between panes with keyboard shortcuts

services/profiles.service.ts — ProfilesService

Member Purpose
profiles All configured profiles (user + built-in)
profileGroups Profile group hierarchy (UUID-based)
recentProfiles Recently used profiles (default: last 3)
getProviders() Collects all ProfileProvider implementations from DI
openProfile(profile) Opens a new tab for the given profile
quickConnect(query) Parses quick-connect string and opens a connection
importProfiles() Triggers profile import from all SSHProfileImporter providers
getBuiltinProfiles() Gets auto-detected profiles (shells, imported SSH configs)
getProfileDefaults(type) Gets default options for a profile type

Quick-connect string parsing:

  • user@host → SSH profile with host and user
  • user@host:port → SSH profile with host, user, and port
  • ssh://user@host:port → SSH profile (explicit protocol)
  • host:port → Telnet profile
  • telnet://host:port → Telnet profile (explicit protocol)

services/hotkeys.service.ts — HotkeysService

Collects hotkey registrations from all HotkeyProvider implementations and dispatches key events:

Member Purpose
hotkey$ Observable<string> — emits the hotkey ID when a registered key combination is pressed
getHotkeyDescription(id) Returns human-readable description for a hotkey
isEnabled(id) Whether a hotkey has at least one binding
globalHotkey$ Emits for global (system-wide) hotkeys

The service listens for keydown events on the document, matches against registered accelerators, and emits on hotkey$.

services/vault.service.ts — VaultService

Member Purpose
isEnabled: boolean Whether vault is configured (has a master passphrase)
isLocked: boolean Whether vault contents are currently encrypted in memory
lock() Zeroes the decryption key and encrypted-state cache
unlock(passphrase: string) Derives key from passphrase, decrypts vault blob
setSecret(key, value) Stores a key-value pair in the encrypted vault
getSecret(key): string Retrieves a decrypted secret by key
removeSecret(key) Deletes a secret
setEnabled(enabled, passphrase) Enables/disables vault, migrating secrets
changePassphrase(oldPass, newPass) Re-encrypts vault with new passphrase

services/commands.service.ts — CommandService

The command palette (Ctrl+Shift+P) system:

Member Purpose
commands Flattened list of all commands from all CommandProvider implementations
run(command) Executes a command's action function
getCommands() Collects, deduplicates, and filters commands (respects commandBlacklist)
lastCommand Most recently executed command (for "repeat last command")

services/themes.service.ts — ThemesService

Manages application-level themes (distinct from terminal color schemes):

Member Purpose
theme Current theme name
themes Available themes
setTheme(name) Applies a theme (CSS class changes)

services/locale.service.ts — LocaleService

Manages i18n translations:

Member Purpose
currentLocale Currently active locale code
availableLocales All available locale codes
setLocale(code) Switches the active locale, triggers retranslation

services/selector.service.ts — SelectorService

Provides reusable UI selector/picker dialogs:

  • Profile picker (with group hierarchy)
  • Color scheme picker (with preview)
  • Generic option selector (used throughout the codebase for any selection UI)

services/tabRecovery.service.ts — TabRecoveryService

Member Purpose
saveTabs(tabs) Serializes tab state using TabRecoveryProvider implementations
recoverTabs() Deserializes saved state, recreates tabs
saveInterval 30 seconds
debounceTime 1 second after changes
closedTabHistory Last 5 closed tabs (for "reopen closed tab")

services/fileProviders.service.ts — FileProvidersService

Aggregates FileProvider implementations:

Member Purpose
readFile(path) Reads a file via the platform-appropriate provider
writeFile(path, data) Writes a file atomically
pickFile(options) Shows a file picker dialog
pickDirectory() Shows a directory picker dialog

services/homeBase.service.ts — HomeBaseService

Handles communication with Tabby's optional home base service:

  • Sends anonymous usage statistics (if enableAnalytics: true)
  • Checks for available updates
  • Retrieves plugin recommendations

api/ — Public API Types

The tabby-core/src/api/ directory exports the TypeScript interfaces, abstract classes, and tokens that define the extension point contracts. This is the stable public API for plugin development:

Export Type Purpose
ConfigProvider Abstract class Contribute default config and platform-specific defaults
HotkeyProvider Abstract class Register hotkey bindings
ProfileProvider Abstract class Define profile types with create/edit/connect logic
QuickConnectProfileProvider Abstract class Parse quick-connect strings
TabRecoveryProvider Abstract class Serialize/deserialize tab state
TabContextMenuItemProvider Abstract class Right-click menu items for tabs
ToolbarButtonProvider Abstract class Main toolbar buttons
CommandProvider Abstract class Command palette entries
SettingsTabProvider Abstract class Settings UI tabs
CLIHandler Abstract class CLI subcommand handlers
ShellProvider Abstract class Local shell detection
FileProvider Abstract class File I/O abstraction
BaseTabComponent Abstract class Base class for all tab types
SplitTabComponent Component Split pane container
Platform Enum Windows, macOS, Linux, Web

8.1.3 tabby-electron/src/ — Electron Platform Bindings

Provides platform-specific implementations that require Electron/Node.js APIs. This plugin is loaded only on the desktop platform (not in the web build).

services/docking.service.ts — ElectronDockingService

Member Purpose
dock(position) Calculates screen bounds and repositions window to edge
undock() Restores window to previous floating position and size
toggle() Shows/hides the docked window with animation
handleDisplayChange() Re-calculates docking position when monitors are added/removed/resized

Position calculation: Given dockPosition, dockFill, and dockSpace:

  • dockFill controls how much of the screen edge the window fills (0-1, default 0.5)
  • dockSpace controls the usable screen ratio (0-1, default 1 — full screen width/height)
  • The window is centered along the docked edge

shells/ — Shell Providers (13 implementations)

Each provider implements the ShellProvider interface:

interface ShellProvider {
    provide(): Promise<Shell[]>
}

interface Shell {
    id: string
    name: string
    command: string
    args: string[]
    env: Record<string, string>
    icon?: string
}
Provider Platform Detection Method
WindowsDefaultShellProvider Windows Registry HKCU\...\ComSpec
WindowsStockShellsProvider Windows Known paths: System32\cmd.exe, PowerShell 5.1
PowerShellCoreShellProvider All Searches PATH for pwsh / pwsh.exe
CmderShellProvider Windows Checks CMDER_ROOT environment variable
Cygwin32ShellProvider Windows Checks C:\cygwin\bin\bash.exe
Cygwin64ShellProvider Windows Checks C:\cygwin64\bin\bash.exe
GitBashShellProvider Windows Checks Program Files\Git\bin\bash.exe and Program Files (x86)\Git\bin\bash.exe
MSYS2ShellProvider Windows Checks C:\msys64\usr\bin\bash.exe and related paths
WSLShellProvider Windows Runs wsl.exe --list --verbose, parses output for distributions
VSDevToolsProvider Windows Scans VS 2017/2019/2022 install dirs via vswhere or known paths
MacOSDefaultShellProvider macOS Runs dscl . -read /Users/$USER UserShell
LinuxDefaultShellProvider Linux Reads $SHELL env var, falls back to /etc/passwd
POSIXShellsProvider macOS/Linux Reads /etc/shells and returns all listed shells

Other Electron-specific services:

Service Purpose
ElectronPlatformService Platform detection (OS, arch), app paths, native file dialogs
ElectronHostAppService App lifecycle bridge: quit, restart, minimize, maximize
ElectronFileProvider File read/write using Node.js fs module
TouchbarService macOS Touch Bar: profile buttons, active tab info
ElectronUpdaterService electron-updater integration: check, download, apply updates
ElectronLogService Winston logger setup: file transport + console transport

8.1.4 tabby-local/src/ — Local Terminal Plugin

services/terminal.service.ts: Creates local terminal tab instances. Resolves the shell executable from the profile, prepares environment variables, and requests PTY spawn from the main process.

services/sessions.service.ts: Tracks active local sessions. Provides session lookup by tab and cleanup on tab close.

Session lifecycle (detailed):

  1. TerminalService.openTab(profile) creates a LocalTerminalTabComponent
  2. The component's ngOnInit requests PTY spawn via IPC
  3. PTYManager in the main process spawns node-pty with:
    • Shell executable from profile or detected default
    • Arguments from profile
    • Working directory from profile or $HOME
    • Environment: merge of process.env + profile overrides
    • Dimensions: current terminal size (cols x rows)
  4. The SessionMiddlewareStack is created and attached
  5. PTY output flows: node-pty → IPC → middleware pipeline → xterm.js
  6. User input flows: xterm.js → middleware pipeline (reverse) → IPC → node-pty

cli.ts: Handles tabby open --directory (opens local terminal in specified directory) and tabby run -- <command> (opens terminal running a specific command).

8.1.5 tabby-terminal/src/ — Terminal Emulation Plugin

Terminal Frontend Architecture:

abstract class Frontend {
    // xterm.js Terminal instance wrapper
    abstract attach(container: HTMLElement): void
    abstract detach(): void
    abstract write(data: string): void
    abstract clear(): void
    abstract resize(cols: number, rows: number): void
    abstract getSelection(): string
    abstract selectAll(): void
    abstract scrollToBottom(): void

    // Events
    input$: Observable<Buffer>    // User keystrokes
    resize$: Observable<{cols, rows}>
    bell$: Observable<void>
    title$: Observable<string>    // OSC title changes
}

xterm.js addon loading:

Addon Class Purpose
@xterm/addon-webgl WebglAddon GPU-accelerated rendering (default)
@xterm/addon-canvas CanvasAddon Canvas 2D fallback rendering
@xterm/addon-search SearchAddon In-terminal text search (Ctrl+Shift+F)
@xterm/addon-serialize SerializeAddon Terminal state serialization (for recovery/clipboard)
@xterm/addon-unicode11 Unicode11Addon Correct width calculation for Unicode 11 characters
@xterm/addon-ligatures LigaturesAddon Font ligature rendering (if ligatures: true)
@xterm/addon-image ImageAddon Sixel, iTerm2 image protocol, Kitty image protocol
@xterm/addon-clipboard ClipboardAddon OSC 52 clipboard sequence handling

middleware/ — Terminal Middleware Pipeline (detailed)

The middleware pipeline is a chain of processors. Each middleware can:

  • Transform data flowing to the terminal (output direction)
  • Transform data flowing from the terminal (input direction)
  • Intercept and suppress data (e.g., login script matching)
Middleware Class Input Transform Output Transform
InputProcessor InputProcessor Maps backspace key to correct escape code (\x7f or \x08 depending on platform/config) None
LoginScriptProcessor LoginScriptProcessor None Watches output for expect patterns; when matched, sends the corresponding response string. Supports regex and literal matching.
OSCProcessor OSCProcessor None Intercepts OSC (Operating System Command) escape sequences for processing (title changes, clipboard operations, custom OSC codes)
TerminalStreamProcessor TerminalStreamProcessor Handles input modes (readline vs. raw) Translates newline formats (CR, LF, CRLF) based on config
UTF8SplitterMiddleware UTF8SplitterMiddleware None Ensures output data is split at valid UTF-8 code point boundaries

SessionMiddlewareStack: Chains middleware in order. Data flows through each middleware sequentially:

Output direction (PTY → terminal):
  Session.data$ → UTF8Splitter → StreamProcessor → OSCProcessor
  → LoginScriptProcessor → InputProcessor → xterm.js.write()

Input direction (user → PTY):
  xterm.js.input$ → InputProcessor → LoginScriptProcessor
  → OSCProcessor → StreamProcessor → UTF8Splitter → Session.write()

Third-party plugins inject middleware via the SessionMiddleware multi-provider token.

Login script example:

options:
  scripts:
    - expect: "login:"
      send: "admin"
      isRegex: false
    - expect: "Password:"
      send: "s3cret"
      isRegex: false
    - expect: "\\$\\s*$"        # Shell prompt
      send: "cd /app && ls"
      isRegex: true

The LoginScriptProcessor processes scripts sequentially — once a pattern matches and the response is sent, it moves to the next script entry. After all scripts are processed, the middleware becomes a passthrough.

8.1.6 tabby-ssh/src/ — SSH Plugin

The SSH plugin is the most complex plugin in Tabby, implementing a full SSH client via the russh Rust-based native library (v0.1.37).

Core services:

Service File Purpose
SSHService services/ssh.service.ts Connection orchestration: handshake, auth, channel management
SSHMultiplexerService services/sshMultiplexer.service.ts Connection pooling: reuses transports for same host
SSHKnownHostsService services/sshKnownHosts.service.ts Host key verification (TOFU + OpenSSH known_hosts)
PasswordStorageService services/passwordStorage.service.ts Credential storage bridge (vault/keytar/plaintext)

SSH session types:

Class Purpose Channel Type
SSHSession Interactive shell session channel with PTY request
SFTPSession File transfer subsystem channel (sftp)
ForwardedPort Port forwarding direct-tcpip or forwarded-tcpip channel
X11Socket X11 forwarding x11 channel

SSH connection flow (detailed):

1. ProfilesService.openProfile(sshProfile)
   │
2. SSHService.createSession(profile)
   │
3. Resolve connection target
   ├── Direct: host:port from profile
   ├── Jump host: recursively connect to jumpHost profile first
   ├── SOCKS proxy: connect via SOCKS5 tunnel
   ├── HTTP proxy: connect via HTTP CONNECT
   └── ProxyCommand: spawn command, use stdin/stdout as transport
   │
4. Check SSHMultiplexerService for existing transport
   ├── Found (reuseSession: true): skip to step 7
   └── Not found: proceed to step 5
   │
5. TCP connection establishment
   │
6. russh handshake
   ├── Version exchange
   ├── KEX (key exchange) — negotiates shared secret
   ├── Host key verification
   │   ├── SSHKnownHostsService.check(host, port, key)
   │   ├── If new: prompt user, store on accept
   │   ├── If match: continue
   │   └── If mismatch: show warning, user must explicitly override
   │
7. Authentication (tries in order until success)
   ├── Public key (from profile.privateKeys)
   │   ├── Try each key in order
   │   ├── Decrypt passphrase-protected keys (vault/keytar/prompt)
   │   └── AutoPrivateKeyLocator: scan ~/.ssh/id_* if no keys in profile
   ├── SSH agent (if agent: auto or explicitly configured)
   │   └── Forward agent request to local SSH agent
   ├── Password (from vault/keytar/prompt)
   └── Keyboard-interactive (server-driven challenge-response)
   │
8. Channel open
   ├── Shell session: request PTY, request shell
   ├── SFTP: request subsystem "sftp"
   ├── Port forward: request direct-tcpip or tcpip-forward
   └── X11: accept x11 channel requests
   │
9. Attach to TerminalTabComponent
   │
10. SessionMiddlewareStack processes I/O

SFTP implementation:

  • Full SFTP subsystem client built on russh
  • Operations: list, upload, download, rename, delete, mkdir, rmdir, chmod, stat
  • Progress tracking for file transfers (bytes transferred, percentage)
  • Resumable uploads (not built-in, but possible by seeking to offset)
  • Context menu extensibility via SFTPContextMenuItemProvider

WinSCP integration: When ssh.winSCPPath is configured, an "Open in WinSCP" action is available. SSHService constructs WinSCP command-line arguments:

WinSCP.exe sftp://user:password@host:port/

Password is passed via command line if available (security concern — visible in process list). For better security, use WinSCP's session storage.

Profile import:

  • SSHProfileImporter implementations scan for SSH profiles from external sources
  • OpenSSH config parser reads ~/.ssh/config and maps Host entries to Tabby profiles
  • Handles Include directives, Match blocks, and wildcard hosts

SSH algorithm negotiation:

  • Default algorithm lists (§7.3) are compiled into the russh native module
  • Per-profile overrides are passed to russh during handshake setup
  • russh handles the actual cryptographic operations in Rust (not JavaScript)

8.1.7 tabby-serial/src/ — Serial Plugin

Uses serialport npm package v11.0.1 for hardware serial communication.

services/serial.service.ts:

Member Purpose
listPorts() Calls SerialPort.list() to enumerate available serial ports
createSession(profile) Opens a serial connection with the specified parameters

Port enumeration returns:

  • Port path (COM3, /dev/ttyUSB0, /dev/tty.usbserial-*)
  • Manufacturer (if available via USB descriptors)
  • Serial number (if available)
  • PnP ID (Windows)
  • Location ID (macOS)

Slow-send mode (slowSend: true):

  • Sends data one character at a time with a configurable delay between each character
  • Useful for devices with small input buffers (microcontrollers, network equipment with limited UART buffers)
  • The delay prevents buffer overrun on the receiving device

8.1.8 tabby-telnet/src/ — Telnet Plugin

Provides Telnet protocol and raw TCP socket connections:

Mode Default Port Implementation
Telnet 23 Telnet option negotiation (WILL/WONT/DO/DONT)
Raw socket 1234 Plain TCP net.Socket, no protocol

The Telnet implementation handles standard Telnet option negotiation for terminal type, window size, and echo mode.

8.1.9 tabby-settings/src/ — Settings UI Plugin

Provides the settings interface. The settings window is a tab with sub-tabs contributed by plugins via SettingsTabProvider.

Built-in settings tabs:

  • General (appearance, behavior)
  • Terminal (rendering, scrollback, keyboard)
  • Profiles (CRUD for all profile types)
  • Hotkeys (visual hotkey editor)
  • Vault (enable, lock, change passphrase)
  • Config file (raw YAML editor with syntax highlighting)

services/ within tabby-settings:

Service Purpose
HotkeySettingsService Hotkey binding capture and validation
ProfileSettingsService Profile creation, editing, deletion, import
VaultSettingsService Vault enable/disable, lock/unlock, passphrase change
ConfigSyncService Config export (download as file) and import (upload file)

8.1.10 tabby-plugin-manager/src/ — Plugin Manager Plugin

services/pluginManager.service.ts:

Member Purpose
installedPlugins Array of installed third-party plugins with metadata
availablePlugins Array of plugins from npm registry search
install(name, version?) Installs a plugin via bundled npm (IPC to main process)
uninstall(name) Removes a plugin package
update(name) Updates a plugin to the latest version
isInstalled(name) Checks if a plugin is installed
checkForUpdates() Compares installed versions against npm registry

Plugin search: Queries the npm registry for packages with the tabby-plugin keyword. Results include package name, description, version, download count, and author.

Plugin compatibility: Plugins declare their compatible Tabby version via peerDependencies in package.json. The plugin manager warns (but does not prevent) installation of incompatible plugins.

Detects clickable patterns in terminal output using TerminalDecorator:

  • URLs: Matches http://, https://, ftp:// followed by valid URL characters
  • IP addresses: IPv4 dotted-decimal notation, IPv6 with brackets
  • File paths: Platform-specific path patterns (absolute paths)

When a pattern is detected, xterm.js renders it as a clickable link. Clicking opens the URL in the default browser or the file in the default application.

8.1.12 tabby-community-color-schemes/ — Color Schemes

Contains 150+ terminal color schemes in the schemes/ directory. Each scheme is defined as a TypeScript object with:

{
    name: 'Dracula',
    foreground: '#f8f8f2',
    background: '#282a36',
    cursor: '#f8f8f2',
    selection: '#44475a',
    colors: [
        '#21222c',  // black
        '#ff5555',  // red
        '#50fa7b',  // green
        '#f1fa8c',  // yellow
        '#bd93f9',  // blue
        '#ff79c6',  // magenta
        '#8be9fd',  // cyan
        '#f8f8f2',  // white
        '#6272a4',  // bright black
        '#ff6e6e',  // bright red
        '#69ff94',  // bright green
        '#ffffa5',  // bright yellow
        '#d6acff',  // bright blue
        '#ff92df',  // bright magenta
        '#a4ffff',  // bright cyan
        '#ffffff',  // bright white
    ],
}

Registered via TerminalColorSchemeProvider. Users can also define custom color schemes in the settings.

8.1.13 tabby-auto-sudo-password/src/ — Auto-Sudo Plugin

Detects sudo password prompts in terminal output via regex pattern matching. When a match is found:

  1. Retrieves the stored password from vault/keytar
  2. Sends the password followed by a newline to the terminal session
  3. Clears the password from memory

The detection pattern matches common sudo prompt formats across Linux distributions.

8.1.14 tabby-uac/ — Windows UAC Helper

A C++ native module (UAC/) that provides UAC (User Account Control) elevation on Windows. The module uses Windows API calls (ShellExecuteExW with runas verb) to request elevation. Used when Tabby needs to perform operations requiring administrator privileges.

8.1.15 tabby-web/src/ — Web Platform Plugin

Provides platform service implementations for running Tabby in a web browser without Electron. Replaces:

  • ElectronPlatformService → WebPlatformService
  • ElectronHostAppService → WebHostAppService
  • ElectronFileProvider → WebFileProvider (using File API)

Limitations in web mode: no local terminal (no PTY), no native SSH (no russh), no serial ports, no system tray, no global hotkeys, no auto-updates.

8.1.16 tabby-web-demo/src/ — Demo Plugin

Provides a fully browser-based demo using the v86 JavaScript x86 emulator. Ships with a minimal Linux image in data/ that boots inside the browser. This allows users to try Tabby's terminal emulation without installing anything.

8.2 Class Hierarchy

The codebase contains 158 classes (per AST extraction). The key inheritance hierarchies:

BaseTabComponent (abstract — tabby-core)
│   Base class for all tab types. Provides lifecycle hooks,
│   activity tracking, title management, and destruction.
│
├── TerminalTabComponent (tabby-terminal)
│   │   Base for terminal-based tabs. Manages xterm.js instance,
│   │   session attachment, middleware stack, and resize handling.
│   │
│   ├── LocalTerminalTabComponent (tabby-local)
│   │       Local PTY terminal. Spawns shell process via node-pty.
│   │
│   ├── SSHTerminalTabComponent (tabby-ssh)
│   │       SSH terminal. Manages SSHSession lifecycle and SFTP access.
│   │
│   ├── SerialTerminalTabComponent (tabby-serial)
│   │       Serial terminal. Manages serialport connection.
│   │
│   └── TelnetTerminalTabComponent (tabby-telnet)
│           Telnet/raw socket terminal.
│
└── SplitTabComponent (tabby-core)
        Container for split pane layouts. Recursively contains
        other BaseTabComponent instances in horizontal/vertical splits.
BaseSession (abstract — tabby-terminal)
│   Base for session types. Provides data$, closed$, write(), resize().
│
├── LocalSession (tabby-local)
│       Wraps node-pty PTY. Handles spawn, write, resize, kill.
│
├── SSHSession (tabby-ssh)
│       Wraps russh SSH channel. Handles auth, shell, exec, env.
│
├── SerialSession (tabby-serial)
│       Wraps serialport SerialPort. Handles open, write, close.
│
└── TelnetSession (tabby-telnet)
        Wraps net.Socket. Handles connect, write, close.
SessionMiddleware (abstract — tabby-terminal)
│   Base for I/O middleware. Provides feedFromSession(), feedFromTerminal().
│
├── InputProcessor — Backspace/delete mapping
├── LoginScriptProcessor — Expect/send automation
├── OSCProcessor — OSC escape sequence interception
├── TerminalStreamProcessor — I/O mode and newline translation
└── UTF8SplitterMiddleware — UTF-8 boundary alignment
ConfigProvider (abstract — tabby-core)
│   Contributes config defaults and platform-specific defaults.
│   One implementation per plugin.
│
├── CoreConfigProvider (tabby-core)
├── ElectronConfigProvider (tabby-electron)
├── TerminalConfigProvider (tabby-terminal)
├── SSHConfigProvider (tabby-ssh)
├── SerialConfigProvider (tabby-serial)
├── TelnetConfigProvider (tabby-telnet)
└── ... (one per plugin)
ProfileProvider (abstract — tabby-core)
│   Defines a connection profile type.
│
├── LocalProfileProvider (tabby-local)
├── SSHProfileProvider (tabby-ssh)
├── SerialProfileProvider (tabby-serial)
└── TelnetProfileProvider (tabby-telnet)
ShellProvider (abstract — tabby-core)
│   Detects available shells on the platform.
│
├── WindowsDefaultShellProvider
├── WindowsStockShellsProvider
├── PowerShellCoreShellProvider
├── CmderShellProvider
├── Cygwin32ShellProvider / Cygwin64ShellProvider
├── GitBashShellProvider
├── MSYS2ShellProvider
├── WSLShellProvider
├── VSDevToolsProvider
├── MacOSDefaultShellProvider
├── LinuxDefaultShellProvider
└── POSIXShellsProvider

8.3 Code Patterns

Angular Dependency Injection

All services use Angular's @Injectable() decorator. Multi-providers use multi: true to allow multiple implementations of the same token:

// Registering extension points in a plugin module
@NgModule({
    providers: [
        // Single provider
        { provide: MyService, useClass: MyServiceImpl },

        // Multi-provider (multiple plugins can contribute)
        { provide: ConfigProvider, useClass: MyConfigProvider, multi: true },
        { provide: HotkeyProvider, useClass: MyHotkeyProvider, multi: true },
        { provide: ProfileProvider, useClass: MyProfileProvider, multi: true },
        { provide: CommandProvider, useClass: MyCommandProvider, multi: true },
        { provide: TerminalColorSchemeProvider, useClass: MyColorSchemes, multi: true },
    ],
})
export default class MyPluginModule {}

Observable Patterns (RxJS)

The codebase uses RxJS extensively for reactive state management and event handling:

// Config change reaction
this.configService.changed$.pipe(
    debounceTime(500)
).subscribe(() => {
    this.applySettings()
})

// Tab lifecycle events
this.appService.tabOpened$.subscribe(tab => {
    if (tab instanceof TerminalTabComponent) {
        this.attachDecorator(tab)
    }
})

// Session data streaming
this.session.data$.subscribe(data => {
    this.frontend.write(data)
})

// Cleanup
ngOnDestroy() {
    this.subscriptions.forEach(s => s.unsubscribe())
}

Async Subjects for Readiness

Services that require asynchronous initialization use AsyncSubject to signal readiness:

class ConfigService {
    ready$ = new AsyncSubject<void>()

    async init() {
        await this.loadProviders()
        await this.loadConfig()
        this.mergeDefaults()
        this.ready$.next()
        this.ready$.complete()  // Signals readiness to all subscribers
    }
}

// Consumer waits for readiness:
await firstValueFrom(this.configService.ready$)
// or
this.configService.ready$.subscribe(() => {
    // Config is now available
})

Atomic File Operations

All config writes use the atomically library to prevent corruption:

import { writeFile } from 'atomically'

// This writes to a temp file, fsyncs, then atomically renames
await writeFile(configPath, yaml.dump(data))

Platform Abstraction Pattern

Platform-specific code is isolated behind abstract services. The Electron and Web plugins provide different implementations:

// Abstract service (tabby-core — no platform dependency)
@Injectable()
abstract class PlatformService {
    abstract getConfigPath(): string
    abstract showOpenDialog(options): Promise<string[]>
    abstract showSaveDialog(options): Promise<string>
    abstract getOSRelease(): string
}

// Electron implementation (tabby-electron)
@Injectable()
class ElectronPlatformService extends PlatformService {
    getConfigPath() {
        return path.join(electron.app.getPath('userData'), 'config.yaml')
    }
    showOpenDialog(options) {
        return electron.dialog.showOpenDialog(options)
    }
}

// Web implementation (tabby-web)
@Injectable()
class WebPlatformService extends PlatformService {
    getConfigPath() {
        return '/virtual/config.yaml'  // IndexedDB-backed
    }
    showOpenDialog(options) {
        // Uses File API
    }
}

Module Export Convention

Every plugin exports its Angular module as the default export:

// tabby-my-plugin/src/index.ts
@NgModule({
    imports: [CommonModule, FormsModule],
    declarations: [MyComponent],
    providers: [...],
})
export default class MyPluginModule {}

The plugin loader expects default exports and uses them to register the module with Angular's injector.

8.4 Error Handling Patterns

Key error conditions and their handling:

Error Message Source Context Handling
"No private keys in profile" SSH auth No keys configured and none auto-discovered Falls back to password auth or prompts user
"WinSCP not found" SFTP UI ssh.winSCPPath not set or invalid Shows notification suggesting WinSCP download
"Cannot save an empty store" ConfigService Prevents accidental config wipe from bugs Write is silently skipped
"Vault not configured" VaultService Operation requires vault but vault: null Prompts user to set up vault passphrase
"Vault is locked" VaultService Operation requires decrypted vault Prompts for vault passphrase
"Frontend not ready" TerminalTab xterm.js not yet initialized when data arrives Buffers data until frontend is ready
"Did not find any bootstrap components" Angular bootstrap Plugin loading failed catastrophically Fatal error — app cannot start

Error reporting flow:

  1. Unhandled errors in main process → captured by Sentry (if enabled)
  2. Unhandled errors in renderer → caught by Angular's ErrorHandler, logged to console, optionally sent to Sentry
  3. Session errors (SSH disconnect, serial port close) → caught by session layer, surfaced as notifications via ngx-toastr
  4. Config errors (invalid YAML) → fallback to backup, log warning

8.5 Testing Strategy

The project uses ESLint for static analysis and TypeScript's compiler for type checking:

# Run ESLint across all packages
yarn run lint

# TypeScript type checking (implicit in build)
yarn run build

No unit test framework (Jest, Mocha, etc.) or test files were identified in the source tree. The project's testing approach relies on:

  1. TypeScript strict mode: Catches type errors at compile time
  2. ESLint: Enforces code quality and consistency rules
  3. Angular compiler: Validates templates and dependency injection at build time
  4. Manual testing: Features are tested manually across platforms
  5. Plugin isolation: The plugin architecture provides natural boundaries — changes in one plugin typically do not affect others
  6. Community testing: As an open-source project with a large user base, issues are quickly reported

Recommended testing approach for contributors:

  1. Test the specific feature/plugin being modified on at least one platform
  2. Cross-platform testing for changes that touch platform-specific code
  3. Build all plugins to verify no compilation errors are introduced
  4. Run yarn run lint to ensure code style compliance
  5. Test with multiple xterm.js frontends (webgl + canvas) for rendering changes
  6. For SSH changes: test with various server implementations (OpenSSH, Dropbear) and key types

8.6 Contributing Guide

Setting Up the Development Environment

# 1. Prerequisites
# - Node.js 22.x
# - Yarn 1.x (npm install -g yarn)
# - Rust toolchain (rustup.rs)
# - Platform build tools (see §2.3)

# 2. Clone the repository
git clone https://github.com/Eugeny/tabby.git
cd tabby

# 3. Install all dependencies (workspace-aware)
yarn install

# 4. Rebuild native modules for Electron's Node.js ABI
npx @electron/rebuild

# 5. Build all plugins and the main app
yarn run build

# 6. Start in development mode
TABBY_DEV=1 yarn run start
# On Windows: set TABBY_DEV=1 && yarn run start

Development Workflow

# Terminal 1: Watch mode (rebuilds on file changes)
yarn run watch

# Terminal 2: Start Tabby (restart manually after watch rebuilds)
yarn run start

The watch command runs webpack in watch mode for all plugin packages simultaneously. When a TypeScript/Angular source file changes, the affected plugin is rebuilt incrementally (typically 1-5 seconds).

Hot reload is not supported — after a rebuild, you must restart the Electron app to load the new code. Press Ctrl+R in the app window to reload the renderer process (faster than full restart, but does not reload main process changes).

Creating a New Plugin

  1. Create directory and initialize:
mkdir tabby-my-feature
cd tabby-my-feature
  1. Create package.json:
{
    "name": "tabby-my-feature",
    "version": "1.0.0",
    "description": "My Tabby plugin",
    "keywords": ["tabby-plugin"],
    "main": "dist/index.js",
    "peerDependencies": {
        "tabby-core": "*",
        "tabby-terminal": "*"
    },
    "devDependencies": {
        "typescript": "^4.9.5"
    }
}
  1. Create webpack.config.mjs (copy from tabby-linkifier/webpack.config.mjs as a template — it is one of the simpler plugins)

  2. Create src/index.ts:

import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { ConfigProvider, CommandProvider } from 'tabby-core'

@Injectable()
class MyConfigProvider extends ConfigProvider {
    defaults = {
        myFeature: {
            enabled: true,
        },
    }
}

@Injectable()
class MyCommandProvider extends CommandProvider {
    async getCommands() {
        return [{
            id: 'my-feature:do-something',
            label: 'My Feature: Do Something',
            run: async () => {
                // Command implementation
            },
        }]
    }
}

@NgModule({
    imports: [CommonModule],
    providers: [
        { provide: ConfigProvider, useClass: MyConfigProvider, multi: true },
        { provide: CommandProvider, useClass: MyCommandProvider, multi: true },
    ],
})
export default class MyFeatureModule {}
  1. Add to workspace in root package.json:
{
    "workspaces": {
        "packages": [
            "...",
            "tabby-my-feature"
        ]
    }
}
  1. Build and test:
yarn install
yarn run build
yarn run start

Code Style Requirements

  • TypeScript strict mode ("strict": true in tsconfig.json)
  • 4-space indentation (no tabs)
  • Single quotes for strings
  • Trailing commas in multi-line constructs
  • Angular component naming: *Component for components, *Service for services, *Provider for DI providers
  • File naming: kebab-case.ts for files, PascalCase for classes
  • ESLint enforced — run yarn run lint before submitting PRs
  • No any types unless absolutely necessary (and documented why)

Internationalization for Plugin Developers

Making plugin strings translatable:

import { TranslateService } from '@ngx-translate/core'

@Injectable()
export class MyService {
    constructor(private translate: TranslateService) {}

    getLabel(): string {
        return this.translate.instant('My translatable string')
    }
}

In Angular templates:

<span translate>My translatable string</span>
<!-- or -->
<span>{{ 'My translatable string' | translate }}</span>

After adding new strings, run yarn run i18n:extract to update the .po files.


9. Integration Points

9.1 OpenSSH Config Import

Tabby imports SSH profiles from OpenSSH's ~/.ssh/config file via SSHProfileImporter. Supported directives:

Directive Mapped To Notes
Host Profile name Wildcard patterns are expanded
HostName options.host
Port options.port Default: 22
User options.user Default: root
IdentityFile options.privateKeys ~ is expanded to home directory
ProxyJump options.jumpHost Resolved to Tabby profile UUID
ForwardAgent options.agentForward
LocalForward options.forwardedPorts (local)
RemoteForward options.forwardedPorts (remote)
DynamicForward options.forwardedPorts (dynamic)
ServerAliveInterval options.keepaliveInterval Converted from seconds to milliseconds
ServerAliveCountMax options.keepaliveCountMax
Include Recursive file inclusion Supports globs

Unsupported directives are silently ignored. The import is one-way — changes in Tabby do not write back to ~/.ssh/config.

9.2 WinSCP Integration

For graphical SFTP file management on Windows:

ssh:
  winSCPPath: "C:\\Program Files (x86)\\WinSCP\\WinSCP.exe"

When configured, an "Open in WinSCP" button appears in SSH terminal tabs. SSHService constructs WinSCP command-line arguments to pass host, port, username, and optionally password/key path.

9.3 Cmder Integration

When Cmder is detected (via CMDER_ROOT environment variable), CmderShellProvider:

  • Registers Cmder as an available shell
  • Points to Cmder's vendor/init.bat for shell initialization
  • Supports both cmd and PowerShell Cmder profiles
  • Clink integration is preserved through Cmder's init scripts

9.4 WSL Integration

WSLShellProvider discovers installed WSL distributions:

# Internal enumeration command
wsl.exe --list --verbose

Each distribution appears as a separate shell profile with:

  • Name: distribution name (e.g., "Ubuntu-22.04")
  • Command: wsl.exe -d <distro-name>
  • Both WSL 1 and WSL 2 distributions are supported

9.5 VS Developer Tools Integration

VSDevToolsProvider detects Visual Studio installations (2017, 2019, 2022) and offers:

  • Developer Command Prompt: cmd.exe with VS build tools in PATH
  • Developer PowerShell: PowerShell with VS build tools in PATH

Detection uses vswhere.exe (bundled with VS Build Tools) or scans known installation directories.

The extras/clink/ directory contains Clink scripts that enhance cmd.exe when launched through Tabby:

  • Readline-style line editing (arrow keys, Ctrl+R history search)
  • Tab completion improvements
  • History persistence
  • Prompt customization hooks

9.7 macOS Automator Workflows

extras/automator-workflows/ contains macOS Automator workflows:

  • Open Tabby Here: Right-click a folder in Finder → "Open Tabby Here" opens a terminal in that directory
  • Integration with Finder's Services menu

9.8 System Tray Integration

On Windows and macOS:

  • Left-click: Toggle window visibility (useful with docking)
  • Right-click context menu:
    • Profile quick-launch (top N profiles as menu items)
    • Show/hide window
    • Quit Tabby

The tray icon is not available on Linux due to inconsistent tray support across desktop environments (particularly Wayland compositors).

9.9 Touch Bar (macOS)

TouchbarService provides macOS Touch Bar integration:

  • Profile quick-launch buttons (configurable which profiles appear)
  • Active tab indicator
  • Connection status (connected/disconnected) for SSH/serial tabs

9.10 Third-Party Plugin Ecosystem

The plugin manager searches npm for packages with the tabby-plugin keyword. The ecosystem includes plugins for:

  • Additional terminal color schemes and themes
  • Enhanced search and filtering
  • Workspace/session management
  • Snippet managers and text expansion
  • Custom shell integrations
  • Notification integrations

Plugin API stability commitment: The extension points defined in tabby-core/src/api/ form the stable public API. The multi-provider tokens (ConfigProvider, ProfileProvider, CommandProvider, etc.) are maintained for backward compatibility across minor versions. Internal service APIs may change between versions without notice.

9.11 Troubleshooting

Application Will Not Start (Windows)

Symptom: Tabby fails to launch, or shows "Cannot create process error 267."

Resolution:

  1. Check the log file at %APPDATA%\tabby\log.txt for error details
  2. Verify no other Tabby instance is running (Task Manager → look for tabby.exe)
  3. Try command-line launch to see console output: tabby.exe --no-sandbox
  4. Reset config: rename %APPDATA%\tabby\config.yaml to config.yaml.bak and restart
  5. If error 267 ("The directory name is invalid"): a profile's cwd points to a directory that no longer exists. Edit config.yaml and remove or fix invalid cwd values.

Note

Error 267 occurs when Windows cannot create a process in the specified directory. This typically happens when a profile's working directory has been deleted or a network drive is disconnected. Editing config.yaml to remove the offending cwd value (or setting it to an empty string for the home directory) resolves the issue.

SSH Key Authentication Failures

Symptom: "No private keys in profile" error or authentication fails despite correct keys.

Resolution:

  1. Verify the key file path in the profile is absolute and the file exists at that path
  2. Check key file permissions: chmod 600 ~/.ssh/id_* (Linux/macOS); on Windows, ensure only the user has read access
  3. If using an SSH agent, verify it is running and the key is loaded: ssh-add -l
  4. On macOS, use the full path (/Users/name/.ssh/id_ed25519), not ~/.ssh/id_ed25519 — tilde expansion may not work in all contexts
  5. For CA-signed keys, specify both the certificate (.pub with certificate) and the private key
  6. Try enabling verbose SSH logging in the developer console to see the exact auth failure

Tip

The AutoPrivateKeyLocator automatically scans ~/.ssh/ for files named id_* (excluding .pub files) and offers them during connection. If your key has a non-standard name, add its path explicitly to the profile's privateKeys array.

Crashes on Ubuntu 24.04

Symptom: Tabby crashes on startup on Ubuntu 24.04 or later.

Resolution:

  1. Launch with GPU disabled: tabby --disable-gpu
  2. If running Wayland, try X11 mode: tabby --ozone-platform=x11
  3. Install required libraries: sudo apt install libsecret-1-0 libx11-dev
  4. Check for conflicting Electron applications or GPU driver issues
  5. For Snap installs: snap refresh tabby to ensure latest version

High Memory Consumption

Symptom: Tabby uses excessive RAM with many tabs open.

Resolution:

  1. Reduce scrollback buffer: set terminal.scrollbackLines to 5000-10000 (default is 25000)
  2. Disable ligatures if not used: terminal.ligatures: false
  3. Switch renderer: terminal.frontend: xterm-canvas to reduce GPU memory
  4. Disable image protocol: terminal.sixel: false
  5. Close tabs that are no longer needed — each tab maintains a full xterm.js instance with its own scrollback buffer
  6. Check for plugins that may be accumulating state

Symptom: Terminal hangs when opening cmd.exe with Clink active.

Resolution:

  1. Update Clink to the latest version
  2. If issue persists, temporarily disable Clink by removing it from the Cmder/cmd profile
  3. Use PowerShell or Git Bash as an alternative
  4. Check Clink's log files for errors

SSH Auto-Disconnect Through Jump Server

Symptom: SSH connections through a jump server disconnect after periods of inactivity.

Caution

Setting keepaliveInterval below 1000ms can trigger rate limiting on some SSH servers. Values of 3000-5000ms are generally safe. If connections still drop, the issue is likely a NAT timeout between you and the jump server (common with corporate firewalls — typical timeout is 5-15 minutes).

Resolution:

  1. Reduce keepalive interval and increase count:
    options:
      keepaliveInterval: 3000    # 3 seconds
      keepaliveCountMax: 20      # More retries before giving up
    
  2. Configure keepalive on the jump server's sshd_config: ClientAliveInterval 30
  3. If behind a NAT/firewall with short timeouts, set keepalive interval below the NAT timeout
  4. Consider using tmux or screen on the remote side for session persistence

Symptom: Links in terminal output include trailing semicolons or other punctuation, or are not detected at all.

Resolution: This is a known limitation of the URL regex pattern in the linkifier. URLs followed by certain punctuation characters may include the punctuation in the detected URL. Workarounds:

  • Manually select and copy the URL
  • Add a space after URLs in scripts to ensure clean detection
  • Monitor the upstream issue tracker for regex improvements