fix(ipc): wire preload bridge so menu Open file actually loads in editor + preview

Two related defects were breaking the 'open file' flow:

1. The BrowserWindow webPreferences had no preload path AND
   contextIsolation: false, so contextBridge.exposeInMainWorld
   threw on load and window.electronAPI was never set. Result:
   every renderer-side ipc.* call returned CHANNEL_MISSING and
   silently no-op'd. Toolbar Open/Save, the file menu, every
   dialog — all broken.

2. The 'file.opened' IPC bridge in register-menu-commands.ts
   dispatched to a 'file.opened' command that was never registered
   as a handler. Even if the preload had worked, the main menu's
   File -> Open would have been a no-op once it reached the
   command store.

Fixes:
- window/index.js: add preload path; set contextIsolation: true
  (required for contextBridge) and nodeIntegration: false
  (renderer no longer needs direct Node access; everything goes
  through the whitelisted preload bridge).
- file-store.ts: add openFileFromMain(path, content) that opens
  a tab + editor buffer from content the main process already read
  (skips the renderer's ipc.file.read since main has the bytes).
  Preserves existing buffer content if the file is already open.
- register-menu-commands.ts: register 'file.opened' to call
  openFileFromMain. Drop the dead 'command.palette' bridge
  (no consumer, no UI, no handler).
- tests: two new cases for openFileFromMain — verifies it does
  not call ipc.file.read and does not clobber user edits on
  re-open.

Verified end-to-end via the run-desktop driver: triggering
file-opened from the main process with a test .md now shows the
content in BOTH the CodeMirror editor and the rendered preview
pane (plus the Outline panel picks up the H1). 497 tests pass.
This commit is contained in:
2026-06-07 22:35:30 +05:30
parent 69afd5fc54
commit 5ef1610873
4 changed files with 64 additions and 3 deletions
+20
View File
@@ -36,6 +36,7 @@ interface FileState {
loadChildren: (dirPath: string) => Promise<void>;
toggleExpanded: (dirPath: string) => void;
openFile: (filePath: string) => Promise<void>;
openFileFromMain: (filePath: string, content: string) => Promise<void>;
closeTab: (id: string) => void;
setActiveTab: (id: string) => void;
markTabClean: (id: string) => void;
@@ -165,6 +166,25 @@ export const useFileStore = create<FileState>()(
});
},
// Used when the main process has already read the file (e.g. file-opened
// IPC from File → Open menu). Skips the renderer-side IPC read since main
// already has the content. Preserves the editor's current buffer content
// if the file is already open.
openFileFromMain: async (filePath, content) => {
const existing = useFileStore.getState().openTabs.find((t) => t.id === filePath);
if (existing) {
set((s) => { s.activeTabId = filePath; });
return;
}
const title = filePath.split('/').pop() ?? filePath;
useEditorStore.getState().openBuffer(filePath, filePath, content);
set((s) => {
s.openTabs.push({ id: filePath, path: filePath, title, dirty: false });
s.activeTabId = filePath;
});
},
closeTab: (id) => {
set((s) => {
const idx = s.openTabs.findIndex((t) => t.id === id);