[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

457
domain/models/connection.ts Normal file
View File

@@ -0,0 +1,457 @@
import type { SftpFilenameEncoding } from './sftp';
import type { KeywordHighlightRule } from './terminal';
// Proxy configuration for SSH connections
type ProxyType = 'http' | 'socks5' | 'command';
// UI locale identifier, stored in settings and used for i18n (e.g., "en", "zh-CN").
export type UILanguage = string;
export interface ProxyConfig {
type: ProxyType;
host: string;
port: number;
command?: string;
identityId?: string;
username?: string;
password?: string;
}
export interface ProxyProfile {
id: string;
label: string;
config: ProxyConfig;
createdAt: number;
updatedAt?: number;
order?: number;
}
// Host chain configuration for jump host / bastion connections
export interface HostChainConfig {
hostIds: string[]; // Array of host IDs in order (first = closest to client)
}
export type MultiLineRunMode = 'lineDelay' | 'paste';
// Per-host SSH algorithm override lists (advanced). Each property, when
// present and non-empty, fully replaces the offered list for that category.
// Category names mirror ssh2's `algorithms` shape (note: `compress`, not
// `compression`). Empty arrays or missing properties keep the default.
export interface HostAlgorithmOverrides {
kex?: string[];
cipher?: string[];
hmac?: string[];
serverHostKey?: string[];
compress?: string[];
}
// Environment variable for SSH session
export interface EnvVar {
name: string;
value: string;
}
// Protocol type for connections
export type BuiltInHostProtocol = 'ssh' | 'telnet' | 'mosh' | 'et' | 'local' | 'serial';
export type PluginHostProtocol = `plugin:${string}`;
export type HostProtocol = BuiltInHostProtocol | PluginHostProtocol;
export type PluginConfigurationValue =
| null
| boolean
| number
| string
| PluginConfigurationValue[]
| { [key: string]: PluginConfigurationValue };
export interface PluginConnectionConfig {
/** Exact namespaced connection Provider contribution ID. */
providerId: string;
/** Opaque, schema-validated Provider configuration retained if the plugin is absent. */
configuration: PluginConfigurationValue;
authenticationProviderId?: string;
/** Host-owned opaque credential reference; never plaintext. */
credentialId?: string;
}
export type HostIconMode = 'auto' | 'custom';
export type HostIconColorMode = 'auto' | 'manual';
export type HostIconId =
| 'server'
| 'terminal'
| 'database'
| 'cloud'
| 'router'
| 'shield'
| 'code'
| 'box'
| 'globe'
| 'cpu'
| 'hard-drive'
| 'network'
| 'wifi'
| 'lock'
| 'key'
| 'monitor'
| 'container'
| 'activity'
| 'zap'
| 'server-cog';
export type HostIconColorId =
| 'blue'
| 'green'
| 'red'
| 'amber'
| 'purple'
| 'cyan'
| 'orange'
| 'slate'
| 'violet'
| 'pink'
| 'rose'
| 'lime'
| 'teal'
| 'sky'
| 'indigo'
| 'zinc';
// Serial port configuration
export type SerialParity = 'none' | 'even' | 'odd' | 'mark' | 'space';
export type SerialFlowControl = 'none' | 'xon/xoff' | 'rts/cts';
export interface SerialConfig {
path: string; // Serial port path (e.g., /dev/ttyUSB0, COM1)
baudRate: number; // Baud rate (e.g., 9600, 115200)
dataBits?: 5 | 6 | 7 | 8; // Data bits (default: 8)
stopBits?: 1 | 1.5 | 2; // Stop bits (default: 1)
parity?: SerialParity; // Parity (default: 'none')
flowControl?: SerialFlowControl; // Flow control (default: 'none')
localEcho?: boolean; // Force local echo (default: false, rely on remote echo)
lineMode?: boolean; // Line mode - buffer input and send on Enter (default: false)
// Store the default explicitly so an open/restored session keeps its launch-time behavior.
backspaceBehavior?: 'default' | 'ctrl-h';
}
// Per-protocol configuration
interface ProtocolConfig {
protocol: HostProtocol;
port: number;
enabled: boolean;
// Mosh-specific
moshServerPath?: string;
// EternalTerminal-specific
etPort?: number;
// Protocol-specific theme override
theme?: string;
}
export interface SftpBookmark {
id: string;
path: string;
label: string;
global?: boolean;
}
export type HostAuthMethod = 'auto' | 'password' | 'key' | 'certificate';
export type HostOperatingSystem = 'linux' | 'windows' | 'macos' | 'freebsd' | 'unknown';
export type HostOsSelection = 'auto' | HostOperatingSystem;
export interface Host {
id: string;
label: string;
hostname: string;
port?: number;
username: string;
// Optional reference to a reusable identity (username + auth) stored in Keychain.
identityId?: string;
group?: string;
tags: string[];
// Legacy compatibility value; use resolveHostOs for runtime decisions.
os: 'linux' | 'windows' | 'macos';
// Absent on old records: preserve Windows/macOS, treat old Linux defaults as auto.
osOverride?: HostOsSelection;
// Device type: 'general' for standard servers, 'network' for switches/routers/firewalls.
// Network devices use raw command execution (no shell wrapping) for AI agent compatibility.
deviceType?: 'general' | 'network';
identityFileId?: string; // Reference to SSHKey
protocol?: HostProtocol; // Default/primary protocol, including namespaced plugin protocols
pluginConnection?: PluginConnectionConfig;
// Runtime marker for in-memory-only hosts (e.g. password deep links).
// Ephemeral hosts are never persisted to the vault or session restore.
ephemeral?: boolean;
// Runtime hint for deep-link launches that target file transfer (e.g.
// JumpServer sftp payloads): auto-open the SFTP side panel on connect.
autoOpenSftpPanel?: boolean;
password?: string;
savePassword?: boolean; // Whether to save the password (default: true)
authMethod?: HostAuthMethod;
// Version 1 distinguishes the explicit per-host login choices from the
// legacy "password" default, which did not mean password-only.
authPolicyVersion?: 1;
// Prefer keyboard-interactive before the password method for MFA/PAM hosts.
requiresMfa?: boolean;
// Use the local SSH agent for login. This is separate from agentForwarding,
// which exposes the local agent to the remote host after login.
useSshAgent?: boolean;
// OpenSSH config metadata used for agent-backed authentication.
identityAgent?: string;
identitiesOnly?: boolean;
addKeysToAgent?: string;
useKeychain?: boolean;
agentForwarding?: boolean;
x11Forwarding?: boolean;
createdAt?: number; // Timestamp when host was created
startupCommand?: string;
startupCommandRunMode?: MultiLineRunMode;
/** Script id (kind=script) to run automatically after connect. */
loginScriptId?: string;
/** Ordered onConnect script IDs for this host (canonical run order). */
connectScriptIds?: string[];
/** Output regex triggers that launch scripts on terminal output. */
outputTriggers?: HostOutputTrigger[];
hostChaining?: string; // Deprecated: use hostChain instead
proxy?: string; // Deprecated: use proxyConfig instead
proxyProfileId?: string; // Reference to reusable proxy profile
proxyConfig?: ProxyConfig; // New structured proxy configuration
hostChain?: HostChainConfig; // New structured host chain configuration
envVars?: string; // Deprecated: use environmentVariables instead
environmentVariables?: EnvVar[]; // Structured environment variables
charset?: string;
moshEnabled?: boolean;
moshServerPath?: string; // Custom mosh-server path (e.g., /usr/local/bin/mosh-server)
etEnabled?: boolean;
etPort?: number; // EternalTerminal server port (default: 2022)
theme?: string;
themeOverride?: boolean; // Explicitly override the global terminal theme for this host
fontFamily?: string; // Terminal font family for this host
fontFamilyOverride?: boolean; // Explicitly override the global terminal font family for this host
fontSize?: number; // Terminal font size for this host (pt)
fontSizeOverride?: boolean; // Explicitly override the global terminal font size for this host
fontWeight?: number; // Terminal font weight for this host (100-900)
fontWeightOverride?: boolean; // Explicitly override the global terminal font weight for this host
distro?: string; // detected distro id (e.g., ubuntu, debian)
distroMode?: 'auto' | 'manual'; // whether distro icon comes from detection or manual override
manualDistro?: string; // manually selected distro id when distroMode='manual'
iconMode?: HostIconMode; // Optional host icon mode. Missing/auto preserves distro detection.
iconId?: HostIconId; // Curated icon override used when iconMode='custom'
iconColorMode?: HostIconColorMode; // Whether icon color follows the icon default or a manual override
iconColor?: HostIconColorId; // Palette color used when iconColorMode='manual'
iconColorCustom?: string; // Custom hex color used when iconColorMode='manual'
// Multi-protocol support
protocols?: ProtocolConfig[]; // Multiple protocol configurations
telnetPort?: number; // Telnet-specific port (for quick access)
telnetEnabled?: boolean; // Is Telnet enabled for this host
telnetIdentityId?: string; // Reference to a Telnet-specific reusable identity
telnetUsername?: string; // Telnet-specific username
telnetPassword?: string; // Telnet-specific password
// Serial-specific configuration (for protocol='serial' hosts)
serialConfig?: SerialConfig;
// SFTP specific configuration
sftpSudo?: boolean; // Use sudo for SFTP operations (requires password)
// Remote file browser protocol: Auto tries SFTP then falls back to SCP-mode
// (shell browse + scp -t/-f transfers) when the SFTP subsystem is unavailable.
sftpFileProtocol?: 'auto' | 'sftp' | 'scp';
sftpEncoding?: SftpFilenameEncoding; // Filename encoding for SFTP operations
sftpBookmarks?: SftpBookmark[]; // Bookmarked SFTP paths for quick navigation
sftpFollowTerminalCwd?: boolean; // Overrides global SFTP follow-terminal-directory setting
// Managed source: if this host is managed by an external file (e.g., ~/.ssh/config)
managedSourceId?: string; // Reference to ManagedSource.id
// Host-level keyword highlighting (overrides/extends global settings)
keywordHighlightRules?: KeywordHighlightRule[];
keywordHighlightEnabled?: boolean;
// Legacy SSH algorithm support for older network equipment (switches, routers)
legacyAlgorithms?: boolean;
// Drop every ecdsa-sha2-* from the offered host-key list. Some old Huawei
// VRP / Cisco IOS stacks negotiate ECDSA but produce signatures ssh2's
// strict RFC verifier rejects ("signature verification failed"). Forcing
// RSA / DSA / Ed25519 fallback restores compatibility — see #1027.
skipEcdsaHostKey?: boolean;
// Per-host SSH algorithm overrides (advanced). When a category's array is
// non-empty, it fully replaces the offered list for that category. Use
// sparingly — incorrect values make the host unreachable.
algorithms?: HostAlgorithmOverrides;
// Per-host SSH keepalive override. When `keepaliveOverride === true`, the
// host uses its own `keepaliveInterval` / `keepaliveCountMax` instead of
// inheriting the global TerminalSettings values. Lets a user keep an
// aggressive cloud-friendly keepalive globally while disabling it for a
// specific router / embedded device whose SSH stack doesn't reply to
// OpenSSH keepalive global requests (issue #581 / #939).
keepaliveInterval?: number; // Seconds; 0 = disabled
keepaliveCountMax?: number; // Unanswered keepalives before declaring dead
keepaliveOverride?: boolean;
// Per-host SSH connection timeouts. Missing values retain Netcatty defaults.
sshTcpConnectTimeoutSeconds?: number;
sshAuthReadyTimeoutSeconds?: number;
// Show local timestamps for this host beside terminal output rows.
// Kept per-host because timestamp visibility is usually a host/workflow preference.
showLineTimestamps?: boolean;
// What the Backspace key sends: undefined = xterm default (no interception), 'ctrl-h' = ^H (0x08)
backspaceBehavior?: 'ctrl-h';
// When true, tab titles stay on the connection label instead of following the
// shell-reported window title (OSC 0/2). Useful when many hosts share one
// bastion profile name.
disableDynamicTabTitle?: boolean;
// Local SSH key file paths (from SSH config IdentityFile or user-added)
// Resolved at connection time — the app reads the file content when connecting.
identityFilePaths?: string[];
// Pin host to top of All hosts view for quick access
pinned?: boolean;
// Timestamp of last successful connection, used for Recently Connected section
lastConnectedAt?: number;
// Per-session shell override for local terminals (from shell discovery)
localShell?: string;
localShellArgs?: string[];
localShellName?: string;
localShellIcon?: string;
localStartDir?: string;
/** User-authored Markdown notes (project, hardware, region, etc.) */
notes?: string;
order?: number;
}
export type KeyType = 'RSA' | 'ECDSA' | 'ED25519';
type KeySource = 'generated' | 'imported' | 'reference';
export type KeyCategory = 'key' | 'certificate' | 'identity';
type IdentityAuthMethod = 'password' | 'key' | 'certificate';
export interface SSHKey {
id: string;
label: string;
type: KeyType;
keySize?: number; // RSA: 4096/2048/1024, ECDSA: 521/384/256
privateKey: string;
publicKey?: string;
certificate?: string;
passphrase?: string; // encrypted or stored securely
savePassphrase?: boolean;
source: KeySource;
category: KeyCategory;
created: number;
filePath?: string;
order?: number;
}
// Identity combines username with authentication method
export interface Identity {
id: string;
label: string;
username: string;
authMethod: IdentityAuthMethod;
password?: string; // For password auth
keyId?: string; // Reference to SSHKey for key/certificate auth
created: number;
order?: number;
}
export type SnippetKind = 'snippet' | 'script';
export type SnippetMultiLineRunMode = MultiLineRunMode;
export type ScriptLanguage = 'javascript' | 'python';
export type ScriptTrigger = 'manual' | 'onConnect' | 'onOutput';
export interface Snippet {
id: string;
label: string;
command: string; // Multi-line script or automation script source
tags?: string[];
package?: string; // package path
targets?: string[]; // host ids
/** Group paths resolved against the latest host inventory when the snippet runs. */
targetGroups?: string[];
/** When true, script/snippet applies to every connectable host (no per-host picker). */
targetsAllHosts?: boolean;
shortkey?: string; // Keyboard shortcut to send this snippet in terminal (e.g., "F1", "Ctrl + F1")
noAutoRun?: boolean; // If true, paste command without executing (no trailing Enter)
multiLineRunMode?: SnippetMultiLineRunMode; // Multi-line auto-run behavior; default is paste.
order?: number;
/** Default 'snippet' — static text paste. 'script' runs via nct automation engine. */
kind?: SnippetKind;
language?: ScriptLanguage;
description?: string;
trigger?: ScriptTrigger;
/** Regex pattern when trigger is 'onOutput'. */
triggerPattern?: string;
}
export interface HostOutputTrigger {
id: string;
pattern: string;
scriptId: string;
}
export interface VaultNote {
id: string;
title: string;
content: string;
group?: string;
tags?: string[];
linkedHostIds?: string[];
createdAt: number;
updatedAt: number;
order?: number;
isPinned?: boolean;
}
export interface ChatMessage {
role: 'user' | 'model';
text: string;
}
export interface GroupNode {
name: string;
path: string;
children: Record<string, GroupNode>;
hosts: Host[];
/** Pre-computed total host count including all descendants. Set during tree construction. */
totalHostCount?: number;
}
/** Default configuration for a group. Hosts in this group inherit these values when not explicitly set. */
export interface GroupConfig {
path: string;
order?: number;
username?: string;
password?: string;
savePassword?: boolean;
authMethod?: HostAuthMethod;
identityId?: string;
identityFileId?: string;
identityFilePaths?: string[];
port?: number;
protocol?: 'ssh' | 'telnet';
deviceType?: 'general' | 'network';
agentForwarding?: boolean;
proxyProfileId?: string;
proxyConfig?: ProxyConfig;
hostChain?: HostChainConfig;
startupCommand?: string;
startupCommandRunMode?: MultiLineRunMode;
loginScriptId?: string;
legacyAlgorithms?: boolean;
skipEcdsaHostKey?: boolean;
algorithms?: HostAlgorithmOverrides;
environmentVariables?: EnvVar[];
charset?: string;
moshEnabled?: boolean;
moshServerPath?: string;
etEnabled?: boolean;
etPort?: number;
telnetEnabled?: boolean;
telnetPort?: number;
telnetIdentityId?: string;
telnetUsername?: string;
telnetPassword?: string;
theme?: string;
themeOverride?: boolean;
fontFamily?: string;
fontFamilyOverride?: boolean;
fontSize?: number;
fontSizeOverride?: boolean;
fontWeight?: number;
fontWeightOverride?: boolean;
backspaceBehavior?: 'ctrl-h';
}
export interface SyncConfig {
gistId: string;
githubToken: string;
gistToken?: string; // Alias for githubToken (deprecated, use githubToken)
lastSync?: number;
}

79
domain/models/history.ts Normal file
View File

@@ -0,0 +1,79 @@
// Known Hosts - discovered from system SSH known_hosts file
import type { HostIconColorId, HostIconColorMode, HostIconId, HostIconMode } from './connection';
export interface KnownHost {
id: string;
hostname: string; // The host pattern from known_hosts
port: number;
keyType: string; // ssh-rsa, ssh-ed25519, ecdsa-sha2-nistp256, etc.
publicKey: string; // The host's public key fingerprint or full key
fingerprint?: string; // SHA256 fingerprint without the SHA256: prefix
discoveredAt: number;
lastSeen?: number;
convertedToHostId?: string; // If converted to managed host
order?: number;
}
// Shell History - records real commands executed in terminal sessions
export interface ShellHistoryEntry {
id: string;
command: string;
hostId: string; // ID of the host where command was executed
hostLabel: string; // Label for display
sessionId: string;
timestamp: number;
}
// Remote Shell History - commands parsed from a remote host's own shell
// history file (~/.bash_history, ~/.zsh_history, fish_history), read on
// demand through the SSH/ET exec channel. Distinct from ShellHistoryEntry,
// which records commands typed inside Netcatty's own terminal sessions.
export type RemoteHistorySource = 'bash' | 'zsh' | 'fish';
export interface RemoteHistoryEntry {
id: string;
command: string;
source: RemoteHistorySource;
timestamp?: number; // Only set when the history file carries one (zsh EXTENDED_HISTORY, fish `when`)
}
// Connection Log - records connection history
export interface ConnectionLog {
id: string;
sessionId?: string; // Terminal session ID for matching during capture
hostId: string; // Host ID (can be empty for local terminal)
hostLabel: string; // Display label (e.g., 'Local Terminal' or host label)
hostname: string; // Target hostname or 'localhost'
username: string; // SSH username or system username
protocol: 'ssh' | 'telnet' | 'local' | 'mosh' | 'et' | 'serial';
hostOs?: 'linux' | 'windows' | 'macos'; // Snapshot of the connected host OS for log icons
hostDistro?: string; // Snapshot of the connected host distro/vendor icon id
hostIconMode?: HostIconMode; // Snapshot of the host icon mode for log icons
hostIconId?: HostIconId; // Snapshot of the built-in host icon id
hostIconColorMode?: HostIconColorMode; // Snapshot of the host icon color source
hostIconColor?: HostIconColorId; // Snapshot of the host icon color id
hostIconColorCustom?: string; // Snapshot of the custom host icon color
startTime: number; // Connection start timestamp
endTime?: number; // Connection end timestamp (undefined if still active)
localUsername: string; // System username of the local user
localHostname: string; // Local machine hostname
saved: boolean; // Whether this log is bookmarked/saved
terminalData?: string; // Captured terminal output data for replay
themeId?: string; // Terminal theme ID for this log view
fontSize?: number; // Terminal font size for this log view
}
// Session Logs Settings - for auto-saving terminal logs to local filesystem
export type SessionLogFormat = 'txt' | 'raw' | 'html';
// Managed Source - external file that manages a group of hosts (e.g., ~/.ssh/config)
type ManagedSourceType = 'ssh_config';
export interface ManagedSource {
id: string;
type: ManagedSourceType;
filePath: string;
groupName: string;
lastSyncedAt: number;
lastFileHash?: string;
}

View File

@@ -0,0 +1,290 @@
// Keyboard Shortcuts / Hotkeys
export type HotkeyScheme = 'disabled' | 'mac' | 'pc';
export interface KeyBinding {
id: string;
action: string;
label: string;
mac: string; // e.g., '⌘+1', '⌘+⌥+arrows'
pc: string; // e.g., 'Ctrl+1', 'Ctrl+Alt+arrows'
category: 'tabs' | 'terminal' | 'navigation' | 'app' | 'sftp';
}
// User's custom key bindings - only stores overrides from defaults
export type CustomKeyBindings = Record<string, { mac?: string; pc?: string }>;
// Parse a key string like "⌘ + Shift + K" or "Ctrl + Alt + T" into normalized form
export const parseKeyCombo = (keyStr: string): { modifiers: string[]; key: string } | null => {
if (!keyStr || keyStr === 'Disabled') return null;
const parts = keyStr.split('+').map(p => p.trim());
const key = parts.pop() || '';
return { modifiers: parts, key };
};
const KEY_STRING_TO_EVENT_KEY: Record<string, string> = {
Space: ' ',
'↑': 'ArrowUp',
'↓': 'ArrowDown',
'←': 'ArrowLeft',
'→': 'ArrowRight',
Esc: 'Escape',
'⌫': 'Backspace',
Del: 'Delete',
'↵': 'Enter',
'⇥': 'Tab',
};
/** Rebuild a keydown-like event from a stored shortcut string for matching. */
export const keyStringToKeyboardEvent = (keyString: string): KeyboardEvent | null => {
const parsed = parseKeyCombo(keyString);
if (!parsed) return null;
const modifiers = new Set(parsed.modifiers);
const mappedKey = KEY_STRING_TO_EVENT_KEY[parsed.key];
const key = mappedKey ?? (parsed.key.length === 1 ? parsed.key.toLowerCase() : parsed.key);
return {
key,
code: '',
metaKey: modifiers.has('⌘') || modifiers.has('Win'),
ctrlKey: modifiers.has('⌃') || modifiers.has('Ctrl'),
altKey: modifiers.has('⌥') || modifiers.has('Alt'),
shiftKey: modifiers.has('Shift'),
} as KeyboardEvent;
};
const PHYSICAL_SHORTCUT_KEY_NAMES: Record<string, string> = {
Backquote: '`',
Minus: '-',
Equal: '=',
BracketLeft: '[',
BracketRight: ']',
Backslash: '\\',
Semicolon: ';',
Quote: "'",
Comma: ',',
Period: '.',
Slash: '/',
};
const physicalShortcutKeyName = (e: Pick<KeyboardEvent, 'code'>): string | null => {
const code = e.code;
if (/^Key[A-Z]$/.test(code)) return code.slice(3);
if (/^Digit[0-9]$/.test(code)) return code.slice(5);
return PHYSICAL_SHORTCUT_KEY_NAMES[code] ?? null;
};
/**
* Resolve the 1-9 tab shortcut digit from a key event.
* Prefer the physical Digit code so Shift+[1...9] still works when e.key is "!" etc.
*/
export const tabShortcutDigitFromEvent = (
e: Pick<KeyboardEvent, 'key' | 'code'>,
): number | null => {
const key = physicalShortcutKeyName(e) ?? e.key;
if (!/^[1-9]$/.test(key)) return null;
return Number.parseInt(key, 10);
};
const LATIN_SHORTCUT_KEY_PATTERN = /^\p{Script=Latin}$/u;
const ASCII_SHORTCUT_KEY_PATTERN = /^[A-Za-z]$/;
const PRINTABLE_NON_LETTER_SHORTCUT_KEY_PATTERN = /^[^\p{Letter}\p{Number}\s]$/u;
const shortcutEventKey = (e: KeyboardEvent): string => {
const physicalKey = physicalShortcutKeyName(e);
if (
LATIN_SHORTCUT_KEY_PATTERN.test(e.key) ||
PRINTABLE_NON_LETTER_SHORTCUT_KEY_PATTERN.test(e.key)
) {
return e.key;
}
return physicalKey ?? e.key;
};
// Convert keyboard event to a key string
export const keyEventToString = (e: KeyboardEvent, isMac: boolean): string => {
const parts: string[] = [];
if (isMac) {
if (e.metaKey) parts.push('⌘');
if (e.ctrlKey) parts.push('⌃');
if (e.altKey) parts.push('⌥');
if (e.shiftKey) parts.push('Shift');
} else {
if (e.ctrlKey) parts.push('Ctrl');
if (e.altKey) parts.push('Alt');
if (e.shiftKey) parts.push('Shift');
if (e.metaKey) parts.push('Win');
}
// Get the key name
let keyName = shortcutEventKey(e);
// Normalize special keys
if (keyName === ' ') keyName = 'Space';
else if (keyName === 'ArrowUp') keyName = '↑';
else if (keyName === 'ArrowDown') keyName = '↓';
else if (keyName === 'ArrowLeft') keyName = '←';
else if (keyName === 'ArrowRight') keyName = '→';
else if (keyName === 'Escape') keyName = 'Esc';
else if (keyName === 'Backspace') keyName = '⌫';
else if (keyName === 'Delete') keyName = 'Del';
else if (keyName === 'Enter') keyName = '↵';
else if (keyName === 'Tab') keyName = '⇥';
else if (ASCII_SHORTCUT_KEY_PATTERN.test(keyName)) keyName = keyName.toUpperCase();
// Don't include modifier keys themselves
if (['Meta', 'Control', 'Alt', 'Shift'].includes(e.key)) {
return parts.join(' + ');
}
parts.push(keyName);
return parts.join(' + ');
};
// Check if a keyboard event matches a key binding string
export const matchesKeyBinding = (e: KeyboardEvent, keyStr: string, isMac: boolean): boolean => {
if (!keyStr || keyStr === 'Disabled') return false;
// Handle range patterns like "[1...9]"
if (keyStr.includes('[1...9]')) {
const basePattern = keyStr.replace('[1...9]', '');
const digit = tabShortcutDigitFromEvent(e);
if (digit === null) return false;
const key = String(digit);
// Check modifiers match the base pattern
const testStr = basePattern + key;
const physicalDigitEvent = {
key,
code: e.code,
metaKey: e.metaKey,
ctrlKey: e.ctrlKey,
altKey: e.altKey,
shiftKey: e.shiftKey,
} as KeyboardEvent;
return matchesKeyBinding(physicalDigitEvent, testStr.trim(), isMac);
}
// Handle arrow key patterns like "arrows"
if (keyStr.includes('arrows')) {
const basePattern = keyStr.replace('arrows', '');
const key = e.key;
// Check if it's an arrow key
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(key)) return false;
// Map arrow key to symbol for matching
const arrowSymbol = key === 'ArrowUp' ? '↑'
: key === 'ArrowDown' ? '↓'
: key === 'ArrowLeft' ? '←'
: '→';
// Check modifiers match the base pattern
const testStr = basePattern + arrowSymbol;
return matchesKeyBinding(e, testStr.trim(), isMac);
}
const parsed = parseKeyCombo(keyStr);
if (!parsed) return false;
const { modifiers, key } = parsed;
const hasMacModifiers = modifiers.some((modifier) => ['⌘', '⌃', '⌥'].includes(modifier));
const hasPcModifiers = modifiers.some((modifier) => ['Ctrl', 'Alt', 'Win'].includes(modifier));
if ((!isMac && hasMacModifiers) || (isMac && hasPcModifiers)) {
return false;
}
// Check modifiers
if (isMac) {
const needMeta = modifiers.includes('⌘');
const needCtrl = modifiers.includes('⌃');
const needAlt = modifiers.includes('⌥');
const needShift = modifiers.includes('Shift');
if (e.metaKey !== needMeta) return false;
if (e.ctrlKey !== needCtrl) return false;
if (e.altKey !== needAlt) return false;
if (e.shiftKey !== needShift) return false;
} else {
const needCtrl = modifiers.includes('Ctrl');
const needAlt = modifiers.includes('Alt');
const needShift = modifiers.includes('Shift');
const needMeta = modifiers.includes('Win');
if (e.ctrlKey !== needCtrl) return false;
if (e.altKey !== needAlt) return false;
if (e.shiftKey !== needShift) return false;
if (e.metaKey !== needMeta) return false;
}
const normalizeKey = (rawKey: string): string => {
let normalizedKey = rawKey;
if (normalizedKey === ' ') normalizedKey = 'Space';
else if (normalizedKey === 'ArrowUp') normalizedKey = '↑';
else if (normalizedKey === 'ArrowDown') normalizedKey = '↓';
else if (normalizedKey === 'ArrowLeft') normalizedKey = '←';
else if (normalizedKey === 'ArrowRight') normalizedKey = '→';
else if (normalizedKey === 'Escape') normalizedKey = 'Esc';
else if (normalizedKey === 'Backspace') normalizedKey = '⌫';
else if (normalizedKey === 'Delete') normalizedKey = 'Del';
else if (normalizedKey === '[') normalizedKey = '[';
else if (normalizedKey === ']') normalizedKey = ']';
else if (normalizedKey === 'Del') normalizedKey = 'Del';
return normalizedKey;
};
const eventKey = normalizeKey(shortcutEventKey(e));
const parsedKey = normalizeKey(key);
return eventKey.toLowerCase() === parsedKey.toLowerCase();
};
export const DEFAULT_KEY_BINDINGS: KeyBinding[] = [
// Tab Management
{ id: 'switch-tab-1-9', action: 'switchToTab', label: 'Switch to Tab [1...9]', mac: '⌘ + [1...9]', pc: 'Ctrl + [1...9]', category: 'tabs' },
{ id: 'next-tab', action: 'nextTab', label: 'Next Tab', mac: '⌘ + Shift + ]', pc: 'Ctrl + Tab', category: 'tabs' },
{ id: 'prev-tab', action: 'prevTab', label: 'Previous Tab', mac: '⌘ + Shift + [', pc: 'Ctrl + Shift + Tab', category: 'tabs' },
{ id: 'close-tab', action: 'closeTab', label: 'Close Tab', mac: '⌘ + W', pc: 'Ctrl + W', category: 'tabs' },
{ id: 'close-session', action: 'closeSession', label: 'Close Session Pane', mac: '⌘ + Shift + W', pc: 'Ctrl + Shift + W', category: 'tabs' },
{ id: 'new-tab', action: 'newTab', label: 'New Local Tab', mac: '⌘ + T', pc: 'Ctrl + T', category: 'tabs' },
// Terminal Operations
{ id: 'copy', action: 'copy', label: 'Copy from Terminal', mac: '⌘ + C', pc: 'Ctrl + Shift + C', category: 'terminal' },
{ id: 'paste', action: 'paste', label: 'Paste to Terminal', mac: '⌘ + V', pc: 'Ctrl + Shift + V', category: 'terminal' },
{ id: 'paste-selection', action: 'pasteSelection', label: 'Paste Selection to Terminal', mac: '⌘ + Shift + X', pc: 'Ctrl + Shift + X', category: 'terminal' },
{ id: 'select-all', action: 'selectAll', label: 'Select All in Terminal', mac: '⌘ + A', pc: 'Ctrl + Shift + A', category: 'terminal' },
{ id: 'clear-buffer', action: 'clearBuffer', label: 'Clear Terminal Buffer', mac: '⌘ + ⌃ + K', pc: 'Ctrl + Shift + K', category: 'terminal' },
{ id: 'search-terminal', action: 'searchTerminal', label: 'Open Terminal Search', mac: '⌘ + F', pc: 'Ctrl + F', category: 'terminal' },
{ id: 'increase-terminal-font-size', action: 'increaseTerminalFontSize', label: 'Increase Terminal Font Size', mac: '⌘ + =', pc: 'Ctrl + =', category: 'terminal' },
{ id: 'decrease-terminal-font-size', action: 'decreaseTerminalFontSize', label: 'Decrease Terminal Font Size', mac: '⌘ + -', pc: 'Ctrl + -', category: 'terminal' },
{ id: 'reset-terminal-font-size', action: 'resetTerminalFontSize', label: 'Reset Terminal Font Size', mac: '⌘ + 0', pc: 'Ctrl + 0', category: 'terminal' },
// Navigation / Split View
{ id: 'move-focus', action: 'moveFocus', label: 'Move focus between Split View panes', mac: '⌘ + ⌥ + arrows', pc: 'Ctrl + Alt + arrows', category: 'navigation' },
{ id: 'split-horizontal', action: 'splitHorizontal', label: 'Split Horizontal', mac: '⌘ + D', pc: 'Ctrl + Shift + D', category: 'navigation' },
{ id: 'split-vertical', action: 'splitVertical', label: 'Split Vertical', mac: '⌘ + Shift + D', pc: 'Ctrl + Shift + E', category: 'navigation' },
{ id: 'toggle-pane-zoom', action: 'togglePaneZoom', label: 'Toggle Pane Zoom', mac: '⌥ + M', pc: 'Alt + M', category: 'navigation' },
// App Features
{ id: 'open-hosts', action: 'openHosts', label: 'Open Hosts Page', mac: 'Disabled', pc: 'Disabled', category: 'app' },
{ id: 'open-local', action: 'openLocal', label: 'Open Local Terminal', mac: '⌘ + L', pc: 'Ctrl + L', category: 'app' },
{ id: 'open-sftp', action: 'openSftp', label: 'Open SFTP', mac: '⌘ + Shift + O', pc: 'Ctrl + Shift + O', category: 'app' },
{ id: 'port-forwarding', action: 'portForwarding', label: 'Open Port Forwarding', mac: '⌘ + P', pc: 'Ctrl + P', category: 'app' },
{ id: 'command-palette', action: 'commandPalette', label: 'Open Command Palette', mac: '⌘ + K', pc: 'Ctrl + K', category: 'app' },
{ id: 'quick-switch', action: 'quickSwitch', label: 'Quick Switch', mac: '⌘ + J', pc: 'Ctrl + J', category: 'app' },
{ id: 'new-workspace', action: 'newWorkspace', label: 'New Workspace', mac: '⌘ + Shift + J', pc: 'Ctrl + Shift + J', category: 'app' },
{ id: 'snippets', action: 'snippets', label: 'Open Snippets', mac: '⌘ + Shift + S', pc: 'Ctrl + Shift + S', category: 'app' },
{ id: 'broadcast', action: 'broadcast', label: 'Switch the Broadcast Mode', mac: '⌘ + B', pc: 'Ctrl + B', category: 'app' },
{ id: 'toggle-side-panel', action: 'toggleSidePanel', label: 'Toggle Side Panel', mac: '⌘ + \\', pc: 'Ctrl + \\', category: 'app' },
{ id: 'open-settings', action: 'openSettings', label: 'Open Settings', mac: '⌘ + ,', pc: 'Ctrl + ,', category: 'app' },
// SFTP Operations
{ id: 'sftp-copy', action: 'sftpCopy', label: 'Copy Files', mac: '⌘ + C', pc: 'Ctrl + C', category: 'sftp' },
{ id: 'sftp-cut', action: 'sftpCut', label: 'Cut Files', mac: '⌘ + X', pc: 'Ctrl + X', category: 'sftp' },
{ id: 'sftp-paste', action: 'sftpPaste', label: 'Paste Files', mac: '⌘ + V', pc: 'Ctrl + V', category: 'sftp' },
{ id: 'sftp-select-all', action: 'sftpSelectAll', label: 'Select All Files', mac: '⌘ + A', pc: 'Ctrl + A', category: 'sftp' },
{ id: 'sftp-rename', action: 'sftpRename', label: 'Rename File', mac: 'F2', pc: 'F2', category: 'sftp' },
{ id: 'sftp-delete', action: 'sftpDelete', label: 'Delete Files', mac: '⌘ + ⌫', pc: 'Delete', category: 'sftp' },
{ id: 'sftp-refresh', action: 'sftpRefresh', label: 'Refresh', mac: '⌘ + R', pc: 'F5', category: 'sftp' },
{ id: 'sftp-new-folder', action: 'sftpNewFolder', label: 'New Folder', mac: '⌘ + Shift + N', pc: 'Ctrl + Shift + N', category: 'sftp' },
{ id: 'sftp-open', action: 'sftpOpen', label: 'Open File / Enter Directory', mac: 'Enter', pc: 'Enter', category: 'sftp' },
{ id: 'sftp-go-parent', action: 'sftpGoParent', label: 'Go to Parent Directory', mac: '⌫', pc: 'Backspace', category: 'sftp' },
{ id: 'sftp-navigate-to', action: 'sftpNavigateTo', label: 'Navigate to Selected Directory', mac: '⌘ + Enter', pc: 'Ctrl + Enter', category: 'sftp' },
];

