Skip to content
Closed
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
17 changes: 13 additions & 4 deletions src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,12 +481,21 @@ function createWindow(): void {
const ZOOM_OUT_KEYS = new Set(['-', '_']);
mainWindow.webContents.on('before-input-event', (event, input) => {
if (!mainWindow || mainWindow.isDestroyed()) 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).
// Also prevent Ctrl+Shift+R / Cmd+Shift+R (hard reload).
if ((input.control || input.meta) && input.key.toLowerCase() === 'r') {
// Intercept Ctrl+R / Cmd+R to prevent Chromium's built-in page reload,
// then notify the renderer via IPC so it can refresh the session (fixes #58, #85).
// We must preventDefault here because Chromium handles Ctrl+R at the browser
// engine level, which also blocks the keydown from reaching the renderer —
// hence the IPC bridge.
if ((input.control || input.meta) && !input.shift && input.key.toLowerCase() === 'r') {
event.preventDefault();
mainWindow.webContents.send('session:refresh');
return;
}
// Also block Ctrl+Shift+R (hard reload)
if ((input.control || input.meta) && input.shift && input.key.toLowerCase() === 'r') {
event.preventDefault();
return;
}
Comment on lines +492 to 501

Choose a reason for hiding this comment

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

medium

The logic for handling Ctrl+R and Ctrl+Shift+R can be combined to be more concise and reduce duplication of the condition check. This would improve readability and maintainability.

    if ((input.control || input.meta) && input.key.toLowerCase() === 'r') {
      event.preventDefault();
      if (!input.shift) {
        mainWindow.webContents.send('session:refresh');
      }
      return;
    }

Expand Down
3 changes: 3 additions & 0 deletions src/preload/constants/ipcChannels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,6 @@ export const WINDOW_IS_MAXIMIZED = 'window:isMaximized';

/** Relaunch the application */
export const APP_RELAUNCH = 'app:relaunch';

/** Refresh session shortcut (main → renderer, triggered by Ctrl+R / Cmd+R) */
export const SESSION_REFRESH = 'session:refresh';
10 changes: 10 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
HTTP_SERVER_GET_STATUS,
HTTP_SERVER_START,
HTTP_SERVER_STOP,
SESSION_REFRESH,
SSH_CONNECT,
SSH_DISCONNECT,
SSH_GET_CONFIG_HOSTS,
Expand Down Expand Up @@ -347,6 +348,15 @@ const electronAPI: ElectronAPI = {
};
},

// Session refresh event (Ctrl+R / Cmd+R intercepted by main process)
onSessionRefresh: (callback: () => void): (() => void) => {
const listener = (): void => callback();
ipcRenderer.on(SESSION_REFRESH, listener);
return (): void => {
ipcRenderer.removeListener(SESSION_REFRESH, listener);
};
},

// Shell operations
openPath: (targetPath: string, projectRoot?: string) =>
ipcRenderer.invoke('shell:openPath', targetPath, projectRoot),
Expand Down
5 changes: 5 additions & 0 deletions src/renderer/api/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,11 @@ export class HttpAPIClient implements ElectronAPI {
onTodoChange = (callback: (event: FileChangeEvent) => void): (() => void) =>
this.addEventListener('todo-change', callback);

// No-op in browser mode — Ctrl+R refresh is Electron-only
onSessionRefresh = (_callback: () => void): (() => void) => {
return () => {};
};

// ---------------------------------------------------------------------------
// Shell operations (browser fallbacks)
// ---------------------------------------------------------------------------
Expand Down
9 changes: 9 additions & 0 deletions src/renderer/components/chat/ChatHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,15 @@ export const ChatHistory = ({ tabId }: ChatHistoryProps): JSX.Element => {
checkScrollButton();
}, [conversation, checkScrollButton]);

// Listen for session-refresh-scroll-bottom events (from Ctrl+R / refresh button)
useEffect(() => {
const handler = (): void => {
scrollToBottom('smooth');
};
window.addEventListener('session-refresh-scroll-bottom', handler);
return () => window.removeEventListener('session-refresh-scroll-bottom', handler);
}, [scrollToBottom]);

// Callback to register AI group refs (combines with visibility hook)
const registerAIGroupRefCombined = useCallback(
(groupId: string) => {
Expand Down
12 changes: 11 additions & 1 deletion src/renderer/components/chat/viewers/MarkdownViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
highlightSearchInChildren,
type SearchContext,
} from '../searchHighlightUtils';
import { highlightLine } from '../viewers/syntaxHighlighter';

// =============================================================================
// Types
Expand Down Expand Up @@ -154,9 +155,18 @@ function createViewerMarkdownComponents(searchCtx: SearchContext | null): Compon
const isBlock = (hasLanguage ?? false) || isMultiLine;

if (isBlock) {
const lang = codeClassName?.replace('language-', '') ?? '';
const raw = typeof children === 'string' ? children : '';
const text = raw.replace(/\n$/, '');
const lines = text.split('\n');
return (
<code className="font-mono text-xs" style={{ color: COLOR_TEXT }}>
{hl(children)}
{lines.map((line, i) => (
<React.Fragment key={i}>
{hl(highlightLine(line, lang))}
{i < lines.length - 1 ? '\n' : null}
</React.Fragment>
))}
</code>
);
}
Expand Down
216 changes: 213 additions & 3 deletions src/renderer/components/chat/viewers/syntaxHighlighter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,200 @@ const KEYWORDS: Record<string, Set<string>> = {
'true',
'false',
]),
r: new Set([
'if',
'else',
'for',
'while',
'repeat',
'function',
'return',
'next',
'break',
'in',
'library',
'require',
'source',
'TRUE',
'FALSE',
'NULL',
'NA',
'Inf',
'NaN',
'NA_integer_',
'NA_real_',
'NA_complex_',
'NA_character_',
]),
ruby: new Set([
'def',
'class',
'module',
'end',
'do',
'if',
'elsif',
'else',
'unless',
'while',
'until',
'for',
'in',
'begin',
'rescue',
'ensure',
'raise',
'return',
'yield',
'block_given?',
'require',
'require_relative',
'include',
'extend',
'attr_accessor',
'attr_reader',
'attr_writer',
'self',
'super',
'nil',
'true',
'false',
'and',
'or',
'not',
'then',
'when',
'case',
'lambda',
'proc',
'puts',
'print',
]),
php: new Set([
'function',
'class',
'interface',
'trait',
'extends',
'implements',
'namespace',
'use',
'public',
'private',
'protected',
'static',
'abstract',
'final',
'const',
'var',
'new',
'return',
'if',
'elseif',
'else',
'for',
'foreach',
'while',
'do',
'switch',
'case',
'break',
'continue',
'default',
'try',
'catch',
'finally',
'throw',
'as',
'echo',
'print',
'require',
'require_once',
'include',
'include_once',
'true',
'false',
'null',
'array',
'isset',
'unset',
'empty',
'self',
'this',
]),
sql: new Set([
'SELECT',
'FROM',
'WHERE',
'INSERT',
'INTO',
'UPDATE',
'SET',
'DELETE',
'CREATE',
'ALTER',
'DROP',
'TABLE',
'INDEX',
'VIEW',
'DATABASE',
'JOIN',
'INNER',
'LEFT',
'RIGHT',
'OUTER',
'FULL',
'CROSS',
'ON',
'AND',
'OR',
'NOT',
'IN',
'EXISTS',
'BETWEEN',
'LIKE',
'IS',
'NULL',
'AS',
'ORDER',
'BY',
'GROUP',
'HAVING',
'LIMIT',
'OFFSET',
'UNION',
'ALL',
'DISTINCT',
'COUNT',
'SUM',
'AVG',
'MIN',
'MAX',
'CASE',
'WHEN',
'THEN',
'ELSE',
'END',
'BEGIN',
'COMMIT',
'ROLLBACK',
'TRANSACTION',
'PRIMARY',
'KEY',
'FOREIGN',
'REFERENCES',
'CONSTRAINT',
'DEFAULT',
'VALUES',
'TRUE',
'FALSE',
'INTEGER',
'VARCHAR',
'TEXT',
'BOOLEAN',
'DATE',
'TIMESTAMP',
]),
};

// Extend tsx/jsx to use typescript/javascript keywords
Expand Down Expand Up @@ -296,8 +490,23 @@ export function highlightLine(line: string, language: string): React.ReactNode[]
break;
}

