diff --git a/.gitignore b/.gitignore index a8fd4f3..8849ce2 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ coverage/ # Temporary files *.tmp .cache/ +.worktrees diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 139177b..195dde0 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -19,6 +19,8 @@ import { agentStatusManager } from './services/AgentStatusManager'; import { hookService } from './services/HookService'; // Eagerly initialize so custom language detection is ready before any file opens import './services/CustomLanguageManager'; +// Register terminal font size zoom shortcuts (Cmd+/-) +import './services/TerminalFontSize'; import { useClipboardFix } from './hooks/useClipboardFix'; import { useGlobalContextMenuFix } from './hooks/useGlobalContextMenuFix'; diff --git a/frontend/src/hooks/useCodeReviewTerminal.ts b/frontend/src/hooks/useCodeReviewTerminal.ts index d133755..1bb8585 100644 --- a/frontend/src/hooks/useCodeReviewTerminal.ts +++ b/frontend/src/hooks/useCodeReviewTerminal.ts @@ -10,6 +10,7 @@ import { Terminal } from '@xterm/xterm'; import { FitAddon } from '@xterm/addon-fit'; import { loadAddons } from '../lib/terminal-utils'; import { spawn, type PtyHandle } from '../services/PortablePty'; +import { getTerminalFontSize, terminalFontSize } from '../services/TerminalFontSize'; import { getAgentSettings, getDefaultAgent } from '../lib/settings-api'; export interface CodeReviewTerminalOptions { @@ -55,7 +56,7 @@ export function useCodeReviewTerminal( foreground: '#e6e6e6', }, fontFamily: 'Menlo, Monaco, "Courier New", monospace', - fontSize: 13, + fontSize: getTerminalFontSize(), cursorBlink: true, allowProposedApi: true, // Required for Unicode11 addon }); @@ -140,6 +141,18 @@ export function useCodeReviewTerminal( } } + // React to global font size changes + createEffect(() => { + const size = terminalFontSize(); + if (terminal) { + terminal.options.fontSize = size; + fitAddon?.fit(); + if (pty && terminal.cols > 0 && terminal.rows > 0) { + pty.resize(terminal.cols, terminal.rows).catch(() => {}); + } + } + }); + const fitTerminal = () => { fitAddon?.fit(); }; diff --git a/frontend/src/services/TerminalFontSize.ts b/frontend/src/services/TerminalFontSize.ts new file mode 100644 index 0000000..63a5e69 --- /dev/null +++ b/frontend/src/services/TerminalFontSize.ts @@ -0,0 +1,116 @@ +/** + * TerminalFontSize - Global terminal font size with Cmd+/- zoom support + * + * Persists the font size in localStorage and updates all active terminals. + */ + +import { createSignal } from 'solid-js'; +import { terminalPool } from './TerminalPool'; +import { keyboardShortcutManager } from './KeyboardShortcutManager'; + +const STORAGE_KEY = 'codelane-terminal-font-size'; +const DEFAULT_FONT_SIZE = 13; +const MIN_FONT_SIZE = 8; +const MAX_FONT_SIZE = 32; +const STEP = 1; + +function loadFontSize(): number { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + const parsed = parseInt(stored, 10); + if (!isNaN(parsed) && parsed >= MIN_FONT_SIZE && parsed <= MAX_FONT_SIZE) { + return parsed; + } + } + } catch { + // localStorage unavailable + } + return DEFAULT_FONT_SIZE; +} + +function saveFontSize(size: number): void { + try { + localStorage.setItem(STORAGE_KEY, String(size)); + } catch { + // localStorage unavailable + } +} + +const [fontSize, setFontSizeSignal] = createSignal(loadFontSize()); + +function applyToAllTerminals(size: number): void { + for (const handle of terminalPool.getAllHandles()) { + handle.terminal.options.fontSize = size; + try { + handle.fitAddon.fit(); + // Resize PTY to match new cols/rows after font size change + terminalPool.resize(handle.id, handle.terminal.cols, handle.terminal.rows).catch(() => {}); + } catch { + // terminal may not be attached yet + } + } +} + +export function getTerminalFontSize(): number { + return fontSize(); +} + +/** Reactive signal accessor for use in SolidJS effects */ +export const terminalFontSize = fontSize; + +export function setTerminalFontSize(size: number): void { + const clamped = Math.max(MIN_FONT_SIZE, Math.min(MAX_FONT_SIZE, size)); + setFontSizeSignal(clamped); + saveFontSize(clamped); + applyToAllTerminals(clamped); +} + +export function increaseTerminalFontSize(): void { + setTerminalFontSize(fontSize() + STEP); +} + +export function decreaseTerminalFontSize(): void { + setTerminalFontSize(fontSize() - STEP); +} + +export function resetTerminalFontSize(): void { + setTerminalFontSize(DEFAULT_FONT_SIZE); +} + +// Register global keyboard shortcuts for terminal zoom +keyboardShortcutManager.register({ + id: 'terminalZoomIn', + description: 'Increase terminal font size', + key: '=', + modifiers: { cmdOrCtrl: true }, + handler: () => increaseTerminalFontSize(), + scope: 'global', +}); + +keyboardShortcutManager.register({ + id: 'terminalZoomInPlus', + description: 'Increase terminal font size (+)', + key: '+', + modifiers: { cmdOrCtrl: true }, + handler: () => increaseTerminalFontSize(), + scope: 'global', +}); + +keyboardShortcutManager.register({ + id: 'terminalZoomOut', + description: 'Decrease terminal font size', + key: '-', + modifiers: { cmdOrCtrl: true }, + handler: () => decreaseTerminalFontSize(), + scope: 'global', +}); + +keyboardShortcutManager.register({ + id: 'terminalZoomReset', + description: 'Reset terminal font size', + key: '0', + modifiers: { cmdOrCtrl: true }, + handler: () => resetTerminalFontSize(), + scope: 'global', +}); diff --git a/frontend/src/services/TerminalPool.ts b/frontend/src/services/TerminalPool.ts index b5ef710..8016f22 100644 --- a/frontend/src/services/TerminalPool.ts +++ b/frontend/src/services/TerminalPool.ts @@ -11,6 +11,7 @@ import { spawn, type PtyHandle } from './PortablePty'; import { getLaneAgentConfig, checkCommandExists } from '../lib/settings-api'; import { createTerminal, createFitAddon, attachKeyHandlers, isTerminalViewportAtBottom } from '../lib/terminal-utils'; +import { getTerminalFontSize } from './TerminalFontSize'; import { agentStatusManager } from './AgentStatusManager'; import type { DetectableAgentType } from '../types/agentStatus'; import type { @@ -127,7 +128,7 @@ class TerminalPool { */ private async createTerminalHandle(config: TerminalConfig): Promise { // Create xterm.js instance with shared configuration - const terminal = createTerminal(); + const terminal = createTerminal(undefined, getTerminalFontSize()); const fitAddon = createFitAddon(terminal); let status: TerminalStatus = 'initializing'; diff --git a/packages/shared/src/terminal/terminal-utils.ts b/packages/shared/src/terminal/terminal-utils.ts index d2fc1a6..1e81d33 100644 --- a/packages/shared/src/terminal/terminal-utils.ts +++ b/packages/shared/src/terminal/terminal-utils.ts @@ -91,12 +91,12 @@ export function isTerminalViewportAtBottom( /** * Creates a pre-configured xterm.js Terminal instance with current theme */ -export function createTerminal(theme: any): Terminal { +export function createTerminal(theme?: any, fontSize: number = 13): Terminal { return new Terminal({ cursorBlink: false, cursorStyle: 'block', fontFamily: 'Menlo, Monaco, "Courier New", monospace', - fontSize: 13, + fontSize, lineHeight: 1.4, allowProposedApi: true, // Required for Unicode11 addon allowTransparency: false, @@ -212,6 +212,11 @@ export function attachKeyHandlers( return false; } + // Cmd/Ctrl+= / Cmd/Ctrl+- / Cmd/Ctrl+0: terminal zoom (handled by KeyboardShortcutManager) + if (isMod && (event.key === '=' || event.key === '+' || event.key === '-' || event.key === '0')) { + return false; + } + // Shift+Enter: Claude Code compatibility if (event.key === 'Enter' && event.shiftKey) { sendShiftEnter();