View File

@@ -0,0 +1,39 @@
// Port Forwarding Types
export type PortForwardingType = 'local' | 'remote' | 'dynamic';
/**
* Display / projection status for a port-forwarding rule.
* `unknown` means the authoritative main-process snapshot could not be read;
* it must never be persisted to localStorage.
*/
export type PortForwardingStatus =
| 'inactive'
| 'connecting'
| 'active'
| 'error'
| 'unknown';
export interface PortForwardingRule {
id: string;
label: string;
order?: number;
type: PortForwardingType;
// Common fields
localPort: number;
bindAddress: string; // e.g., '127.0.0.1', '0.0.0.0'
// For local and remote forwarding
remoteHost?: string;
remotePort?: number;
// Host to tunnel through
hostId?: string;
// Auto-start: if true, this rule will automatically start when the app launches
autoStart?: boolean;
/**
* Runtime projection for the UI. Authoritative phase lives in the Electron
* main-process registry; this field is rebuilt from snapshots / events and
* must not be treated as durable configuration.
*/
status: PortForwardingStatus;
error?: string;
createdAt: number;
lastUsedAt?: number;
}

137
domain/models/sftp.ts Normal file
View File

@@ -0,0 +1,137 @@
// SFTP Types
export type SftpFilenameEncoding = 'auto' | 'utf-8' | 'gb18030';
export interface SftpFileEntry {
name: string;
type: 'file' | 'directory' | 'symlink';
size: number;
sizeFormatted: string;
lastModified: number;
lastModifiedFormatted: string;
permissions?: string;
owner?: string;
group?: string;
linkTarget?: 'file' | 'directory' | null; // For symlinks: the type of the target, or null if broken
hidden?: boolean; // Windows hidden attribute (only set for local Windows filesystem)
}
export interface SftpConnection {
id: string;
hostId: string;
hostLabel: string;
isLocal: boolean;
status: 'connecting' | 'connected' | 'disconnected' | 'error';
error?: string;
currentPath: string;
homeDir?: string;
/** True when this SFTP connection reuses an existing terminal SSH session */
reusedConnection?: boolean;
/** Terminal session whose confirmed SSH transport backs this connection. */
sourceSessionId?: string;
fileProtocol?: 'auto' | 'sftp' | 'scp';
}
export type TransferStatus =
| 'pending'
| 'queued'
| 'transferring'
| 'pausing'
| 'paused'
| 'attention'
| 'interrupted'
| 'completed'
| 'failed'
| 'cancelled';
export type TransferDirection = 'upload' | 'download' | 'remote-to-remote' | 'local-copy';
export type TransferOrigin = 'manual' | 'drag-drop' | 'editor-sync' | 'agent' | 'internal';
export type TransferPhase = 'scanning' | 'compressing' | 'uploading' | 'transferring' | 'extracting' | 'verifying';
export type TransferControlKind = 'stream' | 'compressed-upload';
export interface DirectoryResumeCheckpoint {
/** Version 1 used a full SHA-256 digest for every appended entry. Version 2
* keeps the SHA-256 compression state so adding another fixed-width identity
* is still cryptographically chained without re-hashing string wrappers. */
version: 1 | 2;
/** Traversal prefix whose source/target metadata is covered by manifestHash. */
coveredEntries: number;
/** Covered entries already completed and compacted out of the task array. */
completedEntries: number;
/** Fixed-size chained SHA-256 value of the covered traversal prefix. */
manifestHash: string;
}
export interface TransferTask {
id: string;
batchId?: string;
fileName: string;
originalFileName?: string;
sourcePath: string;
targetPath: string;
sourceConnectionId: string;
targetConnectionId: string;
targetHostId?: string;
/** Full endpoint key (hostId:hostname:port:protocol) for distinguishing
* same-hostId uploads with different session-time overrides. */
targetConnectionKey?: string;
direction: TransferDirection;
status: TransferStatus;
totalBytes: number;
transferredBytes: number;
speed: number; // bytes per second
error?: string;
startTime: number;
endTime?: number;
isDirectory: boolean;
progressMode?: 'bytes' | 'files';
childTasks?: string[]; // For directory transfers
parentTaskId?: string;
sourceLastModified?: number; // Cached from file list to avoid redundant stat
skipConflictCheck?: boolean; // Skip conflict check for replace operations
replaceExistingTarget?: boolean; // Delete the existing target before transferring
retryable?: boolean; // False for task types that cannot be safely replayed through generic retry
ownerId?: string;
sourceHostId?: string;
sourceHostLabel?: string;
targetHostLabel?: string;
origin?: TransferOrigin;
background?: boolean;
phase?: TransferPhase;
/** Selects the background job API used by the global transfer center. */
controlKind?: TransferControlKind;
/** Monotonic backend lifecycle version. Newer pause/resume truth wins over stale progress or panel snapshots. */
lifecycleEpoch?: number;
resumable?: boolean;
checkpointBytes?: number;
resumeStage?: 'direct' | 'download' | 'upload';
downloadCheckpointBytes?: number;
uploadCheckpointBytes?: number;
priority?: number;
updatedAt?: number;
pauseUnavailableReason?: string;
conflict?: FileConflict;
stagedTargetPath?: string;
sourceFingerprint?: string;
reconnectRequired?: boolean;
/** Stable position and identity used to compact completed directory children. */
directoryEntryIndex?: number;
directoryEntryIdentity?: string;
/** Fixed-size resume record stored only on a top-level directory task. */
directoryResumeCheckpoint?: DirectoryResumeCheckpoint;
}
export type FileConflictAction = 'stop' | 'skip' | 'replace' | 'duplicate' | 'merge';
export interface FileConflict {
transferId: string;
batchId?: string;
fileName: string;
sourcePath: string;
targetPath: string;
isDirectory: boolean;
existingType?: 'file' | 'directory' | 'symlink';
applyToAllCount?: number;
existingSize: number;
newSize: number;
existingModified: number;
newModified: number;
}

