[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
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:
457
domain/models/connection.ts
Normal file
457
domain/models/connection.ts
Normal 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
79
domain/models/history.ts
Normal 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;
|
||||
}
|
||||
290
domain/models/keyBindings.ts
Normal file
290
domain/models/keyBindings.ts
Normal 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' },
|
||||
];
|
||||
39
domain/models/portForwarding.ts
Normal file
39
domain/models/portForwarding.ts
Normal 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
137
domain/models/sftp.ts
Normal 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
675
domain/models/terminal.ts
Normal 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;
|
||||
}
|
||||
47
domain/models/workspace.ts
Normal file
47
domain/models/workspace.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user