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
284 lines
12 KiB
TypeScript
284 lines
12 KiB
TypeScript
import { useCallback, useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
|
|
import { TERMINAL_THEME_AUTO } from '../../../domain/terminalAppearance';
|
|
import { retainStableSessionsIgnoringPresentation } from '../../../domain/terminalPaneSessionsEqual';
|
|
import { getAppSettingsRuntime } from '../../state/appRuntimeBridge';
|
|
import { useAppearanceChromeStore } from '../../state/appearanceChromeStore';
|
|
import { useCustomThemes } from '../../state/customThemeStore';
|
|
import {
|
|
useSessionSnapshot,
|
|
useSessionSnapshotActions,
|
|
} from '../../state/sessionSnapshotStore';
|
|
import {
|
|
useSettingsChromeActions,
|
|
useSettingsChromeStore,
|
|
} from '../../state/settingsChromeStore';
|
|
import {
|
|
useTerminalSettingsActions,
|
|
useTerminalSettingsStore,
|
|
} from '../../state/terminalSettingsStore';
|
|
import { useThemeRuntime, useTerminalAppearanceInjection } from '../../state/useThemeRuntime';
|
|
import {
|
|
useVaultSnapshot,
|
|
} from '../../state/vaultSnapshotStore';
|
|
import { getAppHandlers, subscribeAppHandlers } from '../appHandlersBridge';
|
|
import { publishAppShellDomainSlice } from '../appShellPropsStore';
|
|
import { useAppLocalUiStore } from '../appLocalUiStore';
|
|
import { registerThemeRuntimeActions } from '../themeRuntimeBridge';
|
|
|
|
/**
|
|
* Terminal island: sessions from `sessionSnapshotStore`, terminal settings
|
|
* from `terminalSettingsStore`, mutators via snapshot actions, theme runtime
|
|
* owned here, glue handlers from the app handlers bridge. Assembles the full
|
|
* terminal domain bag field-by-field — never spreads a prepared bag.
|
|
*/
|
|
export function TerminalHost() {
|
|
const session = useSessionSnapshot();
|
|
const sessionActions = useSessionSnapshotActions();
|
|
const vault = useVaultSnapshot();
|
|
const terminalSettings = useTerminalSettingsStore();
|
|
const terminalSettingsActions = useTerminalSettingsActions();
|
|
const {
|
|
followAppTerminalTheme,
|
|
terminalThemeId,
|
|
terminalThemeDarkId,
|
|
terminalThemeLightId,
|
|
} = terminalSettings;
|
|
const settingsChrome = useSettingsChromeStore();
|
|
const settingsChromeActions = useSettingsChromeActions();
|
|
const appearance = useAppearanceChromeStore();
|
|
const customThemes = useCustomThemes();
|
|
const local = useAppLocalUiStore();
|
|
const handlers = useSyncExternalStore(
|
|
subscribeAppHandlers,
|
|
getAppHandlers,
|
|
getAppHandlers,
|
|
);
|
|
|
|
// Call-time getters: SettingsPublisher registers the runtime in a layout
|
|
// effect after this Host's first render. Capturing noop setters into
|
|
// useThemeRuntime would permanently drop follow-app UI theme persistence.
|
|
const setLightUiThemeId = useCallback((id: string) => {
|
|
getAppSettingsRuntime()?.setLightUiThemeId?.(id);
|
|
}, []);
|
|
const setDarkUiThemeId = useCallback((id: string) => {
|
|
getAppSettingsRuntime()?.setDarkUiThemeId?.(id);
|
|
}, []);
|
|
|
|
const themeRuntime = useThemeRuntime({
|
|
terminalThemeId,
|
|
terminalThemeDarkId,
|
|
terminalThemeLightId,
|
|
followAppTerminalTheme,
|
|
resolvedTheme: settingsChrome.resolvedTheme,
|
|
lightUiThemeId: settingsChrome.lightUiThemeId,
|
|
darkUiThemeId: settingsChrome.darkUiThemeId,
|
|
accentMode: appearance.accentMode,
|
|
customAccent: appearance.customAccent,
|
|
customThemes,
|
|
setTheme: settingsChromeActions.setTheme,
|
|
setLightUiThemeId,
|
|
setDarkUiThemeId,
|
|
});
|
|
|
|
const {
|
|
globalAppearance,
|
|
accentedGlobalAppearance,
|
|
clearIntent: clearThemeIntent,
|
|
settleManualIntent: settleManualThemeIntent,
|
|
pickTheme: pickTerminalTheme,
|
|
resolveFocusedAppearance,
|
|
currentTerminalTheme,
|
|
} = themeRuntime;
|
|
|
|
// Inject live accent into CSS vars without publishing accented theme identity
|
|
// into the terminal domain bag (accent drag must not rebuild AppShell).
|
|
useTerminalAppearanceInjection(accentedGlobalAppearance, {
|
|
includeChromeSurfaces: followAppTerminalTheme,
|
|
});
|
|
|
|
const prevFollowAppTerminalThemeRef = useRef(followAppTerminalTheme);
|
|
useLayoutEffect(() => {
|
|
if (prevFollowAppTerminalThemeRef.current === followAppTerminalTheme) return;
|
|
prevFollowAppTerminalThemeRef.current = followAppTerminalTheme;
|
|
clearThemeIntent();
|
|
}, [followAppTerminalTheme, clearThemeIntent]);
|
|
|
|
// Bridge exposes the stable base theme only — ChromeHost must not republish
|
|
// when accentedGlobalAppearance identity churns during color-picker drag.
|
|
const themeBridgeActions = useMemo(() => ({
|
|
clearThemeIntent,
|
|
settleManualThemeIntent,
|
|
pickTerminalTheme,
|
|
resolveFocusedAppearance: resolveFocusedAppearance as (...args: never[]) => unknown,
|
|
currentTerminalTheme,
|
|
globalAppearance,
|
|
}), [
|
|
clearThemeIntent,
|
|
currentTerminalTheme,
|
|
globalAppearance,
|
|
pickTerminalTheme,
|
|
resolveFocusedAppearance,
|
|
settleManualThemeIntent,
|
|
]);
|
|
|
|
useLayoutEffect(() => {
|
|
registerThemeRuntimeActions(themeBridgeActions);
|
|
return () => {
|
|
registerThemeRuntimeActions(null);
|
|
};
|
|
}, [themeBridgeActions]);
|
|
|
|
const sessionsForShellRef = useRef(session.sessions);
|
|
const sessionsForShell = retainStableSessionsIgnoringPresentation(
|
|
sessionsForShellRef.current,
|
|
session.sessions as never,
|
|
);
|
|
sessionsForShellRef.current = sessionsForShell as typeof session.sessions;
|
|
|
|
const hostById = useMemo(
|
|
() => new Map(vault.hosts.map((host) => [host.id, host])),
|
|
[vault.hosts],
|
|
);
|
|
|
|
const terminalHosts = useMemo(
|
|
() => (
|
|
local.ephemeralHosts.length > 0
|
|
? [...vault.hosts, ...local.ephemeralHosts]
|
|
: vault.hosts
|
|
),
|
|
[local.ephemeralHosts, vault.hosts],
|
|
);
|
|
|
|
const handleDefaultTerminalThemeChange = useCallback((themeId: string) => {
|
|
// Persist the default theme for ephemeral/manual hosts. Mode overrides
|
|
// reset to auto so the chosen theme becomes the new baseline for the
|
|
// current resolved UI mode (same behavior as pre-Host App).
|
|
terminalSettingsActions?.setTerminalThemeId(themeId);
|
|
if (settingsChrome.resolvedTheme === 'dark') {
|
|
terminalSettingsActions?.setTerminalThemeDarkId(TERMINAL_THEME_AUTO);
|
|
} else {
|
|
terminalSettingsActions?.setTerminalThemeLightId(TERMINAL_THEME_AUTO);
|
|
}
|
|
}, [settingsChrome.resolvedTheme, terminalSettingsActions]);
|
|
|
|
const handleFollowAppTerminalThemeChange = useCallback((themeId: string) => {
|
|
pickTerminalTheme(themeId);
|
|
}, [pickTerminalTheme]);
|
|
|
|
const terminalDomain = useMemo(() => {
|
|
if (!handlers) return null;
|
|
return {
|
|
addSessionToWorkspace: sessionActions?.addSessionToWorkspace,
|
|
appendHostToWorkspace: sessionActions?.appendHostToWorkspace,
|
|
appendLocalTerminalToWorkspace: sessionActions?.appendLocalTerminalToWorkspace,
|
|
clearSessionFontSizeOverride: sessionActions?.clearSessionFontSizeOverride,
|
|
closeSession: sessionActions?.closeSession,
|
|
closeTabsBatch: handlers.closeTabsBatch,
|
|
copySessionWithCurrentShell: handlers.copySessionWithCurrentShell,
|
|
copyWorkspaceWithCurrentShell: handlers.copyWorkspaceWithCurrentShell,
|
|
copySessionToNewWindowWithCurrentShell: handlers.copySessionToNewWindowWithCurrentShell,
|
|
duplicateSessionWithCurrentShell: handlers.duplicateSessionWithCurrentShell,
|
|
closeWorkspace: sessionActions?.closeWorkspace,
|
|
createWorkspaceFromSessions: sessionActions?.createWorkspaceFromSessions,
|
|
createWorkspaceFromTargets: handlers.createWorkspaceFromTargets,
|
|
createWorkspaceWithHosts: handlers.createWorkspaceWithHosts,
|
|
currentTerminalTheme,
|
|
draggingSessionId: session.draggingSessionId,
|
|
editorWordWrap: terminalSettings.editorWordWrap,
|
|
followAppTerminalTheme: terminalSettings.followAppTerminalTheme,
|
|
clearThemeIntent,
|
|
settleManualThemeIntent,
|
|
pickTerminalTheme,
|
|
resolveSessionAppearance: resolveFocusedAppearance,
|
|
handleConnectSerial: handlers.handleConnectSerial,
|
|
handleConnectToHost: handlers.handleConnectToHost,
|
|
handleCreateLocalTerminal: handlers.handleCreateLocalTerminal,
|
|
handleDefaultTerminalThemeChange,
|
|
handleFollowAppTerminalThemeChange,
|
|
handleHotkeyAction: handlers.handleHotkeyAction,
|
|
handleSessionStatusChange: handlers.handleSessionStatusChange,
|
|
handleTerminalDataCapture: handlers.handleTerminalDataCapture,
|
|
handleUpdateHostFromTerminal: handlers.handleUpdateHostFromTerminal,
|
|
hostById,
|
|
terminalHosts,
|
|
updateTerminalHosts: handlers.updateTerminalHosts,
|
|
hotkeyScheme: terminalSettings.hotkeyScheme,
|
|
isBroadcastEnabled: sessionActions?.isBroadcastEnabled,
|
|
isGlobalBroadcastEnabled: sessionActions?.isGlobalBroadcastEnabled,
|
|
canUseGlobalBroadcast: sessionActions?.canUseGlobalBroadcast,
|
|
keyBindings: terminalSettings.keyBindings,
|
|
openNoteRequest: local.openNoteRequest,
|
|
portForwardingRules: local.portForwardingRules,
|
|
removeSessionFromWorkspace: sessionActions?.removeSessionFromWorkspace,
|
|
reorderWorkspaceSessions: sessionActions?.reorderWorkspaceSessions,
|
|
runSnippet: handlers.runSnippet,
|
|
sessionLogsDir: terminalSettings.sessionLogsDir,
|
|
sessionLogsEnabled: terminalSettings.sessionLogsEnabled,
|
|
sessionLogsFormat: terminalSettings.sessionLogsFormat,
|
|
sessionLogsTimestampsEnabled: terminalSettings.sessionLogsTimestampsEnabled,
|
|
sessions: sessionsForShell,
|
|
setDraggingSessionId: sessionActions?.setDraggingSessionId,
|
|
setEditorWordWrap: terminalSettingsActions?.setEditorWordWrap,
|
|
setTerminalFontFamilyId: terminalSettingsActions?.setTerminalFontFamilyId,
|
|
setTerminalFontSize: terminalSettingsActions?.setTerminalFontSize,
|
|
setWorkspaceFocusedSession: sessionActions?.setWorkspaceFocusedSession,
|
|
sftpAutoOpenSidebar: terminalSettings.sftpAutoOpenSidebar,
|
|
sftpFollowTerminalCwd: terminalSettings.sftpFollowTerminalCwd,
|
|
setSftpFollowTerminalCwd: terminalSettingsActions?.setSftpFollowTerminalCwd,
|
|
sftpAutoSync: terminalSettings.sftpAutoSync,
|
|
sftpDefaultViewMode: terminalSettings.sftpDefaultViewMode,
|
|
sftpDoubleClickBehavior: terminalSettings.sftpDoubleClickBehavior,
|
|
sftpShowHiddenFiles: terminalSettings.sftpShowHiddenFiles,
|
|
sftpUseCompressedUpload: terminalSettings.sftpUseCompressedUpload,
|
|
splitSessionWithCurrentShell: handlers.splitSessionWithCurrentShell,
|
|
sshDebugLogsEnabled: terminalSettings.sshDebugLogsEnabled,
|
|
terminalFontFamilyId: terminalSettings.terminalFontFamilyId,
|
|
terminalFontSize: terminalSettings.terminalFontSize,
|
|
terminalSettings: terminalSettings.terminalSettings,
|
|
terminalThemeId: terminalSettings.terminalThemeId,
|
|
toggleBroadcast: sessionActions?.toggleBroadcast,
|
|
toggleGlobalBroadcast: sessionActions?.toggleGlobalBroadcast,
|
|
onToggleGlobalBroadcast: sessionActions?.toggleGlobalBroadcast,
|
|
toggleScriptsSidePanelRef: handlers.toggleScriptsSidePanelRef,
|
|
toggleSidePanelRef: handlers.toggleSidePanelRef,
|
|
terminalPaneMagnificationRef: handlers.terminalPaneMagnificationRef,
|
|
sftpPaneMagnificationRef: handlers.sftpPaneMagnificationRef,
|
|
toggleWorkspaceViewMode: sessionActions?.toggleWorkspaceViewMode,
|
|
updateHostDistro: handlers.updateTerminalHostDistro,
|
|
updateSplitSizes: sessionActions?.updateSplitSizes,
|
|
updateSessionFontSize: sessionActions?.updateSessionFontSize,
|
|
updateSessionRestoreCwd: sessionActions?.updateSessionRestoreCwd,
|
|
updateSessionDynamicTitle: sessionActions?.updateSessionDynamicTitle,
|
|
updateSessionCodingCliProvider: sessionActions?.updateSessionCodingCliProvider,
|
|
updateTerminalSetting: terminalSettingsActions?.updateTerminalSetting,
|
|
workspaces: session.workspaces,
|
|
};
|
|
}, [
|
|
clearThemeIntent,
|
|
currentTerminalTheme,
|
|
handleDefaultTerminalThemeChange,
|
|
handleFollowAppTerminalThemeChange,
|
|
handlers,
|
|
hostById,
|
|
local.openNoteRequest,
|
|
local.portForwardingRules,
|
|
pickTerminalTheme,
|
|
resolveFocusedAppearance,
|
|
session.draggingSessionId,
|
|
session.workspaces,
|
|
sessionActions,
|
|
sessionsForShell,
|
|
settleManualThemeIntent,
|
|
terminalHosts,
|
|
terminalSettings,
|
|
terminalSettingsActions,
|
|
]);
|
|
|
|
useLayoutEffect(() => {
|
|
if (terminalDomain) publishAppShellDomainSlice('terminal', terminalDomain);
|
|
}, [terminalDomain]);
|
|
|
|
return null;
|
|
}
|