fix: prevent Ctrl+R page reload and show platform-aware shortcuts (#58)

Intercept Ctrl+R/Cmd+R in before-input-event to prevent Electron's default page reload. Replace hardcoded macOS shortcut symbols with platform-aware helpers that show Ctrl+ on Windows/Linux.
This commit is contained in:
Psypeal Gwai 2026-02-22 05:36:18 -08:00
parent ffa94f5e0f
commit 258dc6d82f
7 changed files with 45 additions and 10 deletions

View file

@ -481,7 +481,16 @@ function createWindow(): void {
const ZOOM_OUT_KEYS = new Set(['-', '_']); const ZOOM_OUT_KEYS = new Set(['-', '_']);
mainWindow.webContents.on('before-input-event', (event, input) => { mainWindow.webContents.on('before-input-event', (event, input) => {
if (!mainWindow || mainWindow.isDestroyed()) return; if (!mainWindow || mainWindow.isDestroyed()) return;
if (!input.meta || input.type !== 'keyDown') return; if (input.type !== 'keyDown') return;
// Prevent Electron's default Ctrl+R / Cmd+R page reload so the renderer
// keyboard handler can use it as "Refresh Session" (fixes #58).
if ((input.control || input.meta) && input.key.toLowerCase() === 'r') {
event.preventDefault();
return;
}
if (!input.meta) return;
const currentLevel = mainWindow.webContents.getZoomLevel(); const currentLevel = mainWindow.webContents.getZoomLevel();

View file

@ -11,6 +11,7 @@ import React, { useEffect, useMemo, useState } from 'react';
import { api } from '@renderer/api'; import { api } from '@renderer/api';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { formatShortcut } from '@renderer/utils/stringUtils';
import { createLogger } from '@shared/utils/logger'; import { createLogger } from '@shared/utils/logger';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
@ -75,7 +76,7 @@ const CommandSearch = ({ value, onChange }: Readonly<CommandSearchProps>): React
<button <button
onClick={() => openCommandPalette()} onClick={() => openCommandPalette()}
className="flex shrink-0 items-center gap-1 transition-opacity hover:opacity-80" className="flex shrink-0 items-center gap-1 transition-opacity hover:opacity-80"
title={selectedProjectId ? 'Search in sessions (⌘K)' : 'Search projects (⌘K)'} title={selectedProjectId ? `Search in sessions (${formatShortcut('K')})` : `Search projects (${formatShortcut('K')})`}
> >
<kbd className="flex h-5 items-center justify-center rounded border border-border bg-surface-overlay px-1.5 text-[10px] font-medium text-text-muted"> <kbd className="flex h-5 items-center justify-center rounded border border-border bg-surface-overlay px-1.5 text-[10px] font-medium text-text-muted">
<Command className="size-2.5" /> <Command className="size-2.5" />

View file

@ -16,7 +16,7 @@ import { useEffect, useRef, useState } from 'react';
import { isElectronMode } from '@renderer/api'; import { isElectronMode } from '@renderer/api';
import { HEADER_ROW1_HEIGHT, HEADER_ROW2_HEIGHT } from '@renderer/constants/layout'; import { HEADER_ROW1_HEIGHT, HEADER_ROW2_HEIGHT } from '@renderer/constants/layout';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { truncateMiddle } from '@renderer/utils/stringUtils'; import { formatShortcut, truncateMiddle } from '@renderer/utils/stringUtils';
import { Check, ChevronDown, GitBranch, PanelLeft } from 'lucide-react'; import { Check, ChevronDown, GitBranch, PanelLeft } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
@ -369,7 +369,7 @@ export const SidebarHeader = (): React.JSX.Element => {
backgroundColor: isCollapseHovered ? 'var(--color-surface-raised)' : 'transparent', backgroundColor: isCollapseHovered ? 'var(--color-surface-raised)' : 'transparent',
} as React.CSSProperties } as React.CSSProperties
} }
title="Collapse sidebar (⌘B)" title={`Collapse sidebar (${formatShortcut('B')})`}
> >
<PanelLeft className="size-4" /> <PanelLeft className="size-4" />
</button> </button>

View file

@ -14,6 +14,7 @@ import { horizontalListSortingStrategy, SortableContext } from '@dnd-kit/sortabl
import { isElectronMode } from '@renderer/api'; import { isElectronMode } from '@renderer/api';
import { HEADER_ROW1_HEIGHT } from '@renderer/constants/layout'; import { HEADER_ROW1_HEIGHT } from '@renderer/constants/layout';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import { formatShortcut } from '@renderer/utils/stringUtils';
import { Bell, PanelLeft, Plus, RefreshCw, Search, Settings } from 'lucide-react'; import { Bell, PanelLeft, Plus, RefreshCw, Search, Settings } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow'; import { useShallow } from 'zustand/react/shallow';
@ -330,7 +331,7 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
onMouseEnter={() => setRefreshHover(true)} onMouseEnter={() => setRefreshHover(true)}
onMouseLeave={() => setRefreshHover(false)} onMouseLeave={() => setRefreshHover(false)}
onClick={handleRefresh} onClick={handleRefresh}
title="Refresh Session (Cmd+R)" title={`Refresh Session (${formatShortcut('R')})`}
> >
<RefreshCw className="size-4" /> <RefreshCw className="size-4" />
</button> </button>
@ -367,7 +368,7 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
color: searchHover ? 'var(--color-text)' : 'var(--color-text-muted)', color: searchHover ? 'var(--color-text)' : 'var(--color-text-muted)',
backgroundColor: searchHover ? 'var(--color-surface-raised)' : 'transparent', backgroundColor: searchHover ? 'var(--color-surface-raised)' : 'transparent',
}} }}
title="Search (Cmd+K)" title={`Search (${formatShortcut('K')})`}
> >
<Search className="size-4" /> <Search className="size-4" />
</button> </button>

