[codex] add terminal font size shortcuts

This commit is contained in:
陈大猫
2026-06-01 15:25:31 +08:00
committed by GitHub
parent ea41389842
commit 63b95bb68e
14 changed files with 246 additions and 6 deletions

View File

@@ -0,0 +1,70 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { handleGlobalHotkeyKeyDownImpl } from './app/AppHandlers.ts';
import { matchesKeyBinding } from '../domain/models.ts';
import { DEFAULT_KEY_BINDINGS } from '../domain/models/keyBindings.ts';
class FakeHTMLElement {
tagName = 'TEXTAREA';
isContentEditable = false;
classList = {
contains: (className: string) => className === 'xterm-helper-textarea',
};
closest(selector: string): FakeHTMLElement | null {
return selector.includes('xterm') ? this : null;
}
hasAttribute(name: string): boolean {
return name === 'data-session-id';
}
}
const previousHTMLElement = globalThis.HTMLElement;
globalThis.HTMLElement = FakeHTMLElement as unknown as typeof HTMLElement;
test.after(() => {
globalThis.HTMLElement = previousHTMLElement;
});
test('global hotkey handler lets terminal font size shortcuts reach xterm', () => {
const target = new FakeHTMLElement();
const handledActions: string[] = [];
let prevented = false;
let stopped = false;
const event = {
key: '=',
code: 'Equal',
ctrlKey: true,
metaKey: false,
altKey: false,
shiftKey: false,
target,
composedPath: () => [target],
preventDefault: () => {
prevented = true;
},
stopPropagation: () => {
stopped = true;
},
} as unknown as KeyboardEvent;
handleGlobalHotkeyKeyDownImpl(
() => ({
HOTKEY_DEBUG: false,
closeTabKeyStr: 'Ctrl + W',
executeHotkeyAction: (action: string) => {
handledActions.push(action);
},
hotkeyScheme: 'pc',
keyBindings: DEFAULT_KEY_BINDINGS,
matchesKeyBinding,
}),
event,
);
assert.deepEqual(handledActions, []);
assert.equal(prevented, false);
assert.equal(stopped, false);
});

View File

