[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:
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;
|
||||
}
|
||||
Reference in New Issue
Block a user