Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ coverage/
# Temporary files
*.tmp
.cache/
.worktrees
2 changes: 2 additions & 0 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
15 changes: 14 additions & 1 deletion frontend/src/hooks/useCodeReviewTerminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
});
Expand Down Expand Up @@ -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();
};
Expand Down
116 changes: 116 additions & 0 deletions frontend/src/services/TerminalFontSize.ts
Original file line number Diff line number Diff line change
@@ -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';
Comment on lines +7 to +9
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TerminalFontSize imports terminalPool, while TerminalPool imports getTerminalFontSize from TerminalFontSize, creating a circular module dependency. This is fragile (can yield partially-initialized exports under different import orders/HMR/tests) and also couples the font-size service to the pool implementation. Consider breaking the cycle by having TerminalPool observe terminalFontSize() (e.g., a Solid effect in the pool module) and apply updates to its handles, or by introducing a lightweight callback/subscription interface so TerminalFontSize doesn’t import TerminalPool directly.

Copilot uses AI. Check for mistakes.

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',
});
Comment on lines +11 to +116
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file registers global zoom shortcuts, but there are no unit tests covering the persistence/clamping behavior (8–32px) or that the Cmd/Ctrl shortcuts actually update state. Since the repo already has service-level tests for KeyboardShortcutManager, consider adding tests for TerminalFontSize (e.g., mocking localStorage and verifying increase/decrease/reset + shortcut dispatch update the stored value).

Copilot uses AI. Check for mistakes.
3 changes: 2 additions & 1 deletion frontend/src/services/TerminalPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -127,7 +128,7 @@ class TerminalPool {
*/
private async createTerminalHandle(config: TerminalConfig): Promise<TerminalHandle> {
// Create xterm.js instance with shared configuration
const terminal = createTerminal();
const terminal = createTerminal(undefined, getTerminalFontSize());
const fitAddon = createFitAddon(terminal);

let status: TerminalStatus = 'initializing';
Expand Down
9 changes: 7 additions & 2 deletions packages/shared/src/terminal/terminal-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down