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
51 changes: 46 additions & 5 deletions main/src/core/importBoundary.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import fs from 'fs';
import path from 'path';
import { describe, expect, it } from 'vitest';
import {
DAEMON_OWNED_CHANNEL_PREFIXES,
DAEMON_OWNED_EXACT_CHANNELS,
ELECTRON_ADAPTER_ONLY_CHANNELS,
} from '../../../shared/types/daemon';

const MAIN_SRC_ROOT = path.resolve(process.cwd(), 'src');

Expand Down Expand Up @@ -46,11 +51,47 @@ describe('daemon/client import boundary', () => {
});

it('keeps the daemon server free of Electron bootstrap imports', () => {
const source = readMainSrcFile('daemon/server.ts');
const daemonBoundaryFiles = [
'daemon/server.ts',
'ipc/daemon.ts',
];

for (const relativePath of daemonBoundaryFiles) {
const source = readMainSrcFile(relativePath);

expect(source, relativePath).not.toContain("from 'electron'");
expect(source, relativePath).not.toContain('mainWindow');
expect(source, relativePath).not.toMatch(/from ['"](?:\.\.\/)+(?:index)['"]/);
expect(source, relativePath).not.toMatch(/from ['"](?:\.\.\/)+(?:index)\.ts['"]/);
}
});

expect(source).not.toContain("from 'electron'");
expect(source).not.toContain('mainWindow');
expect(source).not.toMatch(/from ['"](?:\.\.\/)+(?:index)['"]/);
expect(source).not.toMatch(/from ['"](?:\.\.\/)+(?:index)\.ts['"]/);
it('routes daemon-owned preload invokes through the runtime-safe bridge helper', () => {
const source = readMainSrcFile('preload.ts');

expect(source).toContain('function isDaemonOwnedChannel');
expect(source).toContain('isDaemonOwnedChannel');
expect(source).not.toContain("../../shared/types/daemon");
expect(source).not.toContain("from './daemon/daemonChannels'");
expect(source).toContain("ipcRenderer.invoke('daemon:invoke', channel, ...args)");
expect(source).not.toMatch(
/ipcRenderer\.invoke\('(sessions:|projects:|folders:|prompts:|resource-monitor:|panels:|terminal:|logs:|git:(cancel-status-for-project|clone-repo|commit|execute-project|file-status|get-github-remote|restore|revert)|file:(copy|delete|duplicate|exists|getPath|list|move|read|read-binary|read-project|readAtRevision|rename|resolveAbsolutePath|search|write|write-binary|write-project))/,
);
});

it('keeps preload channel ownership literals aligned with the shared daemon contract', () => {
const source = readMainSrcFile('preload.ts');

for (const prefix of DAEMON_OWNED_CHANNEL_PREFIXES) {
expect(source).toContain(`'${prefix}'`);
}

for (const channel of DAEMON_OWNED_EXACT_CHANNELS) {
expect(source).toContain(`'${channel}'`);
}

for (const channel of ELECTRON_ADAPTER_ONLY_CHANNELS) {
expect(source).toContain(`'${channel}'`);
}
});
});
2 changes: 1 addition & 1 deletion main/src/daemon/commandRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isDaemonOwnedChannel } from '../../../shared/types/daemon';
import { isDaemonOwnedChannel } from './daemonChannels';

export type PaneCommandHandler<TArgs extends unknown[] = unknown[], TResult = unknown> = (
...args: TArgs
Expand Down
6 changes: 6 additions & 0 deletions main/src/daemon/daemonChannels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export {
DAEMON_OWNED_CHANNEL_PREFIXES,
DAEMON_OWNED_EXACT_CHANNELS,
ELECTRON_ADAPTER_ONLY_CHANNELS,
isDaemonOwnedChannel,
} from '../../../shared/types/daemon';
2 changes: 1 addition & 1 deletion main/src/daemon/socketFraming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
PaneDaemonFrameDecoder,
} from './socketFraming';
import {
isDaemonOwnedChannel,
isPaneDaemonEventFrame,
isPaneDaemonFrame,
isPaneDaemonRequestFrame,
Expand All @@ -13,6 +12,7 @@ import {
type PaneDaemonRequestFrame,
type PaneDaemonResponseFrame,
} from '../../../shared/types/daemon';
import { isDaemonOwnedChannel } from './daemonChannels';

describe('Pane daemon framing', () => {
it('encodes frames as newline-delimited JSON', () => {
Expand Down
9 changes: 4 additions & 5 deletions main/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -952,11 +952,6 @@ async function createWindow() {
resourceMonitorService.handleVisibilityChange(true);
});

// Pull-path query so the renderer can get the authoritative focus state on
// mount without waiting for the next focus-change event. Default to true
// (focused) if mainWindow is somehow null at call time.
ipcMain.handle('window:is-focused', () => mainWindow?.isFocused() ?? true);

mainWindow.on('restore', () => {
// Don't assume restore = focused. The OS will fire 'focus' if/when the user
// actually focuses the window; that is what restarts git/resource work.
Expand Down Expand Up @@ -1212,6 +1207,10 @@ app.whenReady().then(async () => {
await initializeServices();
console.log('[Main] Services initialized, creating window...');

// Register before any renderer loads. useNotifications pulls this on mount
// and will race a late registration inside createWindow/loadURL.
ipcMain.handle('window:is-focused', () => mainWindow?.isFocused() ?? true);

// Start the ptyHost supervisor before the window opens so the renderer's
// preload listener for 'ptyHost-port' has a port to receive when the window
// finishes loading. Gated on the `usePtyHost` setting: when off (default),
Expand Down
2 changes: 1 addition & 1 deletion main/src/ipc/daemon.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isDaemonOwnedChannel } from '../../../shared/types/daemon';
import { isDaemonOwnedChannel } from '../daemon/daemonChannels';
import type { PaneCommandRegistry } from '../daemon/commandRegistry';

interface IpcMainHandleLike {
Expand Down
61 changes: 60 additions & 1 deletion main/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type { CreateSessionRequest, Session } from './types/session';
import type { AppConfig, UpdateConfigRequest } from './types/config';
import type { CreateProjectRequest, UpdateProjectRequest, Project } from '../../frontend/src/types/project';
import type { ToolPanel } from '../../shared/types/panels';
import { isDaemonOwnedChannel } from '../../shared/types/daemon';

interface LogEntry {
timestamp: string;
Expand Down Expand Up @@ -88,6 +87,66 @@ interface UpdaterInfo {
size?: number;
}

const DAEMON_OWNED_CHANNEL_PREFIXES = [
'folders:',
'logs:',
'panels:',
'projects:',
'prompts:',
'resource-monitor:',
'sessions:',
'terminal:',
] as const;

const DAEMON_OWNED_EXACT_CHANNELS = [
'git:cancel-status-for-project',
'git:clone-repo',
'git:commit',
'git:execute-project',
'git:file-status',
'git:get-github-remote',
'git:restore',
'git:revert',
'file:copy',
'file:delete',
'file:duplicate',
'file:exists',
'file:getPath',
'file:list',
'file:move',
'file:read',
'file:read-binary',
'file:read-project',
'file:readAtRevision',
'file:rename',
'file:resolveAbsolutePath',
'file:search',
'file:write',
'file:write-binary',
'file:write-project',
] as const;

const ELECTRON_ADAPTER_ONLY_CHANNELS = new Set<string>([
'file:showInFolder',
'sessions:open-ide',
'sessions:set-active-session',
'terminal:clipboard-paste-image',
]);

// Sandboxed Electron preload scripts cannot reliably require local runtime
// modules, so the daemon-owned channel classifier stays inline here.
function isDaemonOwnedChannel(channel: string): boolean {
if (ELECTRON_ADAPTER_ONLY_CHANNELS.has(channel)) {
return false;
}

if (DAEMON_OWNED_EXACT_CHANNELS.includes(channel as (typeof DAEMON_OWNED_EXACT_CHANNELS)[number])) {
return true;
}

return DAEMON_OWNED_CHANNEL_PREFIXES.some((prefix) => channel.startsWith(prefix));
}

// Increase max listeners for ipcRenderer to prevent warnings when many components listen to events
ipcRenderer.setMaxListeners(50);

Expand Down
12 changes: 7 additions & 5 deletions shared/types/daemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ interface PaneDaemonResponseFrameCandidate {
error?: unknown;
}

const DAEMON_OWNED_CHANNEL_PREFIXES = [
export const DAEMON_OWNED_CHANNEL_PREFIXES = [
'folders:',
'logs:',
'panels:',
Expand All @@ -57,7 +57,7 @@ const DAEMON_OWNED_CHANNEL_PREFIXES = [
'terminal:',
] as const;

const DAEMON_OWNED_EXACT_CHANNELS = [
export const DAEMON_OWNED_EXACT_CHANNELS = [
'git:cancel-status-for-project',
'git:clone-repo',
'git:commit',
Expand Down Expand Up @@ -85,15 +85,17 @@ const DAEMON_OWNED_EXACT_CHANNELS = [
'file:write-project',
] as const;

const ELECTRON_ADAPTER_ONLY_CHANNELS = new Set<string>([
export const ELECTRON_ADAPTER_ONLY_CHANNELS = [
'file:showInFolder',
'sessions:open-ide',
'sessions:set-active-session',
'terminal:clipboard-paste-image',
]);
] as const;

const ELECTRON_ADAPTER_ONLY_CHANNEL_SET = new Set<string>(ELECTRON_ADAPTER_ONLY_CHANNELS);

export function isDaemonOwnedChannel(channel: string): boolean {
if (ELECTRON_ADAPTER_ONLY_CHANNELS.has(channel)) {
if (ELECTRON_ADAPTER_ONLY_CHANNEL_SET.has(channel)) {
return false;
}

Expand Down