import React, { createContext, lazy, memo, Suspense, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
import { activeTabStore } from '../../application/state/activeTabStore';
import {
applySessionPresentation,
usePresentedSession,
} from '../../application/state/sessionPresentationStore';
import { useTerminalLayoutSuppressActive } from '../../application/state/terminalLayoutSuppressStore';
import type { TerminalSessionExitEvent } from '../../application/state/resolveTerminalSessionExitIntent';
import { createTerminalSelectionAttachment } from '../../application/state/terminalSelectionAttachment';
import { getTopTabInsertionTarget, isPointInsideRect, WORKSPACE_SESSION_DRAG_TYPE } from '../../application/state/terminalDragData';
import { useAIState } from '../../application/state/useAIState';
import { useAISessionsStore } from '../../application/state/aiSessionsStore';
import { useStoredBoolean } from '../../application/state/useStoredBoolean';
import { isSavedVaultHost } from '../../domain/ephemeralHosts';
import {
buildAITerminalSessionInfo,
type AIPanelContext,
type AITerminalSessionInfo,
} from '../../domain/buildAITerminalSessionInfo';
export { buildAITerminalSessionInfo };
export type { AIPanelContext, AITerminalSessionInfo };
import { collectSessionIds, SplitDirection } from '../../domain/workspace';
import { resolveSessionTabTitle } from '../../domain/sessionTabTitle';
import { terminalPaneSessionsEqual } from '../../domain/terminalPaneSessionsEqual';
import {
resolveTerminalHibernateEnabled,
resolveTerminalHibernateEnabledForProtocol,
} from '../../domain/terminalHibernate';
import { KeyBinding, TerminalSettings } from '../../domain/models';
import type { TerminalCwdChangeMeta } from '../terminal/sftpCwd';
import { STORAGE_KEY_AI_SHOW_TERMINAL_SELECTION_ACTION } from '../../infrastructure/config/storageKeys';
import { cn } from '../../lib/utils';
import { LazyLoadBoundary } from '../ui/lazy-load-boundary';
import type { DropEntry } from '../../lib/sftpFileUtils';
import type { GroupConfig, Host, Identity, KnownHost, ProxyProfile, SSHKey, Snippet, TerminalSession, VaultNote, Workspace } from '../../types';
import type { ExecutorContext } from '../../infrastructure/ai/cattyAgent/executor';
import type { AISession } from '../../infrastructure/ai/types';
import Terminal from '../Terminal';
import { removePaneVisible, setPaneVisible } from '../terminal/paneVisibilityStore';
import type { TerminalBroadcastInputOptions } from '../terminal/terminalHelpers';
import type { TerminalContextReader } from '../../domain/terminalContextRead';
import {
getTerminalPaneRenderSnapshot,
parseTerminalPaneRenderSnapshot,
resolveInactiveTerminalPaneStyle,
shouldUseTerminalPaneSplitLayout,
type TerminalPaneHiddenSize,
} from '../terminalPaneVisibility';
import type { ResolvedAppearance, TerminalAppearanceHostScope } from '../../domain/terminalAppearanceRuntime';
import type { TerminalSidePanelAutoOpenTab } from '../../domain/terminalSidePanelAutoOpen';
import type { SidePanelTool } from '../../domain/sidePanelLayout';
import {
resolvePaneMagnificationStyle,
type PaneMagnificationController,
type PaneMagnificationTarget,
} from '../../domain/paneMagnification';
export type SidePanelTab = SidePanelTool;
const LazyAIChatSidePanel = lazy(() =>
import('../AIChatSidePanel').then((module) => ({ default: module.AIChatSidePanel })),
);
if (typeof requestIdleCallback === 'function') {
requestIdleCallback(() => {
void import('../AIChatSidePanel');
});
}
const AIChatSidePanelFallback = memo(function AIChatSidePanelFallback() {
return (
);
});
export type WorkspaceRect = { x: number; y: number; w: number; h: number };
export type SplitHint = {
direction: 'horizontal' | 'vertical';
position: 'left' | 'right' | 'top' | 'bottom';
targetSessionId?: string;
rect?: { x: number; y: number; w: number; h: number };
} | null;
export type ResizerHandle = {
id: string;
splitId: string;
index: number;
direction: 'vertical' | 'horizontal';
rect: { x: number; y: number; w: number; h: number };
splitArea: { w: number; h: number };
};
export type PendingSftpUpload = {
requestId: string;
hostId: string;
/** Full connection identity (id:hostname:port:protocol) for session-override awareness */
connectionKey: string;
/** Terminal session where the drop originated, including Mosh and ET. */
originSessionId?: string;
/** Terminal session whose active route must own the accepting SFTP connection. */
sourceSessionId?: string;
targetPath?: string;
entries: DropEntry[];
};
export type SnippetExecutor = (
command: string,
noAutoRun?: boolean,
options?: {
broadcast?: boolean;
multiLineRunMode?: Snippet["multiLineRunMode"];
/** When false, do not steal keyboard focus (multi-tab fan-out). Default true. */
focus?: boolean;
},
/**
* Returns true when the command was written to the session. False means the
* executor could not write and the caller may fall back to a direct backend
* write. May be async when the pane needs to wake from hibernation first.
*/
) => boolean | Promise;
export type PendingTerminalSelectionForAI = {
requestId: string;
tabId: string;
text: string;
};
export function hexToHslToken(hex: string): string {
const normalized = hex.startsWith('#') ? hex : `#${hex}`;
const r = parseInt(normalized.slice(1, 3), 16) / 255;
const g = parseInt(normalized.slice(3, 5), 16) / 255;
const b = parseInt(normalized.slice(5, 7), 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h = 0;
let s = 0;
const l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
break;
case g:
h = ((b - r) / d + 2) / 6;
break;
default:
h = ((r - g) / d + 4) / 6;
break;
}
}
return `${Math.round(h * 3600) / 10} ${Math.round(s * 1000) / 10}% ${Math.round(l * 1000) / 10}%`;
}
export function adjustLightnessToken(hsl: string, delta: number): string {
const parts = hsl.split(/\s+/);
const newL = Math.max(0, Math.min(100, parseFloat(parts[2]) + delta));
return `${parts[0]} ${parts[1]} ${Math.round(newL * 10) / 10}%`;
}
export function adjustSaturationToken(hsl: string, factor: number): string {
const parts = hsl.split(/\s+/);
const newS = Math.max(0, Math.min(100, parseFloat(parts[1]) * factor));
return `${parts[0]} ${Math.round(newS * 10) / 10}% ${parts[2]}`;
}
export type { TerminalThemePreviewState } from './terminalThemePreview';
export {
emptyTerminalThemePreview,
listThemePreviewSessionIds,
resolvePaneThemePreviewId,
} from './terminalThemePreview';
export const clearTerminalPreviewVars = (sessionId: string | null) => {
if (!sessionId || typeof document === 'undefined') return;
const pane = document.querySelector(`[data-session-id="${sessionId}"]`);
if (!pane) return;
pane.style.removeProperty('--terminal-preview-bg');
pane.style.removeProperty('--terminal-preview-fg');
pane.style.removeProperty('--terminal-preview-border');
pane.style.removeProperty('--terminal-preview-toolbar-btn');
pane.style.removeProperty('--terminal-preview-toolbar-btn-hover');
pane.style.removeProperty('--terminal-preview-toolbar-btn-active');
};
export const setStylePropertyIfChanged = (element: HTMLElement, property: string, value: string) => {
if (element.style.getPropertyValue(property) === value) return;
element.style.setProperty(property, value);
};
const removeStylePropertyIfSet = (element: HTMLElement, property: string) => {
if (!element.style.getPropertyValue(property)) return;
element.style.removeProperty(property);
};
const HOST_TREE_PREVIEW_PROPERTIES = [
'--terminal-host-tree-bg',
'--terminal-host-tree-fg',
'--terminal-host-tree-muted',
'--terminal-host-tree-separator',
'--terminal-host-tree-hover-bg',
'--terminal-host-tree-active-bg',
'--terminal-host-tree-drop-bg',
'--terminal-host-tree-folder-fg',
] as const;
const getHostTreePreviewRoots = (): HTMLElement[] => {
if (typeof document === 'undefined') return [];
return Array.from(document.querySelectorAll(
'[data-section="app-host-tree-layer"], [data-section="terminal-host-tree-sidebar"]',
));
};
export const clearHostTreePreviewVars = () => {
const roots = getHostTreePreviewRoots();
for (const root of roots) {
for (const property of HOST_TREE_PREVIEW_PROPERTIES) {
removeStylePropertyIfSet(root, property);
}
}
};
export const clearTopTabsPreviewVars = () => {
if (typeof document === 'undefined') return;
const tabsRoot = document.querySelector('[data-top-tabs-root]');
if (!tabsRoot) return;
removeStylePropertyIfSet(tabsRoot, '--top-tabs-bg');
removeStylePropertyIfSet(tabsRoot, '--top-tabs-fg');
removeStylePropertyIfSet(tabsRoot, '--top-tabs-muted');
removeStylePropertyIfSet(tabsRoot, '--top-tabs-active-bg');
removeStylePropertyIfSet(tabsRoot, '--top-tabs-accent');
removeStylePropertyIfSet(tabsRoot, '--background');
removeStylePropertyIfSet(tabsRoot, '--foreground');
removeStylePropertyIfSet(tabsRoot, '--accent');
removeStylePropertyIfSet(tabsRoot, '--accent-foreground');
removeStylePropertyIfSet(tabsRoot, '--primary');
removeStylePropertyIfSet(tabsRoot, '--primary-foreground');
removeStylePropertyIfSet(tabsRoot, '--secondary');
removeStylePropertyIfSet(tabsRoot, '--border');
removeStylePropertyIfSet(tabsRoot, '--muted-foreground');
};
export const filterTabsMap = (source: Map, validIds: Set): Map => {
let changed = false;
const next = new Map();
for (const [id, value] of source) {
if (validIds.has(id)) {
next.set(id, value);
} else {
changed = true;
}
}
return changed ? next : source;
};
export { ChunkedEscapeFilter, hasNotifiableTerminalOutput } from './activityEscapeFilter';
/**
* Providers, permissions, agent config and the session mutators — everything
* except `sessions` / `activeSessionIdMap` / `draftsByScope` /
* `panelViewByScope`, which `useAIState` deliberately keeps out of its return
* and publishes to `aiSessionsStore` instead. Because the hot slices are absent,
* a landing token cannot change this Context's identity, so provider and
* permission consumers stay put while a turn streams.
*/
export type AIConfigValue = ReturnType;
const AIConfigContext = createContext(null);
interface AIChatPanelsHostProps {
mountedTabIds: string[];
activeTabId: string | null;
activeSidePanelTab: SidePanelTab | null;
contextsByTabId: Map;
resolveExecutorContext: (scope: {
type: 'terminal' | 'workspace';
targetId?: string;
label?: string;
}) => ExecutorContext;
pendingTerminalSelection?: PendingTerminalSelectionForAI | null;
onPendingTerminalSelectionConsumed?: (requestId: string) => void;
notes: VaultNote[];
hosts: Host[];
snippets: Snippet[];
onOpenVaultNoteFromChat?: (noteId: string) => void;
onOpenVaultHostFromChat?: (hostId: string) => void;
onOpenVaultSectionFromChat?: (section: 'notes' | 'hosts' | 'snippets') => void;
onOpenVaultSnippetFromChat?: (snippetId: string) => void;
}
const EMPTY_WORKSPACES: Workspace[] = [];
interface AIStateMaintenanceHostProps {
validAIScopeTargetIds: Set;
workspaces?: Workspace[] | null;
}
const AIStateProviderInner: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const aiConfig = useAIState();
return (
{children}
);
};
export const AIStateProvider = memo(AIStateProviderInner);
AIStateProvider.displayName = 'AIStateProvider';
const AIStateMaintenanceHostInner: React.FC = ({
validAIScopeTargetIds,
workspaces: workspacesProp,
}) => {
const aiConfig = useContext(AIConfigContext);
if (!aiConfig) {
throw new Error('AIStateMaintenanceHost must be rendered inside AIStateProvider');
}
// Guard missing prop so a wiring gap cannot crash the terminal shell.
const workspaces = workspacesProp ?? EMPTY_WORKSPACES;
const {
cleanupOrphanedSessions,
seedWorkspaceActiveSessionFromMembers,
handoffDissolvedWorkspaceScope,
retargetWorkspaceActiveChatForMemberLoss,
} = aiConfig;
const previousWorkspacesRef = useRef(workspaces);
const previousSessionWorkspaceRef = useRef(
new Map(workspaces.flatMap((workspace) => (
collectSessionIds(workspace.root).map((sessionId) => [sessionId, workspace.id] as const)
))),
);
useEffect(() => {
const previousWorkspaces = previousWorkspacesRef.current;
const previousIds = new Set(previousWorkspaces.map((workspace) => workspace.id));
const nextIds = new Set(workspaces.map((workspace) => workspace.id));
const previousSessionWorkspace = previousSessionWorkspaceRef.current;
for (const workspace of workspaces) {
if (previousIds.has(workspace.id)) continue;
const memberTerminalIds = collectSessionIds(workspace.root);
seedWorkspaceActiveSessionFromMembers({
workspaceId: workspace.id,
memberTerminalIds,
preferredTerminalId: workspace.focusedSessionId,
});
}
for (const workspace of workspaces) {
for (const sessionId of collectSessionIds(workspace.root)) {
const previousWorkspaceId = previousSessionWorkspace.get(sessionId);
if (previousWorkspaceId === workspace.id) continue;
// Member newly joined this workspace — seed only fills an empty map.
// Prefer the workspace focused pane so we don't pin the joiner's chat
// ahead of the pane the user is already looking at.
seedWorkspaceActiveSessionFromMembers({
workspaceId: workspace.id,
memberTerminalIds: collectSessionIds(workspace.root),
preferredTerminalId: workspace.focusedSessionId,
});
break;
}
}
for (const workspace of workspaces) {
if (!previousIds.has(workspace.id)) continue;
const previousWorkspace = previousWorkspaces.find((entry) => entry.id === workspace.id);
if (!previousWorkspace) continue;
const previousMemberIds = collectSessionIds(previousWorkspace.root);
const currentMemberIds = collectSessionIds(workspace.root);
if (previousMemberIds.every((sessionId) => currentMemberIds.includes(sessionId))) continue;
retargetWorkspaceActiveChatForMemberLoss({
workspaceId: workspace.id,
previousMemberTerminalIds: previousMemberIds,
currentMemberTerminalIds: currentMemberIds,
preferredTerminalId: workspace.focusedSessionId,
});
}
for (const workspace of previousWorkspaces) {
if (nextIds.has(workspace.id)) continue;
const memberTerminalIds = collectSessionIds(workspace.root);
handoffDissolvedWorkspaceScope({
workspaceId: workspace.id,
terminalIds: memberTerminalIds.filter((sessionId) => validAIScopeTargetIds.has(sessionId)),
preferredTerminalId: workspace.focusedSessionId,
});
}
previousWorkspacesRef.current = workspaces;
previousSessionWorkspaceRef.current = new Map(workspaces.flatMap((workspace) => (
collectSessionIds(workspace.root).map((sessionId) => [sessionId, workspace.id] as const)
)));
cleanupOrphanedSessions(validAIScopeTargetIds);
}, [
cleanupOrphanedSessions,
handoffDissolvedWorkspaceScope,
retargetWorkspaceActiveChatForMemberLoss,
seedWorkspaceActiveSessionFromMembers,
validAIScopeTargetIds,
workspaces,
]);
return null;
};
export const AIStateMaintenanceHost = memo(AIStateMaintenanceHostInner);
AIStateMaintenanceHost.displayName = 'AIStateMaintenanceHost';
interface AISidePanelStateRootProps {
validAIScopeTargetIds: Set;
workspaces?: Workspace[] | null;
children: React.ReactNode;
}
const AISidePanelStateRootInner: React.FC = ({
validAIScopeTargetIds,
workspaces,
children,
}) => (
{children}
);
export const AISidePanelStateRoot = memo(AISidePanelStateRootInner);
AISidePanelStateRoot.displayName = 'AISidePanelStateRoot';
function aiChatPanelsHostAreEqual(
prev: AIChatPanelsHostProps,
next: AIChatPanelsHostProps,
): boolean {
if (prev.mountedTabIds !== next.mountedTabIds) return false;
if (prev.contextsByTabId !== next.contextsByTabId) return false;
if (prev.activeSidePanelTab !== next.activeSidePanelTab) return false;
if (prev.pendingTerminalSelection !== next.pendingTerminalSelection) return false;
if (prev.onPendingTerminalSelectionConsumed !== next.onPendingTerminalSelectionConsumed) return false;
if (prev.resolveExecutorContext !== next.resolveExecutorContext) return false;
if (prev.notes !== next.notes) return false;
if (prev.hosts !== next.hosts) return false;
if (prev.snippets !== next.snippets) return false;
if (prev.onOpenVaultNoteFromChat !== next.onOpenVaultNoteFromChat) return false;
if (prev.onOpenVaultHostFromChat !== next.onOpenVaultHostFromChat) return false;
if (prev.onOpenVaultSectionFromChat !== next.onOpenVaultSectionFromChat) return false;
if (prev.onOpenVaultSnippetFromChat !== next.onOpenVaultSnippetFromChat) return false;
if (prev.activeTabId === next.activeTabId) return true;
for (let i = 0; i < prev.mountedTabIds.length; i += 1) {
const tabId = prev.mountedTabIds[i];
const prevAiVisible = prev.activeTabId === tabId && prev.activeSidePanelTab === 'ai';
const nextAiVisible = next.activeTabId === tabId && next.activeSidePanelTab === 'ai';
if (prevAiVisible !== nextAiVisible) return false;
}
return true;
}
const consumedTerminalSelectionRequestIds = new Set();
const CONSUMED_TERMINAL_SELECTION_REQUEST_ID_LIMIT = 64;
function markTerminalSelectionRequestConsumed(requestId: string): void {
consumedTerminalSelectionRequestIds.add(requestId);
if (consumedTerminalSelectionRequestIds.size <= CONSUMED_TERMINAL_SELECTION_REQUEST_ID_LIMIT) {
return;
}
const oldest = consumedTerminalSelectionRequestIds.values().next().value;
if (oldest !== undefined) consumedTerminalSelectionRequestIds.delete(oldest);
}
const AIChatPanelsHostInner: React.FC = ({
mountedTabIds,
activeTabId,
activeSidePanelTab,
contextsByTabId,
resolveExecutorContext,
pendingTerminalSelection,
onPendingTerminalSelectionConsumed,
notes,
hosts,
snippets,
onOpenVaultNoteFromChat,
onOpenVaultHostFromChat,
onOpenVaultSectionFromChat,
onOpenVaultSnippetFromChat,
}) => {
const aiConfig = useContext(AIConfigContext);
if (!aiConfig) {
throw new Error('AIChatPanelsHost must be rendered inside AIStateProvider');
}
const {
sessions,
activeSessionIdMap,
draftsByScope,
panelViewByScope,
} = useAISessionsStore();
const {
defaultAgentId,
showDraftView,
updateDraft,
} = aiConfig;
useEffect(() => {
if (!pendingTerminalSelection) return;
if (consumedTerminalSelectionRequestIds.has(pendingTerminalSelection.requestId)) {
return;
}
const context = contextsByTabId.get(pendingTerminalSelection.tabId);
if (!context) return;
const attachment = createTerminalSelectionAttachment(pendingTerminalSelection.text);
markTerminalSelectionRequestConsumed(pendingTerminalSelection.requestId);
onPendingTerminalSelectionConsumed?.(pendingTerminalSelection.requestId);
if (!attachment) return;
const scopeKey = `${context.scopeType}:${context.scopeTargetId ?? ''}`;
const isSessionView =
panelViewByScope[scopeKey]?.mode === 'session'
|| activeSessionIdMap[scopeKey] != null;
if (!isSessionView) {
showDraftView(scopeKey);
}
updateDraft(scopeKey, defaultAgentId, (draft) => ({
...draft,
attachments: [...draft.attachments, attachment],
}));
}, [
activeSessionIdMap,
contextsByTabId,
defaultAgentId,
onPendingTerminalSelectionConsumed,
panelViewByScope,
pendingTerminalSelection,
showDraftView,
updateDraft,
]);
return (
<>
{mountedTabIds.map((tabId) => {
const context = contextsByTabId.get(tabId);
if (!context) return null;
const isVisible = activeTabId === tabId && activeSidePanelTab === 'ai';
return (
}>
}
draftsByScope={draftsByScope}
panelViewByScope={panelViewByScope}
setActiveSessionId={aiConfig.setActiveSessionId}
ensureDraftForScope={aiConfig.ensureDraftForScope}
updateDraft={aiConfig.updateDraft}
showDraftView={aiConfig.showDraftView}
showSessionView={aiConfig.showSessionView}
clearDraftForScope={aiConfig.clearDraftForScope}
addDraftFiles={aiConfig.addDraftFiles}
removeDraftFile={aiConfig.removeDraftFile}
createSession={aiConfig.createSession}
deleteSession={aiConfig.deleteSession}
updateSessionTitle={aiConfig.updateSessionTitle}
updateSessionExternalSessionId={aiConfig.updateSessionExternalSessionId}
addMessageToSession={aiConfig.addMessageToSession}
updateLastMessage={aiConfig.updateLastMessage}
updateMessageById={aiConfig.updateMessageById}
persistContextCompaction={aiConfig.persistContextCompaction}
providers={aiConfig.providers}
activeProviderId={aiConfig.activeProviderId}
activeModelId={aiConfig.activeModelId}
defaultAgentId={aiConfig.defaultAgentId}
toolIntegrationMode={aiConfig.toolIntegrationMode}
externalAgents={aiConfig.externalAgents}
setExternalAgents={aiConfig.setExternalAgents}
agentModelMap={aiConfig.agentModelMap}
setAgentModel={aiConfig.setAgentModel}
agentProviderMap={aiConfig.agentProviderMap}
setAgentProvider={aiConfig.setAgentProvider}
agentThinkingMap={aiConfig.agentThinkingMap}
setAgentThinking={aiConfig.setAgentThinking}
updateProvider={aiConfig.updateProvider}
globalPermissionMode={aiConfig.globalPermissionMode}
setGlobalPermissionMode={aiConfig.setGlobalPermissionMode}
commandBlocklist={aiConfig.commandBlocklist}
commandTimeout={aiConfig.commandTimeout}
responseIdleTimeout={aiConfig.responseIdleTimeout}
maxIterations={aiConfig.maxIterations}
webSearchConfig={aiConfig.webSearchConfig}
quickMessages={aiConfig.quickMessages}
scopeType={context.scopeType}
scopeTargetId={context.scopeTargetId}
scopeHostIds={context.scopeHostIds}
scopeLabel={context.scopeLabel}
focusedSessionId={context.focusedSessionId}
terminalSessions={context.terminalSessions}
resolveExecutorContext={resolveExecutorContext}
isVisible={isVisible}
notes={notes}
hosts={hosts}
snippets={snippets}
onOpenVaultNote={onOpenVaultNoteFromChat}
onOpenVaultHost={onOpenVaultHostFromChat}
onOpenVaultSection={onOpenVaultSectionFromChat}
onOpenVaultSnippet={onOpenVaultSnippetFromChat}
/>
);
})}
>
);
};
export const AIChatPanelsHost = memo(AIChatPanelsHostInner, aiChatPanelsHostAreEqual);
AIChatPanelsHost.displayName = 'AIChatPanelsHost';
export interface TerminalLayerProps {
hosts: Host[];
portForwardingRules?: import('../../domain/models').PortForwardingRule[];
customGroups: string[];
groupConfigs: GroupConfig[];
proxyProfiles: ProxyProfile[];
keys: SSHKey[];
identities: Identity[];
snippets: Snippet[];
snippetPackages: string[];
openNoteRequest?: { tabId: string; noteId: string; requestId: number } | null;
onOpenVaultNoteFromChat?: (noteId: string) => void;
onOpenVaultHostFromChat?: (hostId: string) => void;
onOpenVaultSectionFromChat?: (section: 'notes' | 'hosts' | 'snippets') => void;
onOpenVaultSnippetFromChat?: (snippetId: string) => void;
sessions: TerminalSession[];
workspaces: Workspace[];
knownHosts?: KnownHost[];
draggingSessionId: string | null;
terminalTheme: TerminalTheme;
terminalThemeId?: string;
followAppTerminalTheme?: boolean;
pickTerminalTheme?: (themeId: string) => void;
clearThemeIntent?: () => void;
settleManualThemeIntent?: () => void;
resolveSessionAppearance?: (hostScope: TerminalAppearanceHostScope) => ResolvedAppearance;
accentMode?: 'theme' | 'custom';
customAccent?: string;
terminalSettings?: TerminalSettings;
terminalFontFamilyId: string;
fontSize?: number;
hotkeyScheme?: 'disabled' | 'mac' | 'pc';
disableTerminalFontZoom?: boolean;
restoreTerminalCwd?: boolean;
keyBindings?: KeyBinding[];
onHotkeyAction?: (action: string, event: KeyboardEvent) => void;
onUpdateTerminalThemeId?: (themeId: string) => void;
onUpdateTerminalFontFamilyId?: (fontFamilyId: string) => void;
onUpdateTerminalFontSize?: (fontSize: number) => void;
onUpdateTerminalFontWeight?: (fontWeight: number) => void;
onUpdateSessionFontSize?: (sessionId: string, fontSize: number) => void;
onUpdateSessionRestoreCwd?: (sessionId: string, cwd: string | null) => void;
onUpdateSessionDynamicTitle?: (sessionId: string, title: string | null) => void;
onUpdateSessionCodingCliProvider?: (sessionId: string, providerId: import('../../domain/codingCliProviders').CodingCliProviderId | null) => void;
onClearSessionFontSizeOverride?: (sessionId: string) => void;
onCloseSession: (sessionId: string, e?: React.MouseEvent) => void;
onUpdateSessionStatus: (sessionId: string, status: TerminalSession['status']) => void;
onUpdateHostDistro: (hostId: string, distro: string) => void;
onUpdateHost: (host: Host) => void;
onAddKnownHost?: (knownHost: KnownHost) => void;
onCommandExecuted?: (command: string, hostId: string, hostLabel: string, sessionId: string) => void;
onDeleteShellHistoryEntry?: (entryId: string) => void;
onTerminalDataCapture?: (sessionId: string, data: string) => void;
onCreateWorkspaceFromSessions: (baseSessionId: string, joiningSessionId: string, hint: Exclude) => void;
onAddSessionToWorkspace: (workspaceId: string, sessionId: string, hint: Exclude) => void;
onRequestAddToWorkspace?: (workspaceId: string) => void;
onAppendHostToWorkspace?: (workspaceId: string, hostId: string) => void;
onUpdateSplitSizes: (workspaceId: string, splitId: string, sizes: number[]) => void;
onSetDraggingSessionId: (id: string | null) => void;
onToggleWorkspaceViewMode?: (workspaceId: string) => void;
onSetWorkspaceFocusedSession?: (workspaceId: string, sessionId: string) => void;
onReorderWorkspaceSessions?: (workspaceId: string, draggedSessionId: string, targetSessionId: string, position: 'before' | 'after') => void;
onReorderTabs?: (draggedId: string, targetId: string, position: 'before' | 'after', additionalTabIds?: readonly string[]) => void;
onCopySession?: (sessionId: string) => void;
onDuplicateSession?: (sessionId: string) => void;
onCopySessionToNewWindow?: (sessionId: string) => void;
onRemoveSessionFromWorkspace?: (
sessionId: string,
tabInsertionTarget?: { tabId: string; position: 'before' | 'after'; additionalTabIds?: readonly string[] },
) => void;
onSplitSession?: (sessionId: string, direction: SplitDirection) => void;
onConnectToHost: (host: Host) => string | void;
onCreateLocalTerminal?: () => void;
// Broadcast mode
isBroadcastEnabled?: (workspaceId: string) => boolean;
isGlobalBroadcastEnabled?: boolean;
canUseGlobalBroadcast?: boolean;
onToggleBroadcast?: (workspaceId: string) => void;
onToggleGlobalBroadcast?: () => void;
// SFTP side panel
updateHosts: (hosts: Host[]) => void;
updateSnippets?: (snippets: Snippet[]) => void;
updateSnippetPackages?: (packages: string[]) => void;
sftpDefaultViewMode: 'list' | 'tree';
sftpDoubleClickBehavior: 'open' | 'transfer';
sftpAutoSync: boolean;
sftpShowHiddenFiles: boolean;
sftpUseCompressedUpload: boolean;
sftpAutoOpenSidebar: boolean;
terminalSidePanelAutoOpen?: boolean;
terminalSidePanelAutoOpenTab?: TerminalSidePanelAutoOpenTab;
sftpFollowTerminalCwd: boolean;
setSftpFollowTerminalCwd: (enabled: boolean) => void;
editorWordWrap: boolean;
setEditorWordWrap: (value: boolean) => void;
// Session log settings for real-time streaming
sessionLogsEnabled?: boolean;
sessionLogsDir?: string;
sessionLogsFormat?: "txt" | "raw" | "html";
sessionLogsTimestampsEnabled?: boolean;
sshDebugLogsEnabled?: boolean;
showHostTreeSidebar?: boolean;
toggleScriptsSidePanelRef?: React.MutableRefObject<(() => void) | null>;
toggleSidePanelRef?: React.MutableRefObject<(() => void) | null>;
paneMagnificationRef?: React.MutableRefObject;
// Session rename
onStartSessionRename?: (sessionId: string) => void;
onSubmitSessionRename?: (sessionId?: string, name?: string) => void;
}
interface TerminalPaneProps {
session: TerminalSession;
host: Host;
sessionHostResolved: boolean;
chainHosts?: Host[];
sudoAutofillPassword?: string;
sudoAutofillCandidates?: import("../terminal/runtime/terminalSudoAutofill").SudoPasswordAutofillCandidate[];
workspaceById: Map;
workspaceRectsById: Map>;
isTerminalLayerVisible: boolean;
magnifiedPane: { tabId: string; target: PaneMagnificationTarget } | null;
onMagnifyTerminalPane: (tabId: string, sessionId: string) => void;
onTerminalPaneInteraction: (tabId: string, sessionId: string) => void;
workspaceFocusHandlersRef: React.MutableRefObject