@@ -2,8 +2,10 @@
import type React from 'react';
import type { Host, HostProtocol } from '../../types';
import type { PassphraseRequest } from '../../components/PassphraseModal';
import { getTerminalPassthroughActions } from '../state/useGlobalHotkeys';
type AppContextGetter = () => Record<string, any>;
const TERMINAL_PASSTHROUGH_ACTIONS = getTerminalPassthroughActions();
export function handleTrayJumpToSessionImpl(getCtx: AppContextGetter, sessionId: string) {
const { sessions, setActiveTabId, setWorkspaceFocusedSession } = getCtx();
@@ -147,8 +149,7 @@ export function handleGlobalHotkeyKeyDownImpl(getCtx: AppContextGetter, e: Keybo
if (binding.category === 'sftp') {
continue;
}
const terminalActions = ['copy', 'paste', 'pasteSelection', 'selectAll', 'clearBuffer', 'searchTerminal'];
if (terminalActions.includes(binding.action)) {
if (TERMINAL_PASSTHROUGH_ACTIONS.has(binding.action)) {
if (isTerminalElement) {
return;
}

View File

@@ -49,5 +49,8 @@ export const getTerminalPassthroughActions = (): Set<string> => {
'selectAll',
'clearBuffer',
'searchTerminal',
'increaseTerminalFontSize',
'decreaseTerminalFontSize',
'resetTerminalFontSize',
]);
};

View File

@@ -103,6 +103,7 @@ const TerminalComponent: React.FC<TerminalProps> = ({
hotkeyScheme = "disabled",
keyBindings = [],
onHotkeyAction,
onTerminalFontSizeChange,
onStatusChange,
onSessionExit,
onTerminalDataCapture,
@@ -986,7 +987,7 @@ const TerminalComponent: React.FC<TerminalProps> = ({
['--terminal-ui-toolbar-btn-active' as never]: `var(--terminal-preview-toolbar-btn-active, color-mix(in srgb, ${effectiveTheme.colors.cursor} 78%, ${effectiveTheme.colors.background} 22%))`,
}), [effectiveTheme.colors.background, effectiveTheme.colors.cursor, effectiveTheme.colors.foreground]);
useTerminalEffects({ CONNECTION_TIMEOUT, Error, XTERM_PERFORMANCE_CONFIG, applyUserCursorPreference, auth, autocompleteCloseRef, autocompleteInputRef, autocompleteKeyEventRef, captureTerminalLogData, clearTerminalCwd, commandBufferRef, connectionLogBufferRef, containerRef, createPromptLineBreakState, createReplaySafeTerminalLogSanitizer, createXTermRuntime, effectiveFontSize, effectiveFontWeight, effectiveTheme, error, executeSnippetCommand, fitAddonRef, fontFamilyId, fontSize, fontWeightFixupDoneRef, forceSyncRenderAfterResize, handleOsc52ReadRequest, handleTerminalDataCaptureOnce, hasConnectedRef, host, hotkeySchemeRef, identities, inWorkspace, isBroadcastEnabledRef, isFocusMode, isFocused, isLocalConnection, isNetworkDevice, isResizing, isRestoringSelectionRef, isSearchOpen, isSerialConnection, isVisible, isVisibleRef, keyBindingsRef, keys, knownCwdRef, lastFittedSizeRef, lastToastedErrorRef, logger, mouseTrackingRef, onBroadcastInputRef, onCommandExecuted, onHotkeyActionRef, onSnippetShortkeyRef, onSnippetExecutorChange, onTerminalCwdChange, pendingAuthRef, pendingOutputScrollRef, prevIsResizingRef, primaryFontFamily, promptLineBreakStateRef, resizeSession, resolveHostAuth, resolvedFontFamily, safeFit, searchAddonRef, serialConfig, serialLineBufferRef, serializeAddonRef, sessionId, sessionRef, sessionStarters, setError, setHasMouseTracking, setHasSelection, setIsCancelling, setIsDisconnectedDialogDismissed, setIsSearchOpen, setNeedsHostKeyVerification, setPendingHostKeyInfo, setPendingHostKeyRequestId, setProgressLogs, setProgressValue, setShowLogs, setStatus, setTimeLeft, shouldEnableNativeUserInputAutoScroll, shouldProbeSessionCwd, snippetsRef, status, statusRef, t, teardown, termRef, terminalAltKeyOptions, terminalBackend, terminalContextActionsRef, terminalCwdTracker, terminalDataCapturedRef, terminalLogSanitizerRef, terminalSettings, terminalSettingsRef, toHostKeyInfo, toast, updateStatus, useEffect, useLayoutEffect, xtermRuntimeRef, zmodem, zmodemToastedRef });
useTerminalEffects({ CONNECTION_TIMEOUT, Error, XTERM_PERFORMANCE_CONFIG, applyUserCursorPreference, auth, autocompleteCloseRef, autocompleteInputRef, autocompleteKeyEventRef, captureTerminalLogData, clearTerminalCwd, commandBufferRef, connectionLogBufferRef, containerRef, createPromptLineBreakState, createReplaySafeTerminalLogSanitizer, createXTermRuntime, effectiveFontSize, effectiveFontWeight, effectiveTheme, error, executeSnippetCommand, fitAddonRef, fontFamilyId, fontSize, fontWeightFixupDoneRef, forceSyncRenderAfterResize, handleOsc52ReadRequest, handleTerminalDataCaptureOnce, hasConnectedRef, host, hotkeySchemeRef, identities, inWorkspace, isBroadcastEnabledRef, isFocusMode, isFocused, isLocalConnection, isNetworkDevice, isResizing, isRestoringSelectionRef, isSearchOpen, isSerialConnection, isVisible, isVisibleRef, keyBindingsRef, keys, knownCwdRef, lastFittedSizeRef, lastToastedErrorRef, logger, mouseTrackingRef, onBroadcastInputRef, onCommandExecuted, onHotkeyActionRef, onSnippetShortkeyRef, onSnippetExecutorChange, onTerminalCwdChange, onTerminalFontSizeChange, pendingAuthRef, pendingOutputScrollRef, prevIsResizingRef, primaryFontFamily, promptLineBreakStateRef, resizeSession, resolveHostAuth, resolvedFontFamily, safeFit, searchAddonRef, serialConfig, serialLineBufferRef, serializeAddonRef, sessionId, sessionRef, sessionStarters, setError, setHasMouseTracking, setHasSelection, setIsCancelling, setIsDisconnectedDialogDismissed, setIsSearchOpen, setNeedsHostKeyVerification, setPendingHostKeyInfo, setPendingHostKeyRequestId, setProgressLogs, setProgressValue, setShowLogs, setStatus, setTimeLeft, shouldEnableNativeUserInputAutoScroll, shouldProbeSessionCwd, snippetsRef, status, statusRef, t, teardown, termRef, terminalAltKeyOptions, terminalBackend, terminalContextActionsRef, terminalCwdTracker, terminalDataCapturedRef, terminalLogSanitizerRef, terminalSettings, terminalSettingsRef, toHostKeyInfo, toast, updateStatus, useEffect, useLayoutEffect, xtermRuntimeRef, zmodem, zmodemToastedRef });
return <TerminalView ctx={{ ArrowDownToLine, ArrowUpFromLine, Button, Copy, Cpu, HardDrive, HoverCard, HoverCardContent, HoverCardTrigger, Maximize2, MemoryStick, Radio, TerminalAutocomplete, TerminalComposeBar, TerminalConnectionDialog, TerminalContextMenu, TerminalSearchBar, Tooltip, TooltipContent, TooltipTrigger, ZmodemOverwriteDialog, ZmodemProgressIndicator, auth, autocompleteAcceptTextRef, autocompleteCloseRef, autocompleteHostOs, autocompleteInputRef, autocompleteKeyEventRef, autocompleteRepositionRef, autocompleteSettings, chainProgress, cn, containerRef, effectiveTheme, error, executeSnippet, executeSnippetCommand, formatNetSpeed, handleCancelConnect, handleCloseDisconnectedSession, handleCloseSearch, handleDismissDisconnectedDialog, handleDragEnter, handleDragLeave, handleDragOver, handleDrop, handleFindNext, handleFindPrevious, handleHostKeyAddAndContinue, handleHostKeyClose, handleHostKeyContinue, handleOsc52ReadResponse, handleRetry, handleSearch, handleTopOverlayMouseDownCapture, hasMouseTracking, hasSelection, host, hotkeyScheme, inWorkspace, isBroadcastEnabled, isCancelling, isComposeBarOpen, isDraggingOver, isFocusMode, isLocalConnection, isSearchOpen, isVisible, keyBindings, keys, knownCwdRef, needsHostKeyVerification, onBroadcastInput, onCloseSession, onExpandToFocus, onSplitHorizontal, onSplitVertical, onToggleBroadcast, osc52ReadPromptVisible, pendingHostKeyInfo, progressLogs, progressValue, renderControls, scrollToBottomAfterProgrammaticInput, searchMatchCount, serverStats, sessionId, sessionRef, setIsComposeBarOpen, setShowLogs, shouldShowConnectionDialog, showLogs, snippets, status, statusDotTone, t, termRef, terminalBackend, terminalContextActions, terminalCwdTracker, terminalPreviewVars, terminalSettings, timeLeft, toast, zmodem }} />;
};

View File

@@ -481,6 +481,8 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
for (const h of hosts) map.set(h.id, h);
return map;
}, [hosts]);
const hostMapRef = useRef(hostMap);
hostMapRef.current = hostMap;
const proxyProfileIdSet = useMemo(
() => new Set(proxyProfiles.map((profile) => profile.id)),
[proxyProfiles],
@@ -589,6 +591,19 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
}, [sessions, sessionHostsMap, hostMap, groupConfigs, proxyProfileIdSet, proxyProfiles]);
const sessionHostsMapRef = useRef(sessionHostsMap);
sessionHostsMapRef.current = sessionHostsMap;
const handleTerminalFontSizeChange = useCallback((sessionId: string, nextFontSize: number) => {
const sessionHost = sessionHostsMapRef.current.get(sessionId);
if (!sessionHost) return;
const rawHost = hostMapRef.current.get(sessionHost.id);
const usesGlobalFontSize = sessionHost.protocol === 'local' || sessionHost.id?.startsWith('local-') || !rawHost;
if (usesGlobalFontSize) {
onUpdateTerminalFontSize?.(nextFontSize);
return;
}
onUpdateHost({ ...rawHost, fontSize: nextFontSize, fontSizeOverride: true });
}, [onUpdateHost, onUpdateTerminalFontSize]);
const validAIScopeTargetIds = useMemo(() => {
const ids = new Set<string>();
@@ -966,7 +981,7 @@ const TerminalLayerInner: React.FC<TerminalLayerProps> = ({
const prevFocusedSessionIdRef = useRef<string | undefined>(undefined);
useTerminalLayerEffects({ activeSidePanelTab, activeTabId, activeTabIdRef, activeTopTabsThemeId, activeWorkspace, activityTrackedSessions, appliedPreviewSessionRef, applyTerminalPreviewVars, applyTopTabsPreviewVars, cancelAnimationFrame, ChunkedEscapeFilter, clearTerminalPreviewVars, clearTimeout, clearTopTabsPreviewVars, document, dropHint, filterTabsMap, focusedSessionId, followAppTerminalTheme, getSessionActivityIdsToClear, handleOpenAI, handleToggleScriptsSidePanel, handleToggleSidePanel, hasNotifiableTerminalOutput, isFocusMode, isTerminalLayerVisible, lastSidePanelTabRef, Map, Math, onSessionData, onSplitSessionRef, onToggleBroadcastRef, onToggleWorkspaceViewModeRef, onUpdateSplitSizes, prevFocusedSessionIdRef, previewTargetSessionId, requestAnimationFrame, ResizeObserver, resizing, sessionActivityStore, sessions, Set, setDropHint, setResizing, setSftpHostForTab, setSftpInitialLocationForTab, setSftpPendingUploadsForTab, setSidePanelOpenTabs, setThemePreview, setTimeout, setupMcpApprovalBridge, setWorkspaceArea, sftpActiveHost, sftpHostForTab, shouldMarkSessionActivity, sidePanelOpenTabs, splitHorizontalHandlersRef, splitVerticalHandlersRef, terminalRendererCwdBySessionRef, themeCommitTimerRef, themePreview, toggleScriptsSidePanelRef, toggleSidePanelRef, validAIScopeTargetIds, validSessionActivityIds, visibleFocusedThemeId, window, workspaceBroadcastHandlersRef, workspaceFocusHandlersRef, workspaceInnerRef, workspaces });
return <TerminalLayerView ctx={{ accentMode, activeResizers, activeSidePanelTab, activeTabId, activeWorkspace, AIChatPanelsHost, aiContextsByTabId, AIStateMaintenanceHost, AIStateProvider, Array, Button, cn, composeBarThemeColors, computeSplitHint, customAccent, draggingSessionId, dropHint, editorWordWrap, effectiveHosts, findSplitNode, focusedFontFamilyId, focusedFontFamilyOverridden, focusedFontSize, focusedFontSizeOverridden, focusedFontWeight, focusedFontWeightOverridden, focusedSessionId, focusedThemeOverridden, FolderTree, followAppTerminalTheme, fontSize, getTerminalCwd, handleAddKnownHost, handleBroadcastInput, handleCloseSession, handleCloseSidePanel, handleCommandExecuted, handleComposeSend, handleFontFamilyChangeForFocusedSession, handleFontFamilyResetForFocusedSession, handleFontSizeChangeForFocusedSession, handleFontSizeResetForFocusedSession, handleFontWeightChangeForFocusedSession, handleFontWeightResetForFocusedSession, handleOpenAI, handleOpenScripts, handleOpenSftp, handleOpenTheme, handleOsDetected, handlePendingUploadHandled, handleSessionExit, handleSftpInitialLocationApplied, handleSidePanelResizeStart, handleSnippetClickForFocusedSession, handleSnippetFromPanel, handleSnippetExecutorChange, handleStatusChange, handleTerminalCwdChange, handleTerminalDataCapture, handleThemeChangeForFocusedSession, handleThemeResetForFocusedSession, handleToggleSftpFromBar, handleToggleWorkspaceComposeBar, handleUpdateHost, handleWorkspaceDrop, hosts, hotkeyScheme, identities, isBroadcastEnabled, isComposeBarOpen, isFocusMode, isSidePanelOpenForCurrentTab, isTerminalLayerVisible, keyBindings, keys, knownHosts, MessageSquare, mountedAiTabIds, mountedSftpTabIds, onHotkeyAction, onSetWorkspaceFocusedSession, onSplitSession, Palette, PanelLeft, PanelRight, previewedOrVisibleThemeId, refocusActiveTerminalSession, refocusTerminalSession, renderFocusModeSidebar, resizing, resolveAIExecutorContext, resolvedPreviewTheme, ScriptsSidePanel, sessionChainHostsMap, sessionHostsMap, sessionLogConfig, sessions, setDropHint, setEditorWordWrap, setIsComposeBarOpen, setResizing, setSidePanelPosition, sftpActiveHost, sftpAutoSync, sftpDefaultViewMode, sftpDoubleClickBehavior, sftpInitialLocationForTab, sftpPendingUploadsForTab, sftpShowHiddenFiles, SftpSidePanel, sftpUseCompressedUpload, sidePanelPosition, sidePanelWidth, snippetPackages, snippets, splitHorizontalHandlersRef, splitVerticalHandlersRef, t, TerminalComposeBar, terminalFontFamilyId, TerminalPanesHost, terminalSettings, terminalTheme, themePreview, ThemeSidePanel, Tooltip, TooltipContent, TooltipTrigger, updateHosts, validAIScopeTargetIds, workspaceBroadcastHandlersRef, workspaceById, workspaceFocusHandlersRef, workspaceInnerRef, workspaceOuterRef, workspaceOverlayRef, workspaceRectsById, X, Zap }} />;
return <TerminalLayerView ctx={{ accentMode, activeResizers, activeSidePanelTab, activeTabId, activeWorkspace, AIChatPanelsHost, aiContextsByTabId, AIStateMaintenanceHost, AIStateProvider, Array, Button, cn, composeBarThemeColors, computeSplitHint, customAccent, draggingSessionId, dropHint, editorWordWrap, effectiveHosts, findSplitNode, focusedFontFamilyId, focusedFontFamilyOverridden, focusedFontSize, focusedFontSizeOverridden, focusedFontWeight, focusedFontWeightOverridden, focusedSessionId, focusedThemeOverridden, FolderTree, followAppTerminalTheme, fontSize, getTerminalCwd, handleAddKnownHost, handleBroadcastInput, handleCloseSession, handleCloseSidePanel, handleCommandExecuted, handleComposeSend, handleFontFamilyChangeForFocusedSession, handleFontFamilyResetForFocusedSession, handleFontSizeChangeForFocusedSession, handleFontSizeResetForFocusedSession, handleFontWeightChangeForFocusedSession, handleFontWeightResetForFocusedSession, handleOpenAI, handleOpenScripts, handleOpenSftp, handleOpenTheme, handleOsDetected, handlePendingUploadHandled, handleSessionExit, handleSftpInitialLocationApplied, handleSidePanelResizeStart, handleSnippetClickForFocusedSession, handleSnippetFromPanel, handleSnippetExecutorChange, handleStatusChange, handleTerminalCwdChange, handleTerminalDataCapture, handleTerminalFontSizeChange, handleThemeChangeForFocusedSession, handleThemeResetForFocusedSession, handleToggleSftpFromBar, handleToggleWorkspaceComposeBar, handleUpdateHost, handleWorkspaceDrop, hosts, hotkeyScheme, identities, isBroadcastEnabled, isComposeBarOpen, isFocusMode, isSidePanelOpenForCurrentTab, isTerminalLayerVisible, keyBindings, keys, knownHosts, MessageSquare, mountedAiTabIds, mountedSftpTabIds, onHotkeyAction, onSetWorkspaceFocusedSession, onSplitSession, Palette, PanelLeft, PanelRight, previewedOrVisibleThemeId, refocusActiveTerminalSession, refocusTerminalSession, renderFocusModeSidebar, resizing, resolveAIExecutorContext, resolvedPreviewTheme, ScriptsSidePanel, sessionChainHostsMap, sessionHostsMap, sessionLogConfig, sessions, setDropHint, setEditorWordWrap, setIsComposeBarOpen, setResizing, setSidePanelPosition, sftpActiveHost, sftpAutoSync, sftpDefaultViewMode, sftpDoubleClickBehavior, sftpInitialLocationForTab, sftpPendingUploadsForTab, sftpShowHiddenFiles, SftpSidePanel, sftpUseCompressedUpload, sidePanelPosition, sidePanelWidth, snippetPackages, snippets, splitHorizontalHandlersRef, splitVerticalHandlersRef, t, TerminalComposeBar, terminalFontFamilyId, TerminalPanesHost, terminalSettings, terminalTheme, themePreview, ThemeSidePanel, Tooltip, TooltipContent, TooltipTrigger, updateHosts, validAIScopeTargetIds, workspaceBroadcastHandlersRef, workspaceById, workspaceFocusHandlersRef, workspaceInnerRef, workspaceOuterRef, workspaceOverlayRef, workspaceRectsById, X, Zap }} />;
};
export const TerminalLayer = memo(TerminalLayerInner, terminalLayerAreEqual);

View File

@@ -47,6 +47,10 @@ import { terminalAltKeyOptions } from "./altKeyOptions";
import { optionArrowWordJumpSequence } from "./optionArrowWordJump";
import { watchDevicePixelRatio } from "./rendererDprWatch";
import { handleSerialLineModeInput } from "./serialLineInput";
import {
nextTerminalFontSizeForAction,
nextTerminalFontSizeForWheel,
} from "./terminalFontZoom";
import {
markExpectedTerminalCursorPositionReport,
pasteTextIntoTerminal,
@@ -106,6 +110,7 @@ export type CreateXTermRuntimeContext = {
onHotkeyActionRef: RefObject<
((action: string, event: KeyboardEvent) => void) | undefined
>;
onTerminalFontSizeChange?: (fontSize: number) => void;
isBroadcastEnabledRef: RefObject<boolean | undefined>;
onBroadcastInputRef: RefObject<
@@ -489,6 +494,39 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
term.scrollToBottom();
}
};
const currentTerminalFontSize = () => {
const optionFontSize = term.options.fontSize;
return typeof optionFontSize === "number" ? optionFontSize : effectiveFontSize;
};
const applyTerminalFontSize = (nextFontSize: number | null): boolean => {
if (nextFontSize === null) return false;
const currentFontSize = currentTerminalFontSize();
if (nextFontSize !== currentFontSize) {
term.options.fontSize = nextFontSize;
clearWebglTextureAtlas();
try {
fitAddon.fit();
} catch (err) {
logger.warn("[XTerm] fit after font size change failed", err);
}
ctx.onTerminalFontSizeChange?.(nextFontSize);
}
return true;
};
const handleFontSizeWheel = (event: WheelEvent) => {
const currentScheme = ctx.hotkeySchemeRef.current;
const isMac = currentScheme === "mac" || (currentScheme === "disabled" && isMacPlatform());
const nextFontSize = nextTerminalFontSizeForWheel(
event,
currentTerminalFontSize(),
isMac,
);
if (nextFontSize === null) return;
event.preventDefault();
event.stopPropagation();
applyTerminalFontSize(nextFontSize);
};
ctx.container.addEventListener("wheel", handleFontSizeWheel, { passive: false });
term.attachCustomKeyEventHandler((e: KeyboardEvent) => {
// Preserve mouse selection across keystrokes when enabled. xterm.js
@@ -621,6 +659,14 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
ctx.setIsSearchOpen(true);
break;
}
case "increaseTerminalFontSize":
case "decreaseTerminalFontSize":
case "resetTerminalFontSize": {
applyTerminalFontSize(
nextTerminalFontSizeForAction(action, currentTerminalFontSize()),
);
break;
}
}
return false;
}
@@ -938,6 +984,7 @@ export const createXTermRuntime = (ctx: CreateXTermRuntimeContext): XTermRuntime
keywordHighlighter,
clearTextureAtlas: clearWebglTextureAtlas,
dispose: () => {
ctx.container.removeEventListener("wheel", handleFontSizeWheel);
cleanupMiddleClick?.();
stopDprWatch();
keywordHighlighter.dispose();

View File

@@ -0,0 +1,27 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
nextTerminalFontSizeForAction,
nextTerminalFontSizeForWheel,
} from './terminalFontZoom.ts';
test('terminal font size actions step and reset within bounds', () => {
assert.equal(nextTerminalFontSizeForAction('increaseTerminalFontSize', 14), 15);
assert.equal(nextTerminalFontSizeForAction('decreaseTerminalFontSize', 14), 13);
assert.equal(nextTerminalFontSizeForAction('resetTerminalFontSize', 18), 14);
assert.equal(nextTerminalFontSizeForAction('increaseTerminalFontSize', 32), 32);
assert.equal(nextTerminalFontSizeForAction('decreaseTerminalFontSize', 10), 10);
assert.equal(nextTerminalFontSizeForAction('copy', 14), null);
});
test('wheel adjusts terminal font size with the platform modifier only', () => {
assert.equal(nextTerminalFontSizeForWheel({ ctrlKey: true, metaKey: false, deltaY: -1 }, 14, false), 15);
assert.equal(nextTerminalFontSizeForWheel({ ctrlKey: true, metaKey: false, deltaY: 1 }, 14, false), 13);
assert.equal(nextTerminalFontSizeForWheel({ ctrlKey: false, metaKey: true, deltaY: -1 }, 14, true), 15);
assert.equal(nextTerminalFontSizeForWheel({ ctrlKey: false, metaKey: true, deltaY: 1 }, 14, true), 13);
assert.equal(nextTerminalFontSizeForWheel({ ctrlKey: false, metaKey: true, deltaY: -1 }, 14, false), null);
assert.equal(nextTerminalFontSizeForWheel({ ctrlKey: true, metaKey: false, deltaY: -1 }, 14, true), null);
assert.equal(nextTerminalFontSizeForWheel({ ctrlKey: false, metaKey: false, deltaY: -1 }, 14, false), null);
assert.equal(nextTerminalFontSizeForWheel({ ctrlKey: true, metaKey: false, deltaY: 0 }, 14, false), null);
});

View File

@@ -0,0 +1,38 @@
import {
DEFAULT_FONT_SIZE,
MAX_FONT_SIZE,
MIN_FONT_SIZE,
} from "../../../infrastructure/config/fonts";
type WheelLike = Pick<WheelEvent, "ctrlKey" | "metaKey" | "deltaY">;
export const clampTerminalFontSize = (fontSize: number): number =>
Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, fontSize));
export const nextTerminalFontSizeForAction = (
action: string,
currentFontSize: number,
): number | null => {
switch (action) {
case "increaseTerminalFontSize":
return clampTerminalFontSize(currentFontSize + 1);
case "decreaseTerminalFontSize":
return clampTerminalFontSize(currentFontSize - 1);
case "resetTerminalFontSize":
return DEFAULT_FONT_SIZE;
default:
return null;
}
};
export const nextTerminalFontSizeForWheel = (
event: WheelLike,
currentFontSize: number,
isMac: boolean,
): number | null => {
const hasZoomModifier = isMac
? event.metaKey && !event.ctrlKey
: event.ctrlKey && !event.metaKey;
if (!hasZoomModifier || event.deltaY === 0) return null;
return clampTerminalFontSize(currentFontSize + (event.deltaY < 0 ? 1 : -1));
};