View file

@ -7,6 +7,8 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { formatShortcut } from '@renderer/utils/stringUtils';
interface TabContextMenuProps { interface TabContextMenuProps {
x: number; x: number;
y: number; y: number;
@ -100,13 +102,13 @@ export const TabContextMenu = ({
onClick={handleClick(onCloseSelectedTabs)} onClick={handleClick(onCloseSelectedTabs)}
/> />
) : ( ) : (
<MenuItem label="Close Tab" shortcut="⌘W" onClick={handleClick(onCloseTab)} /> <MenuItem label="Close Tab" shortcut={formatShortcut('W')} onClick={handleClick(onCloseTab)} />
)} )}
<MenuItem label="Close Other Tabs" onClick={handleClick(onCloseOtherTabs)} /> <MenuItem label="Close Other Tabs" onClick={handleClick(onCloseOtherTabs)} />
<div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} /> <div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
<MenuItem <MenuItem
label="Split Right" label="Split Right"
shortcut="⌘\" shortcut={formatShortcut('\\')}
onClick={handleClick(onSplitRight)} onClick={handleClick(onSplitRight)}
disabled={disableSplit} disabled={disableSplit}
/> />
@ -127,7 +129,7 @@ export const TabContextMenu = ({
/> />
)} )}
<div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} /> <div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
<MenuItem label="Close All Tabs" shortcut="⇧⌘W" onClick={handleClick(onCloseAllTabs)} /> <MenuItem label="Close All Tabs" shortcut={formatShortcut('W', { shift: true })} onClick={handleClick(onCloseAllTabs)} />
</div> </div>
); );
}; };

View file

@ -7,6 +7,7 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { MAX_PANES } from '@renderer/types/panes'; import { MAX_PANES } from '@renderer/types/panes';
import { formatShortcut } from '@renderer/utils/stringUtils';
import { Check, ClipboardCopy, Eye, EyeOff, Pin, PinOff, Terminal } from 'lucide-react'; import { Check, ClipboardCopy, Eye, EyeOff, Pin, PinOff, Terminal } from 'lucide-react';
interface SessionContextMenuProps { interface SessionContextMenuProps {
@ -98,7 +99,7 @@ export const SessionContextMenu = ({
}} }}
> >
<MenuItem label="Open in Current Pane" onClick={handleClick(onOpenInCurrentPane)} /> <MenuItem label="Open in Current Pane" onClick={handleClick(onOpenInCurrentPane)} />
<MenuItem label="Open in New Tab" shortcut="⌘ Click" onClick={handleClick(onOpenInNewTab)} /> <MenuItem label="Open in New Tab" shortcut={`${formatShortcut('')}Click`} onClick={handleClick(onOpenInNewTab)} />
<div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} /> <div className="mx-2 my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
<MenuItem <MenuItem
label="Split Right and Open" label="Split Right and Open"

View file

@ -2,6 +2,27 @@
* String utilities for display formatting. * String utilities for display formatting.
*/ */
const isMacPlatform =
typeof window !== 'undefined' && window.navigator.userAgent.includes('Macintosh');
/** Returns '⌘' on macOS, 'Ctrl' on Windows/Linux. */
export const modKey = isMacPlatform ? '⌘' : 'Ctrl+';
/** Returns '⇧' on macOS, 'Shift+' on Windows/Linux. */
export const shiftKey = isMacPlatform ? '⇧' : 'Shift+';
/**
* Formats a keyboard shortcut for the current platform.
* @example formatShortcut('R') '⌘R' on Mac, 'Ctrl+R' on Windows/Linux
* @example formatShortcut('W', { shift: true }) '⇧⌘W' on Mac, 'Ctrl+Shift+W' on Windows/Linux
*/
export function formatShortcut(key: string, opts?: { shift?: boolean }): string {
if (opts?.shift) {
return isMacPlatform ? `${shiftKey}${modKey}${key}` : `${modKey}${shiftKey}${key}`;
}
return `${modKey}${key}`;
}
/** /**
* Truncates a string in the middle to preserve both the beginning and end. * Truncates a string in the middle to preserve both the beginning and end.
* Useful for branch names where the unique identifier is often at the end. * Useful for branch names where the unique identifier is often at the end.