// Check for comment (# style for Python/Shell)
if ((language === 'python' || language === 'bash') && remaining.startsWith('#')) {
// Check for comment (# style for Python/Shell/R/Ruby/PHP)
if (
(language === 'python' || language === 'bash' || language === 'r' || language === 'ruby' || language === 'php') &&
remaining.startsWith('#')
) {
segments.push(
React.createElement(
'span',
{ key: currentPos, style: { color: 'var(--syntax-comment)', fontStyle: 'italic' } },
remaining
)
);
break;
}

// Check for comment (-- style for SQL)
if (language === 'sql' && remaining.startsWith('--')) {
segments.push(
React.createElement(
'span',
Expand Down Expand Up @@ -326,7 +535,8 @@ export function highlightLine(line: string, language: string): React.ReactNode[]
const wordMatch = /^([a-zA-Z_$][a-zA-Z0-9_$]*)/.exec(remaining);
if (wordMatch) {
const word = wordMatch[1];
if (keywords.has(word)) {
// SQL keywords are case-insensitive
if (keywords.has(word) || (language === 'sql' && keywords.has(word.toUpperCase()))) {
segments.push(
React.createElement(
'span',
Expand Down
7 changes: 4 additions & 3 deletions src/renderer/components/layout/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
setSelectedTabIds,
clearTabSelection,
openDashboard,
fetchSessionDetail,
refreshSessionInPlace,
fetchSessions,
unreadCount,
openNotificationsTab,
Expand All @@ -64,7 +64,7 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
setSelectedTabIds: s.setSelectedTabIds,
clearTabSelection: s.clearTabSelection,
openDashboard: s.openDashboard,
fetchSessionDetail: s.fetchSessionDetail,
refreshSessionInPlace: s.refreshSessionInPlace,
fetchSessions: s.fetchSessions,
unreadCount: s.unreadCount,
openNotificationsTab: s.openNotificationsTab,
Expand Down Expand Up @@ -215,9 +215,10 @@ export const TabBar = ({ paneId }: TabBarProps): React.JSX.Element => {
const handleRefresh = async (): Promise<void> => {
if (activeTab?.type === 'session' && activeTab.projectId && activeTab.sessionId) {
await Promise.all([
fetchSessionDetail(activeTab.projectId, activeTab.sessionId, activeTabId ?? undefined),
refreshSessionInPlace(activeTab.projectId, activeTab.sessionId),
fetchSessions(activeTab.projectId),
]);
window.dispatchEvent(new CustomEvent('session-refresh-scroll-bottom'));
}
};

Expand Down
Loading