# Tabby — Administrator & Developer Guide

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](/guides/Eugeny/tabby/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.

```yaml
# 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.

```typescript
// 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

```yaml
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:

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

#### SSH Profile Options (Complete Reference)

```yaml
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)

```yaml
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)

```yaml
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)

```yaml
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:

```yaml
# 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:

```yaml
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:

```yaml
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.

```yaml
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:

```yaml
# 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

```bash
# 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

```bash
# 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

```bash
# 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

```bash
# 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`:

```bash
# 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:**

```bash
# 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:

```yaml
# 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:**
```yaml
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:

```yaml
# 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.

```bash
# 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:

```yaml
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

```yaml
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

```yaml
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:

```yaml
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:

```bash
# 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:**
```bash
# 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

#### Performance-Related Config

```yaml
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:

```yaml
# 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:

```yaml
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.

```yaml
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:

```yaml
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:

```yaml
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

```yaml
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:

```yaml
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

```typescript
// 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):**

```typescript
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:

```typescript
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:

```typescript
// 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:

```typescript
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:**

```typescript
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:**

```yaml
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:

```bash
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.

#### 8.1.11 `tabby-linkifier/src/` — Link Detection Plugin

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:

```typescript
{
    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:

```typescript
// 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:

```typescript
// 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:

```typescript
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:

```typescript
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:

```typescript
// 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:

```typescript
// 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:

```bash
# 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

```bash
# 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

```bash
# 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:**
```bash
mkdir tabby-my-feature
cd tabby-my-feature
```

2. **Create `package.json`:**
```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"
    }
}
```

3. **Create `webpack.config.mjs`** (copy from `tabby-linkifier/webpack.config.mjs` as a template — it is one of the simpler plugins)

4. **Create `src/index.ts`:**
```typescript
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 {}
```

5. **Add to workspace** in root `package.json`:
```json
{
    "workspaces": {
        "packages": [
            "...",
            "tabby-my-feature"
        ]
    }
}
```

6. **Build and test:**
```bash
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:

```typescript
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:
```html
<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:

```yaml
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:

```bash
# 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.

### 9.6 Clink Integration

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

#### Stuck on "CMD (clink)" on Windows

**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:
   ```yaml
   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

#### Hyperlink Detection Issues

**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

---

_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. Unofficial; not affiliated with or reviewed by the project._