675
domain/models/terminal.ts Normal file
View File

@@ -0,0 +1,675 @@
import type { HostProtocol, PluginConnectionConfig, SerialConfig, Snippet } from './connection';
import type { CodingCliProviderId } from '../codingCliProviders';
import {
normalizeHibernateHiddenTabsDelaySec,
normalizeHibernateKeepRendererCount,
normalizeHibernateReplayChunkBytes,
} from '../terminalHibernate';
import {
normalizeInlineImageMaxMegapixels,
normalizeInlineImageSequenceLimitMb,
normalizeInlineImageStorageLimitMb,
TERMINAL_INLINE_IMAGE_MAX_MEGAPIXELS_DEFAULT,
TERMINAL_INLINE_IMAGE_SEQUENCE_LIMIT_MB_DEFAULT,
TERMINAL_INLINE_IMAGE_STORAGE_LIMIT_MB_DEFAULT,
} from '../terminalInlineImages';
// Terminal appearance settings
export type CursorShape = 'block' | 'bar' | 'underline';
export type TerminalMouseClickBehavior = 'context-menu' | 'paste' | 'select-word';
export type RightClickBehavior = TerminalMouseClickBehavior;
export type MiddleClickBehavior = 'context-menu' | 'paste' | 'disabled';
export type LinkModifier = 'none' | 'ctrl' | 'alt' | 'meta';
export type TerminalEmulationType = 'xterm-256color' | 'xterm-16color' | 'xterm';
export type DynamicTabTitleMode = 'off' | 'agent' | 'all';
/**
* What the terminal host info bar shows as its primary title (#2708).
* - address: user@host:port when available (historical default)
* - label: vault host label / display name
*/
export type HostInfoBarTitleMode = 'address' | 'label';
/**
* How to assist when a sudo/su password prompt appears (#2156).
* - off: no assist
* - hint: ghost "press Enter" fill of the host session password
* - picker: WindTerm-like list of host + keychain password identities
*/
export type PasswordPromptAssistMode = 'off' | 'hint' | 'picker';
/**
* Which command-history pool autocomplete suggestions draw from (#2595).
* - host: only the current host's recorded commands (default; avoids cross-device noise)
* - global: commands recorded across all hosts
*/
export type AutocompleteHistoryScope = 'host' | 'global';
/**
* When remote programs emit OSC 9 / 777 / 99 desktop-notification sequences.
* - off: ignore
* - unfocused: notify only when this session is not the focused pane or the window is in the background
* - always: honor every notification (default; matches iTerm2 / Ghostty / Codex osc9)
*/
export type OscNotificationMode = 'off' | 'unfocused' | 'always';
/** How an established terminal session reports a later disconnect. */
export type DisconnectedNoticeMode = 'terminal' | 'dialog';
export const DEFAULT_TERMINAL_WORD_SEPARATORS = ' ()[]{}\'"';
// Keyword highlighting configuration
export interface KeywordHighlightRule {
id: string;
label: string; // Display name (e.g., "Error", "Warning", "OK")
patterns: string[]; // Regex patterns to match
color: string; // Highlight color (hex)
enabled: boolean;
// Set to true when the user edits a built-in rule's label/patterns so
// normalize keeps the user-edited values instead of overwriting them with
// the latest shipped defaults. Absent / false means "still tracking defaults"
// and the rule picks up new built-in patterns added in later versions.
customized?: boolean;
}
export interface TerminalSettings {
// Rendering
scrollback: number; // Number of lines kept in buffer
drawBoldInBrightColors: boolean; // Draw bold text in bright colors
terminalEmulationType: TerminalEmulationType; // Terminal emulation type (TERM env var)
startupCommandDelayMs: number; // Delay (ms) after connect before sending the startup command; also used between multiple lines
// Font
fontLigatures: boolean; // Enable font ligatures
fontSmoothing: boolean; // Use native macOS/WebKit font anti-aliasing
fontWeight: number; // Normal font weight (100-900)
fontWeightBold: number; // Bold font weight (100-900)
linePadding: number; // Additional space between lines
fallbackFont: string; // Fallback font family
// Cursor
cursorShape: CursorShape;
cursorBlink: boolean;
/** Highlight the buffer row under the cursor (WindTerm-style decoration). */
highlightCursorLine: boolean;
// Accessibility
minimumContrastRatio: number; // Minimum contrast ratio (1-21)
// Keyboard
altAsMeta: boolean; // Use ⌥ as the Meta key
optionArrowWordJump: boolean; // macOS: Option+←/→ send Meta-b/f for word jump
shiftEnterNewlineEnabled: boolean; // Send configured text on Shift+Enter
shiftEnterNewlineText: string; // Backslash-escaped text sent by Shift+Enter
kittyKeyboardProtocolEnabled: boolean; // Enable Kitty keyboard protocol support
scrollOnInput: boolean; // Scroll terminal to bottom on input
scrollOnOutput: boolean; // Scroll terminal to bottom on output
scrollOnKeyPress: boolean; // Scroll terminal to bottom on key press
scrollOnPaste: boolean; // Scroll terminal to bottom on paste
smoothScrolling: boolean; // Animate viewport scrolling instead of jumping instantly
// Mouse
rightClickBehavior: RightClickBehavior;
// Show the app context menu even when a fullscreen app (tmux/vim) holds mouse tracking
showContextMenuOverFullscreenApps: boolean;
middleClickBehavior: MiddleClickBehavior;
copyOnSelect: boolean; // Automatically copy selected text
/**
* When true, terminal copy paths strip display-padding spaces and join
* soft-wrapped rows before writing the clipboard. When false, use raw
* xterm getSelection() (screen-cell layout as-is).
*/
normalizeTextOnCopy: boolean;
middleClickPaste: boolean; // Legacy mirror for older settings payloads
wordSeparators: string; // Characters for word selection
linkModifier: LinkModifier; // Modifier key to click links
autoCloseOnExit: boolean; // Automatically close terminal UI after eligible session exits
disconnectedNoticeMode: DisconnectedNoticeMode; // Non-blocking terminal line or legacy dialog after disconnect
// Keyword Highlighting
keywordHighlightEnabled: boolean;
keywordHighlightRules: KeywordHighlightRule[];
// Local Shell Configuration
localShell: string; // Path to shell executable (empty = system default)
localShellArgs: string[]; // Launch args for a custom local shell (e.g. ["--login", "-i"] for msys2 bash); ignored for discovered shells
localStartDir: string; // Starting directory for local terminal (empty = home directory)
// SSH Connection
verifyHostKeys: boolean; // Verify SSH host keys before authenticating
keepaliveInterval: number; // Seconds between SSH-level keepalive packets (0 = disabled)
keepaliveCountMax: number; // Unanswered keepalives before declaring the connection dead
sshAutoReconnectEnabled: boolean; // Automatically reconnect SSH sessions after unexpected disconnects
x11Display: string; // Optional local X11 DISPLAY override (empty = use system DISPLAY/default)
// Mosh Connection
// Legacy override retained for old settings payloads and internal callers.
// The normal UI path uses Netcatty's bundled mosh-client.
moshClientPath: string;
// Server Stats Display (Linux only)
showHostInfoBar: boolean; // Show host identity and server stats above the terminal
/** Primary title in the host info bar: connection address or vault label. */
hostInfoBarTitleMode: HostInfoBarTitleMode;
showServerStats: boolean; // Show CPU/Memory/Disk in terminal statusbar
serverStatsRefreshInterval: number; // Seconds between stats refresh (default: 30)
// System Manager side panel polling (seconds)
systemManagerProcessRefreshInterval: number;
systemManagerTmuxRefreshInterval: number;
systemManagerDockerListRefreshInterval: number;
systemManagerDockerStatsRefreshInterval: number;
// Paste
disableBracketedPaste: boolean; // Disable bracketed paste mode (avoid ^[[200~ artifacts)
// When true, pasting while the clipboard holds an image automatically runs
// the "Upload clipboard image" action (SFTP upload + remote-path paste)
// instead of falling back to a text paste. Remote SSH sessions only.
autoUploadClipboardImageOnPaste: boolean;
// Shell `clear` command behavior — controls whether CSI 3 J (erase scrollback)
// from the shell is honored. Default true matches POSIX/ncurses since 2013:
// `clear` clears both visible screen and scrollback. Disable to keep history
// across `clear` (matches iTerm2 default and pre-2013 behavior).
clearWipesScrollback: boolean;
// When true, typing on the keyboard does NOT clear an existing mouse
// selection. Lets the user select text, type a command prefix (e.g. `sz `),
// and then paste the still-live selection. xterm.js's default is to clear
// on input; this opt-in toggle restores the selection right after.
preserveSelectionOnInput: boolean;
// When the final visible output line from a command is not terminated by a
// newline, move a recognized shell prompt to the next visual line. This is
// display-only; raw session logs keep the original byte stream.
forcePromptNewLine: boolean;
// Clipboard
osc52Clipboard: 'off' | 'write-only' | 'read-write' | 'prompt'; // OSC-52 clipboard access: off, write-only (default), read-write, or prompt on read
// Desktop notifications from OSC 9 / OSC 777 notify / OSC 99
oscNotifications: OscNotificationMode;
// Tab titles
dynamicTabTitleMode: DynamicTabTitleMode; // off, agent-only, or all shell-reported titles
// Rendering
rendererType: 'auto' | 'webgl' | 'dom'; // Terminal renderer: auto (detect based on hardware), webgl, or dom
/** Dispose xterm for hidden tabs after a delay to save renderer memory; SSH stays connected. */
hibernateHiddenTabs: boolean;
/** Seconds after a tab leaves view before hibernating (see hibernateHiddenTabs). */
hibernateHiddenTabsDelaySec: number;
/** Skip full hibernate while a full-screen TUI owns the alternate screen buffer. */
hibernateSkipAltScreen: boolean;
/** Hidden tabs whose renderer is kept alive (WebGL suspended) before full hibernate. */
hibernateKeepRendererCount: number;
/** Bytes per animation frame when replaying hibernate snapshots in the renderer. */
hibernateReplayChunkBytes: number;
/** Prefer WASM terminal serialize when available (falls back to JS). */
hibernatePreferWasmSerialize: boolean;
/** Render inline raster images (Kitty graphics / SIXEL / iTerm IIP) emitted by remote programs. */
inlineImagesEnabled: boolean;
/** Kitty graphics protocol (APC G) support; requires inlineImagesEnabled. */
inlineImageKittyEnabled: boolean;
/** SIXEL (DCS q) support; requires inlineImagesEnabled. */
inlineImageSixelEnabled: boolean;
/** iTerm inline image protocol (OSC 1337 File=) support; requires inlineImagesEnabled. */
inlineImageIipEnabled: boolean;
/** Per-terminal inline image cache size in MB (FIFO eviction). */
inlineImageStorageLimitMb: number;
/** Largest single decoded inline image, in megapixels. */
inlineImageMaxMegapixels: number;
/** Largest single inline image escape sequence, in MB, before decoding. */
inlineImageSequenceLimitMb: number;
showLineTimestamps: boolean; // Show output timestamps in a side gutter
// Autocomplete
autocompleteEnabled: boolean; // Enable terminal command autocomplete
autocompleteGhostText: boolean; // Show inline ghost text suggestions (like fish shell)
autocompletePopupMenu: boolean; // Show popup menu with multiple suggestions
autocompleteDebounceMs: number; // Debounce delay for fetching suggestions (ms)
autocompleteMinChars: number; // Minimum characters before showing suggestions
autocompleteMaxSuggestions: number; // Maximum suggestions in popup menu
/** Scope for history-backed autocomplete suggestions (host vs all hosts). */
autocompleteHistoryScope: AutocompleteHistoryScope;
/**
* Assist for sudo/su password prompts: off, quick Enter-to-paste (hint),
* or multi-credential picker. Default hint preserves historical sudo UX.
*/
passwordPromptAssist: PasswordPromptAssistMode;
}
const STRICT_IPV4_OCTET_PATTERN = '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)';
const URL_HIGHLIGHT_PATTERN =
"(?:\\bhttps?:\\/\\/\\[[0-9A-Fa-f:.]+\\](?::\\d+)?(?:[/?#][^\\s<>\"'`]*)?(?<![.,;:!?\\)}])|\\b(?:https?:\\/\\/|www\\.)[^\\s<>\"'`]+(?<![.,;:!?\\])}]))";
const IPV4_HIGHLIGHT_PATTERN =
`(?<![\\w.])(?<!\\bver\\s)(?<!\\bversion\\s)(?:${STRICT_IPV4_OCTET_PATTERN}\\.){3}${STRICT_IPV4_OCTET_PATTERN}(?![\\w.])`;
// Covers full and compressed forms (1:2:3:4:5:6:7:8, fe80::1, ::1, 2001:db8::,
// etc.). Bracketed `[…]:port` URLs are matched by URL_HIGHLIGHT_PATTERN.
// Zone IDs (%eth0) and IPv4-mapped (::ffff:192.0.2.1) are intentionally out
// of scope here — add them as custom patterns if you need them.
const IPV6_HIGHLIGHT_PATTERN =
'(?<![\\w:.])' +
'(?:' +
'(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}' +
'|(?:[0-9A-Fa-f]{1,4}:){1,7}:' +
'|(?:[0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}' +
'|(?:[0-9A-Fa-f]{1,4}:){1,5}(?::[0-9A-Fa-f]{1,4}){1,2}' +
'|(?:[0-9A-Fa-f]{1,4}:){1,4}(?::[0-9A-Fa-f]{1,4}){1,3}' +
'|(?:[0-9A-Fa-f]{1,4}:){1,3}(?::[0-9A-Fa-f]{1,4}){1,4}' +
'|(?:[0-9A-Fa-f]{1,4}:){1,2}(?::[0-9A-Fa-f]{1,4}){1,5}' +
'|[0-9A-Fa-f]{1,4}:(?::[0-9A-Fa-f]{1,4}){1,6}' +
'|::(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}' +
')' +
'(?![\\w:.])';
const MAC_ADDRESS_HIGHLIGHT_PATTERN =
'\\b([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\\b';
export const DEFAULT_KEYWORD_HIGHLIGHT_RULES: KeywordHighlightRule[] = [
{ id: 'error', label: 'Error', patterns: ['\\[error\\]', '\\[err\\]', '\\berror\\b', '\\bfail(ed)?\\b', '\\bfatal\\b', '\\bcritical\\b', '\\bexception\\b'], color: '#F87171', enabled: true },
{ id: 'warning', label: 'Warning', patterns: ['\\[warn(ing)?\\]', '\\bwarn(ing)?\\b', '\\bcaution\\b', '\\bdeprecated\\b'], color: '#FBBF24', enabled: true },
{ id: 'ok', label: 'OK', patterns: ['\\[ok\\]', '\\bok\\b', '\\bsuccess(ful)?\\b', '\\bpassed\\b', '\\bcompleted\\b', '\\bdone\\b'], color: '#34D399', enabled: true },
{ id: 'info', label: 'Info', patterns: ['\\[info\\]', '\\[notice\\]', '\\[note\\]', '\\bnotice\\b', '\\bnote\\b'], color: '#3B82F6', enabled: true },
{ id: 'debug', label: 'Debug', patterns: ['\\[debug\\]', '\\[trace\\]', '\\[verbose\\]', '\\bdebug\\b', '\\btrace\\b', '\\bverbose\\b'], color: '#A78BFA', enabled: true },
{ id: 'ip-mac', label: 'URL, IP & MAC', patterns: [URL_HIGHLIGHT_PATTERN, IPV4_HIGHLIGHT_PATTERN, IPV6_HIGHLIGHT_PATTERN, MAC_ADDRESS_HIGHLIGHT_PATTERN], color: '#EC4899', enabled: true },
];
const cloneKeywordHighlightRule = (rule: KeywordHighlightRule): KeywordHighlightRule => ({
...rule,
patterns: [...rule.patterns],
});
const normalizeKeywordHighlightRules = (
rules?: KeywordHighlightRule[],
): KeywordHighlightRule[] => {
if (!rules || rules.length === 0) {
return DEFAULT_KEYWORD_HIGHLIGHT_RULES.map(cloneKeywordHighlightRule);
}
const defaultRulesById = new Map(
DEFAULT_KEYWORD_HIGHLIGHT_RULES.map((rule) => [rule.id, rule] as const),
);
const normalizedRules = rules.map((rule) => {
const defaultRule = defaultRulesById.get(rule.id);
if (!defaultRule) {
return cloneKeywordHighlightRule(rule);
}
// A built-in rule the user has explicitly edited keeps its label/patterns;
// otherwise we re-sync with the latest defaults so newly shipped patterns
// (e.g. the IPv6 entry in `ip-mac`) propagate to existing users without
// a manual reset.
if (rule.customized) {
return {
...defaultRule,
label: rule.label,
patterns: [...rule.patterns],
color: rule.color,
enabled: rule.enabled,
customized: true,
};
}
return {
...defaultRule,
color: rule.color,
enabled: rule.enabled,
};
});
const existingRuleIds = new Set(normalizedRules.map((rule) => rule.id));
for (const defaultRule of DEFAULT_KEYWORD_HIGHLIGHT_RULES) {
if (!existingRuleIds.has(defaultRule.id)) {
normalizedRules.push(cloneKeywordHighlightRule(defaultRule));
}
}
return normalizedRules;
};
const isMiddleClickBehavior = (value: unknown): value is MiddleClickBehavior => (
value === 'context-menu' ||
value === 'paste' ||
value === 'disabled'
);
const resolveMiddleClickBehavior = (
settings?: Partial<TerminalSettings> | null,
): MiddleClickBehavior => {
if (isMiddleClickBehavior(settings?.middleClickBehavior)) {
return settings.middleClickBehavior;
}
if (
settings &&
Object.prototype.hasOwnProperty.call(settings, 'middleClickPaste') &&
settings.middleClickPaste === false
) {
return 'disabled';
}
return DEFAULT_TERMINAL_SETTINGS.middleClickBehavior;
};
const isDynamicTabTitleMode = (value: unknown): value is DynamicTabTitleMode => (
value === 'off' ||
value === 'agent' ||
value === 'all'
);
const isHostInfoBarTitleMode = (value: unknown): value is HostInfoBarTitleMode => (
value === 'address' ||
value === 'label'
);
const isPasswordPromptAssistMode = (value: unknown): value is PasswordPromptAssistMode => (
value === 'off' ||
value === 'hint' ||
value === 'picker'
);
const isAutocompleteHistoryScope = (value: unknown): value is AutocompleteHistoryScope => (
value === 'host' ||
value === 'global'
);
const isOscNotificationMode = (value: unknown): value is OscNotificationMode => (
value === 'off' ||
value === 'unfocused' ||
value === 'always'
);
const isDisconnectedNoticeMode = (value: unknown): value is DisconnectedNoticeMode => (
value === 'terminal' || value === 'dialog'
);
export const normalizeTerminalSettings = (
settings?: Partial<TerminalSettings> | null,
): TerminalSettings => {
const middleClickBehavior = resolveMiddleClickBehavior(settings);
const wordSeparators = typeof settings?.wordSeparators === 'string'
? settings.wordSeparators
: DEFAULT_TERMINAL_SETTINGS.wordSeparators;
const shiftEnterNewlineText = typeof settings?.shiftEnterNewlineText === 'string'
? settings.shiftEnterNewlineText
: DEFAULT_TERMINAL_SETTINGS.shiftEnterNewlineText;
const mergedSettings = {
...DEFAULT_TERMINAL_SETTINGS,
...(settings ?? {}),
middleClickBehavior,
middleClickPaste: middleClickBehavior === 'paste',
wordSeparators,
shiftEnterNewlineText,
dynamicTabTitleMode: isDynamicTabTitleMode(settings?.dynamicTabTitleMode)
? settings.dynamicTabTitleMode
: DEFAULT_TERMINAL_SETTINGS.dynamicTabTitleMode,
hostInfoBarTitleMode: isHostInfoBarTitleMode(settings?.hostInfoBarTitleMode)
? settings.hostInfoBarTitleMode
: DEFAULT_TERMINAL_SETTINGS.hostInfoBarTitleMode,
passwordPromptAssist: isPasswordPromptAssistMode(settings?.passwordPromptAssist)
? settings.passwordPromptAssist
: DEFAULT_TERMINAL_SETTINGS.passwordPromptAssist,
autocompleteHistoryScope: isAutocompleteHistoryScope(settings?.autocompleteHistoryScope)
? settings.autocompleteHistoryScope
: DEFAULT_TERMINAL_SETTINGS.autocompleteHistoryScope,
oscNotifications: isOscNotificationMode(settings?.oscNotifications)
? settings.oscNotifications
: DEFAULT_TERMINAL_SETTINGS.oscNotifications,
disconnectedNoticeMode: isDisconnectedNoticeMode(settings?.disconnectedNoticeMode)
? settings.disconnectedNoticeMode
: DEFAULT_TERMINAL_SETTINGS.disconnectedNoticeMode,
};
// Migrate legacy 'canvas' renderer to 'dom' (canvas removed in xterm.js 6.0)
const rendererType = (mergedSettings.rendererType as string) === 'canvas'
? 'dom' as const
: mergedSettings.rendererType;
// Persisted installs wrote the old default (8) into localStorage with no UI
// to change it; bump that sentinel to the new default while keeping custom caps.
const autocompleteMaxSuggestions = mergedSettings.autocompleteMaxSuggestions === 8
? DEFAULT_TERMINAL_SETTINGS.autocompleteMaxSuggestions
: mergedSettings.autocompleteMaxSuggestions;
return {
...mergedSettings,
rendererType,
autocompleteMaxSuggestions,
hibernateHiddenTabsDelaySec: normalizeHibernateHiddenTabsDelaySec(
mergedSettings.hibernateHiddenTabsDelaySec,
),
hibernateKeepRendererCount: normalizeHibernateKeepRendererCount(
mergedSettings.hibernateKeepRendererCount,
),
hibernateReplayChunkBytes: normalizeHibernateReplayChunkBytes(
mergedSettings.hibernateReplayChunkBytes,
),
inlineImageStorageLimitMb: normalizeInlineImageStorageLimitMb(
mergedSettings.inlineImageStorageLimitMb,
),
inlineImageMaxMegapixels: normalizeInlineImageMaxMegapixels(
mergedSettings.inlineImageMaxMegapixels,
),
inlineImageSequenceLimitMb: normalizeInlineImageSequenceLimitMb(
mergedSettings.inlineImageSequenceLimitMb,
),
autocompleteGhostText: mergedSettings.autocompletePopupMenu
? false
: mergedSettings.autocompleteGhostText,
keywordHighlightRules: normalizeKeywordHighlightRules(
mergedSettings.keywordHighlightRules,
),
};
};
/** Default scrollback rows for new installs (VS Code uses 1000; we keep a modest headroom). */
export const DEFAULT_TERMINAL_SCROLLBACK = 3000;
const DEFAULT_TERMINAL_SETTINGS: TerminalSettings = {
scrollback: DEFAULT_TERMINAL_SCROLLBACK,
drawBoldInBrightColors: true,
terminalEmulationType: 'xterm-256color',
startupCommandDelayMs: 600,
fontLigatures: true,
fontSmoothing: true,
fontWeight: 400,
fontWeightBold: 700,
linePadding: 0,
fallbackFont: '',
cursorShape: 'block',
cursorBlink: true,
highlightCursorLine: false,
minimumContrastRatio: 1,
altAsMeta: false,
optionArrowWordJump: false,
shiftEnterNewlineEnabled: true,
shiftEnterNewlineText: '\\n',
kittyKeyboardProtocolEnabled: false,
scrollOnInput: true,
scrollOnOutput: false,
scrollOnKeyPress: false,
scrollOnPaste: true,
smoothScrolling: false,
rightClickBehavior: 'context-menu',
showContextMenuOverFullscreenApps: false,
middleClickBehavior: 'paste',
copyOnSelect: false,
normalizeTextOnCopy: true, // Clean soft wraps + padding on copy (opt-out available)
middleClickPaste: true,
wordSeparators: DEFAULT_TERMINAL_WORD_SEPARATORS,
linkModifier: 'none',
autoCloseOnExit: true,
// Issue #3087: keep terminal history visible after an established session disconnects.
disconnectedNoticeMode: 'terminal',
keywordHighlightEnabled: true,
keywordHighlightRules: DEFAULT_KEYWORD_HIGHLIGHT_RULES,
localShell: '', // Empty = use system default
localShellArgs: [], // Launch args for a custom local shell (empty = bridge default args)
localStartDir: '', // Empty = use home directory
// Cloud-friendly defaults: 30s interval keeps NAT/LB state tables alive,
// and 10 unanswered keepalives provides headroom for brief network glitches
// before declaring the session dead (~5 min). Hosts whose SSH stack doesn't
// reply to keepalive@openssh.com (older routers/switches) should set their
// own per-host keepaliveOverride and dial these values down.
verifyHostKeys: true,
keepaliveInterval: 30,
keepaliveCountMax: 10,
sshAutoReconnectEnabled: false,
x11Display: '', // Empty = use DISPLAY/default local X server
moshClientPath: '', // Legacy mosh-client override; normal UI uses bundled mosh-client
showHostInfoBar: true, // Preserve the existing host information bar by default
hostInfoBarTitleMode: 'address', // Historical default: prefer user@host:port
showServerStats: true, // Show server stats by default
serverStatsRefreshInterval: 5, // Refresh every 5 seconds
systemManagerProcessRefreshInterval: 3,
systemManagerTmuxRefreshInterval: 3,
systemManagerDockerListRefreshInterval: 5,
systemManagerDockerStatsRefreshInterval: 3,
disableBracketedPaste: false, // Bracketed paste enabled by default
autoUploadClipboardImageOnPaste: false, // Opt-in: image in clipboard auto-uploads on paste (remote sessions)
clearWipesScrollback: true, // POSIX-standard: shell `clear` clears scrollback too
preserveSelectionOnInput: false, // Opt-in: keep selection alive when typing
forcePromptNewLine: false, // Opt-in: keep the next shell prompt visually separated from unterminated final output lines
osc52Clipboard: 'write-only', // OSC-52: allow remote programs to write clipboard by default
oscNotifications: 'always', // Honor OSC 9/777/99 desktop notifications by default
dynamicTabTitleMode: 'agent',
rendererType: 'auto', // Auto-detect best renderer based on hardware
hibernateHiddenTabs: false,
hibernateHiddenTabsDelaySec: 5,
hibernateSkipAltScreen: true,
hibernateKeepRendererCount: 2,
hibernateReplayChunkBytes: 16 * 1024,
hibernatePreferWasmSerialize: false,
// Opt-in: loading the image addon has bundle/parser cost, and sessions that
// have drawn images cannot full-hibernate. Protocols stay enabled so turning
// the master switch on is a single click.
inlineImagesEnabled: false,
inlineImageKittyEnabled: true,
inlineImageSixelEnabled: true,
inlineImageIipEnabled: true,
inlineImageStorageLimitMb: TERMINAL_INLINE_IMAGE_STORAGE_LIMIT_MB_DEFAULT,
inlineImageMaxMegapixels: TERMINAL_INLINE_IMAGE_MAX_MEGAPIXELS_DEFAULT,
inlineImageSequenceLimitMb: TERMINAL_INLINE_IMAGE_SEQUENCE_LIMIT_MB_DEFAULT,
showLineTimestamps: false, // Opt-in: shows output timestamps beside terminal lines
autocompleteEnabled: true, // Autocomplete enabled by default
autocompleteGhostText: false, // Mutually exclusive with popup menu
autocompletePopupMenu: true, // Popup menu enabled by default
autocompleteDebounceMs: 100, // 100ms debounce
autocompleteMinChars: 1, // Start suggesting after 1 character
autocompleteMaxSuggestions: 50, // Show up to 50 suggestions (popup scrolls)
autocompleteHistoryScope: 'host', // Per-host history suggestions by default (#2595)
passwordPromptAssist: 'hint', // Historical sudo confirm-to-fill; picker is opt-in (#2156)
};
export interface TerminalTheme {
id: string;
name: string;
type: 'dark' | 'light';
isCustom?: boolean;
colors: {
background: string;
foreground: string;
cursor: string;
selection: string;
black: string;
red: string;
green: string;
yellow: string;
blue: string;
magenta: string;
cyan: string;
white: string;
brightBlack: string;
brightRed: string;
brightGreen: string;
brightYellow: string;
brightBlue: string;
brightMagenta: string;
brightCyan: string;
brightWhite: string;
}
}
export interface TerminalSession {
id: string;
hostId: string;
hostLabel: string;
username: string;
hostname: string;
status: 'connecting' | 'connected' | 'disconnected';
workspaceId?: string;
/** Script to auto-run after connect (multi-host script runner). */
pendingScriptId?: string;
/** Snapshot used by "Run now" so unsaved editor changes run exactly as shown. */
pendingScript?: Snippet;
startupCommand?: string; // Command to run after connection (for snippet runner)
noAutoRun?: boolean; // If true, paste command without auto-executing
multiLineRunMode?: Snippet['multiLineRunMode'];
// Connection-time protocol overrides (used instead of looking up from hosts)
protocol?: HostProtocol;
pluginConnection?: PluginConnectionConfig;
port?: number;
moshEnabled?: boolean;
etEnabled?: boolean;
shellType?: 'posix' | 'fish' | 'powershell' | 'cmd' | 'unknown';
charset?: string; // Connection-time charset override (e.g. for quick-connect serial)
// Serial-specific connection settings
serialConfig?: SerialConfig;
localShell?: string; // Shell command for local terminals (from discovery)
localShellArgs?: string[]; // Shell args for local terminals (from discovery)
localShellName?: string; // Display name for local shell (e.g., "Zsh", "Ubuntu (WSL)")
localShellIcon?: string; // Icon identifier for local shell (e.g., "zsh", "ubuntu")
localStartDir?: string; // Per-session starting directory for local terminals
// For sessions created from an existing SSH session: the id of the source
// session whose already-authenticated connection should be reused so the new
// shell channel does not trigger a second MFA prompt (issue #1204). The
// bridge reuses the source connection when it is still live, otherwise it
// falls back to a fresh connection — so this also applies on reconnect: a
// reconnect reuses the source again if still connected, else dials fresh.
reuseConnectionFromSessionId?: string;
// Marker for "Duplicate Session" clones: never multiplex onto any live,
// parked, or in-flight pooled transport (including the source's own
// connection) — always dial a brand-new connection with fresh auth. The
// starter turns this into `reuseTransport: false` on every attempt.
requireFreshConnection?: boolean;
// Per-pane font size override (workspace splits only; not persisted to vault hosts).
fontSize?: number;
fontSizeOverride?: boolean;
/** User-assigned display name for this terminal session (overrides hostLabel in UI) */
customName?: string;
/** Runtime shell-reported window title (OSC 0/2), shown on tabs when enabled */
dynamicTitle?: string;
/** Sticky coding CLI provider detected from launch command or window title */
codingCliProviderId?: CodingCliProviderId;
/** Runtime marker for sessions reconstructed from startup restore. */
restoreState?: 'restored-disconnected';
/**
* Runtime marker for sessions backed by an in-memory-only host (e.g. a
* password deep link). Excluded from session restore persistence because
* the one-time credentials cannot survive a relaunch.
*/
ephemeralHost?: boolean;
/**
* Runtime marker for sessions opened via MCP host_open while "silent
* sessions" is enabled. Hidden from the main window's tab bar (TopTabs,
* QuickSwitcher, orphan tab ordering) and excluded from session-restore
* persistence, but remains a fully live session reachable by terminal
* exec/sftp/session-close tools, and still visible in TrayPanel and the
* external MCP session list.
*/
hiddenFromTabs?: boolean;
/** Runtime hint to auto-open a side panel once the session connects. */
autoOpenSidePanel?: 'sftp';
/** Latest known working directory captured from terminal cwd tracking. */
lastCwd?: string;
/**
* Transient one-shot: a directory a freshly-cloned/split REMOTE session
* should `cd` into on its first connect. Set by the clone factory when a
* copy/split inherits the source pane's cwd; the terminal's restore-cwd
* injection path applies it on connect, and it is cleared from the session as
* soon as a live cwd is tracked (see `updateSessionRestoreCwd`) so a later
* remount + reconnect does not re-inject a stale `cd`. Not persisted across
* relaunch. Local clones use `localStartDir` instead of this field.
*/
pendingInitialCwd?: string;
}