View File

@@ -89,6 +89,7 @@ export interface TerminalProps {
hotkeyScheme?: "disabled" | "mac" | "pc";
keyBindings?: KeyBinding[];
onHotkeyAction?: (action: string, event: KeyboardEvent) => void;
onTerminalFontSizeChange?: (fontSize: number) => void;
onStatusChange?: (sessionId: string, status: TerminalSession["status"]) => void;
onSessionExit?: (sessionId: string, evt: { exitCode?: number; signal?: number; error?: string; reason?: "exited" | "error" | "timeout" | "closed" }) => void;
onTerminalDataCapture?: (sessionId: string, data: string) => void;

View File

@@ -3,7 +3,7 @@
type TerminalEffectsContext = Record<string, any>;
export function useTerminalEffects(ctx: TerminalEffectsContext) {
const { CONNECTION_TIMEOUT, Error, XTERM_PERFORMANCE_CONFIG, applyUserCursorPreference, auth, autocompleteCloseRef, autocompleteInputRef, autocompleteKeyEventRef, captureTerminalLogData, clearTerminalCwd, commandBufferRef, connectionLogBufferRef, containerRef, createPromptLineBreakState, createReplaySafeTerminalLogSanitizer, createXTermRuntime, effectiveFontSize, effectiveFontWeight, effectiveTheme, error, executeSnippetCommand, fitAddonRef, fontFamilyId, fontSize, fontWeightFixupDoneRef, forceSyncRenderAfterResize, handleOsc52ReadRequest, handleTerminalDataCaptureOnce, hasConnectedRef, host, hotkeySchemeRef, identities, inWorkspace, isBroadcastEnabledRef, isFocusMode, isFocused, isLocalConnection, isNetworkDevice, isResizing, isRestoringSelectionRef, isSearchOpen, isSerialConnection, isVisible, isVisibleRef, keyBindingsRef, keys, knownCwdRef, lastFittedSizeRef, lastToastedErrorRef, logger, mouseTrackingRef, onBroadcastInputRef, onCommandExecuted, onHotkeyActionRef, onSnippetExecutorChange, onTerminalCwdChange, pendingAuthRef, pendingOutputScrollRef, prevIsResizingRef, primaryFontFamily, promptLineBreakStateRef, resizeSession, resolveHostAuth, resolvedFontFamily, safeFit, searchAddonRef, serialConfig, serialLineBufferRef, serializeAddonRef, sessionId, sessionRef, sessionStarters, setError, setHasMouseTracking, setHasSelection, setIsCancelling, setIsDisconnectedDialogDismissed, setIsSearchOpen, setNeedsHostKeyVerification, setPendingHostKeyInfo, setPendingHostKeyRequestId, setProgressLogs, setProgressValue, setShowLogs, setStatus, setTimeLeft, shouldEnableNativeUserInputAutoScroll, shouldProbeSessionCwd, onSnippetShortkeyRef, snippetsRef, status, statusRef, t, teardown, termRef, terminalAltKeyOptions, terminalBackend, terminalContextActionsRef, terminalCwdTracker, terminalDataCapturedRef, terminalLogSanitizerRef, terminalSettings, terminalSettingsRef, toHostKeyInfo, toast, updateStatus, useEffect, useLayoutEffect, xtermRuntimeRef, zmodem, zmodemToastedRef } = ctx;
const { CONNECTION_TIMEOUT, Error, XTERM_PERFORMANCE_CONFIG, applyUserCursorPreference, auth, autocompleteCloseRef, autocompleteInputRef, autocompleteKeyEventRef, captureTerminalLogData, clearTerminalCwd, commandBufferRef, connectionLogBufferRef, containerRef, createPromptLineBreakState, createReplaySafeTerminalLogSanitizer, createXTermRuntime, effectiveFontSize, effectiveFontWeight, effectiveTheme, error, executeSnippetCommand, fitAddonRef, fontFamilyId, fontSize, fontWeightFixupDoneRef, forceSyncRenderAfterResize, handleOsc52ReadRequest, handleTerminalDataCaptureOnce, hasConnectedRef, host, hotkeySchemeRef, identities, inWorkspace, isBroadcastEnabledRef, isFocusMode, isFocused, isLocalConnection, isNetworkDevice, isResizing, isRestoringSelectionRef, isSearchOpen, isSerialConnection, isVisible, isVisibleRef, keyBindingsRef, keys, knownCwdRef, lastFittedSizeRef, lastToastedErrorRef, logger, mouseTrackingRef, onBroadcastInputRef, onCommandExecuted, onHotkeyActionRef, onSnippetExecutorChange, onTerminalCwdChange, onTerminalFontSizeChange, pendingAuthRef, pendingOutputScrollRef, prevIsResizingRef, primaryFontFamily, promptLineBreakStateRef, resizeSession, resolveHostAuth, resolvedFontFamily, safeFit, searchAddonRef, serialConfig, serialLineBufferRef, serializeAddonRef, sessionId, sessionRef, sessionStarters, setError, setHasMouseTracking, setHasSelection, setIsCancelling, setIsDisconnectedDialogDismissed, setIsSearchOpen, setNeedsHostKeyVerification, setPendingHostKeyInfo, setPendingHostKeyRequestId, setProgressLogs, setProgressValue, setShowLogs, setStatus, setTimeLeft, shouldEnableNativeUserInputAutoScroll, shouldProbeSessionCwd, onSnippetShortkeyRef, snippetsRef, status, statusRef, t, teardown, termRef, terminalAltKeyOptions, terminalBackend, terminalContextActionsRef, terminalCwdTracker, terminalDataCapturedRef, terminalLogSanitizerRef, terminalSettings, terminalSettingsRef, toHostKeyInfo, toast, updateStatus, useEffect, useLayoutEffect, xtermRuntimeRef, zmodem, zmodemToastedRef } = ctx;
useEffect(() => {
@@ -186,6 +186,7 @@ export function useTerminalEffects(ctx: TerminalEffectsContext) {
hotkeySchemeRef,
keyBindingsRef,
onHotkeyActionRef,
onTerminalFontSizeChange,
isBroadcastEnabledRef,
onBroadcastInputRef,
snippetsRef,

View File

@@ -450,6 +450,7 @@ interface TerminalPaneProps {
isComposeBarOpen: boolean;
sessionLog?: { enabled: true; directory: string; format: string };
onHotkeyAction?: (action: string, event: KeyboardEvent) => void;
onTerminalFontSizeChange?: (sessionId: string, fontSize: number) => void;
onOpenSftp: (
host: Host,
initialPath?: string,
@@ -516,6 +517,7 @@ const terminalPanePropsAreEqual = (
prev.isComposeBarOpen === next.isComposeBarOpen &&
prev.sessionLog === next.sessionLog &&
prev.onHotkeyAction === next.onHotkeyAction &&
prev.onTerminalFontSizeChange === next.onTerminalFontSizeChange &&
prev.onOpenSftp === next.onOpenSftp &&
prev.onTerminalCwdChange === next.onTerminalCwdChange &&
prev.onOpenScripts === next.onOpenScripts &&
@@ -565,6 +567,7 @@ const TerminalPane: React.FC<TerminalPaneProps> = memo(({
isComposeBarOpen,
sessionLog,
onHotkeyAction,
onTerminalFontSizeChange,
onOpenSftp,
onTerminalCwdChange,
onOpenScripts,
@@ -641,6 +644,9 @@ const TerminalPane: React.FC<TerminalPaneProps> = memo(({
onSetWorkspaceFocusedSession?.(activeWorkspaceId, session.id);
}
}, [activeWorkspaceId, isFocusMode, onSetWorkspaceFocusedSession, session.id]);
const handleTerminalFontSizeChange = useCallback((nextFontSize: number) => {
onTerminalFontSizeChange?.(session.id, nextFontSize);
}, [onTerminalFontSizeChange, session.id]);
return (
<div
@@ -681,6 +687,7 @@ const TerminalPane: React.FC<TerminalPaneProps> = memo(({
hotkeyScheme={hotkeyScheme}
keyBindings={keyBindings}
onHotkeyAction={onHotkeyAction}
onTerminalFontSizeChange={handleTerminalFontSizeChange}
onOpenSftp={onOpenSftp}
onTerminalCwdChange={onTerminalCwdChange}
onOpenScripts={onOpenScripts}
@@ -738,6 +745,7 @@ interface TerminalPanesHostProps {
isComposeBarOpen: boolean;
sessionLog?: { enabled: true; directory: string; format: string };
onHotkeyAction?: (action: string, event: KeyboardEvent) => void;
onTerminalFontSizeChange?: TerminalPaneProps['onTerminalFontSizeChange'];
onOpenSftp: TerminalPaneProps['onOpenSftp'];
onTerminalCwdChange: TerminalPaneProps['onTerminalCwdChange'];
onOpenScripts: () => void;

View File

@@ -4,7 +4,7 @@ import React from 'react';
type TerminalLayerViewContext = Record<string, any>;
export function TerminalLayerView({ ctx }: { ctx: TerminalLayerViewContext }) {
const { accentMode, activeResizers, activeSidePanelTab, activeTabId, activeWorkspace, AIChatPanelsHost, aiContextsByTabId, AIStateMaintenanceHost, AIStateProvider, Array, Button, cn, composeBarThemeColors, computeSplitHint, customAccent, draggingSessionId, dropHint, editorWordWrap, effectiveHosts, findSplitNode, focusedFontFamilyId, focusedFontFamilyOverridden, focusedFontSize, focusedFontSizeOverridden, focusedFontWeight, focusedFontWeightOverridden, focusedSessionId, focusedThemeOverridden, FolderTree, followAppTerminalTheme, fontSize, getTerminalCwd, handleAddKnownHost, handleBroadcastInput, handleCloseSession, handleCloseSidePanel, handleCommandExecuted, handleComposeSend, handleFontFamilyChangeForFocusedSession, handleFontFamilyResetForFocusedSession, handleFontSizeChangeForFocusedSession, handleFontSizeResetForFocusedSession, handleFontWeightChangeForFocusedSession, handleFontWeightResetForFocusedSession, handleOpenAI, handleOpenScripts, handleOpenSftp, handleOpenTheme, handleOsDetected, handlePendingUploadHandled, handleSessionExit, handleSftpInitialLocationApplied, handleSidePanelResizeStart, handleSnippetFromPanel, handleSnippetExecutorChange, handleStatusChange, handleTerminalCwdChange, handleTerminalDataCapture, handleThemeChangeForFocusedSession, handleThemeResetForFocusedSession, handleToggleSftpFromBar, handleToggleWorkspaceComposeBar, handleUpdateHost, handleWorkspaceDrop, hosts, hotkeyScheme, identities, isBroadcastEnabled, isComposeBarOpen, isFocusMode, isSidePanelOpenForCurrentTab, isTerminalLayerVisible, keyBindings, keys, knownHosts, MessageSquare, mountedAiTabIds, mountedSftpTabIds, onHotkeyAction, onSetWorkspaceFocusedSession, onSplitSession, Palette, PanelLeft, PanelRight, previewedOrVisibleThemeId, refocusActiveTerminalSession, refocusTerminalSession, renderFocusModeSidebar, resizing, resolveAIExecutorContext, resolvedPreviewTheme, ScriptsSidePanel, sessionChainHostsMap, sessionHostsMap, sessionLogConfig, sessions, setDropHint, setEditorWordWrap, setIsComposeBarOpen, setResizing, setSidePanelPosition, sftpActiveHost, sftpAutoSync, sftpDefaultViewMode, sftpDoubleClickBehavior, sftpInitialLocationForTab, sftpPendingUploadsForTab, sftpShowHiddenFiles, SftpSidePanel, sftpUseCompressedUpload, sidePanelPosition, sidePanelWidth, snippetPackages, snippets, splitHorizontalHandlersRef, splitVerticalHandlersRef, t, TerminalComposeBar, terminalFontFamilyId, TerminalPanesHost, terminalSettings, terminalTheme, themePreview, ThemeSidePanel, Tooltip, TooltipContent, TooltipTrigger, updateHosts, validAIScopeTargetIds, workspaceBroadcastHandlersRef, workspaceById, workspaceFocusHandlersRef, workspaceInnerRef, workspaceOuterRef, workspaceOverlayRef, workspaceRectsById, X, Zap } = ctx;
const { accentMode, activeResizers, activeSidePanelTab, activeTabId, activeWorkspace, AIChatPanelsHost, aiContextsByTabId, AIStateMaintenanceHost, AIStateProvider, Array, Button, cn, composeBarThemeColors, computeSplitHint, customAccent, draggingSessionId, dropHint, editorWordWrap, effectiveHosts, findSplitNode, focusedFontFamilyId, focusedFontFamilyOverridden, focusedFontSize, focusedFontSizeOverridden, focusedFontWeight, focusedFontWeightOverridden, focusedSessionId, focusedThemeOverridden, FolderTree, followAppTerminalTheme, fontSize, getTerminalCwd, handleAddKnownHost, handleBroadcastInput, handleCloseSession, handleCloseSidePanel, handleCommandExecuted, handleComposeSend, handleFontFamilyChangeForFocusedSession, handleFontFamilyResetForFocusedSession, handleFontSizeChangeForFocusedSession, handleFontSizeResetForFocusedSession, handleFontWeightChangeForFocusedSession, handleFontWeightResetForFocusedSession, handleOpenAI, handleOpenScripts, handleOpenSftp, handleOpenTheme, handleOsDetected, handlePendingUploadHandled, handleSessionExit, handleSftpInitialLocationApplied, handleSidePanelResizeStart, handleSnippetFromPanel, handleSnippetExecutorChange, handleStatusChange, handleTerminalCwdChange, handleTerminalDataCapture, handleTerminalFontSizeChange, handleThemeChangeForFocusedSession, handleThemeResetForFocusedSession, handleToggleSftpFromBar, handleToggleWorkspaceComposeBar, handleUpdateHost, handleWorkspaceDrop, hosts, hotkeyScheme, identities, isBroadcastEnabled, isComposeBarOpen, isFocusMode, isSidePanelOpenForCurrentTab, isTerminalLayerVisible, keyBindings, keys, knownHosts, MessageSquare, mountedAiTabIds, mountedSftpTabIds, onHotkeyAction, onSetWorkspaceFocusedSession, onSplitSession, Palette, PanelLeft, PanelRight, previewedOrVisibleThemeId, refocusActiveTerminalSession, refocusTerminalSession, renderFocusModeSidebar, resizing, resolveAIExecutorContext, resolvedPreviewTheme, ScriptsSidePanel, sessionChainHostsMap, sessionHostsMap, sessionLogConfig, sessions, setDropHint, setEditorWordWrap, setIsComposeBarOpen, setResizing, setSidePanelPosition, sftpActiveHost, sftpAutoSync, sftpDefaultViewMode, sftpDoubleClickBehavior, sftpInitialLocationForTab, sftpPendingUploadsForTab, sftpShowHiddenFiles, SftpSidePanel, sftpUseCompressedUpload, sidePanelPosition, sidePanelWidth, snippetPackages, snippets, splitHorizontalHandlersRef, splitVerticalHandlersRef, t, TerminalComposeBar, terminalFontFamilyId, TerminalPanesHost, terminalSettings, terminalTheme, themePreview, ThemeSidePanel, Tooltip, TooltipContent, TooltipTrigger, updateHosts, validAIScopeTargetIds, workspaceBroadcastHandlersRef, workspaceById, workspaceFocusHandlersRef, workspaceInnerRef, workspaceOuterRef, workspaceOverlayRef, workspaceRectsById, X, Zap } = ctx;
return (
<AIStateProvider>
<AIStateMaintenanceHost validAIScopeTargetIds={validAIScopeTargetIds} />
@@ -361,6 +361,7 @@ export function TerminalLayerView({ ctx }: { ctx: TerminalLayerViewContext }) {
isComposeBarOpen={isComposeBarOpen}
sessionLog={sessionLogConfig}
onHotkeyAction={onHotkeyAction}
onTerminalFontSizeChange={handleTerminalFontSizeChange}
onOpenSftp={handleOpenSftp}
onTerminalCwdChange={handleTerminalCwdChange}
onOpenScripts={handleOpenScripts}

View File

@@ -0,0 +1,24 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { DEFAULT_KEY_BINDINGS } from './models/keyBindings.ts';
import { getTerminalPassthroughActions } from '../application/state/useGlobalHotkeys.ts';
test('default shortcuts include terminal font size controls', () => {
const byAction = new Map(DEFAULT_KEY_BINDINGS.map((binding) => [binding.action, binding]));
assert.equal(byAction.get('increaseTerminalFontSize')?.pc, 'Ctrl + =');
assert.equal(byAction.get('decreaseTerminalFontSize')?.pc, 'Ctrl + -');
assert.equal(byAction.get('resetTerminalFontSize')?.pc, 'Ctrl + 0');
assert.equal(byAction.get('increaseTerminalFontSize')?.category, 'terminal');
assert.equal(byAction.get('decreaseTerminalFontSize')?.category, 'terminal');
assert.equal(byAction.get('resetTerminalFontSize')?.category, 'terminal');
});
test('terminal font size shortcuts are handled inside xterm', () => {
const actions = getTerminalPassthroughActions();
assert.equal(actions.has('increaseTerminalFontSize'), true);
assert.equal(actions.has('decreaseTerminalFontSize'), true);
assert.equal(actions.has('resetTerminalFontSize'), true);
});

View File

@@ -206,6 +206,9 @@ export const DEFAULT_KEY_BINDINGS: KeyBinding[] = [
{ 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' },