View File

@@ -0,0 +1,47 @@
export interface RemoteFile {
name: string;
type: 'file' | 'directory' | 'symlink';
size: string;
lastModified: string;
linkTarget?: 'file' | 'directory' | null; // For symlinks: the type of the target, or null if broken
permissions?: string; // rwx format for owner/group/others e.g. "rwxr-xr-x"
owner?: string;
hidden?: boolean; // Windows hidden attribute (only set for local Windows filesystem)
}
export type WorkspaceNode =
| {
id: string;
type: 'pane';
sessionId: string;
}
| {
id: string;
type: 'split';
direction: 'horizontal' | 'vertical';
children: WorkspaceNode[];
sizes?: number[]; // relative sizes for children
};
export type WorkspaceViewMode = 'split' | 'focus';
export interface Workspace {
id: string;
title: string;
root: WorkspaceNode;
viewMode?: WorkspaceViewMode; // 'split' = tiled view (default), 'focus' = left list + single terminal
focusedSessionId?: string; // Which session is focused when in focus mode
focusSessionOrder?: string[]; // User-defined session order for the focus-mode sidebar
snippetId?: string; // If this workspace was created from running a snippet
// Whether `title` is an explicit name the user/caller chose. `false` means a
// user rename or a named-at-creation workspace; `true`/absent means the tab
// may derive a host-based label instead of showing the generic default.
// Absent on legacy workspaces — the tab falls back to the default-title
// string check for those. See resolveWorkspaceTabLabel.
autoTitle?: boolean;
// `title` was composed from the panes' connection labels (e.g. a merged
// workspace "01/02"), not chosen by the user. The store keeps such titles
// synchronized with the workspace's membership and pane renames until the
// user renames the workspace (which clears this flag).
generatedTitle?: boolean;
}