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.
Every DialogContent had aria-describedby="X-desc" and every
DialogDescription had a matching id="X-desc". That pattern is the
default shadcn/ui recipe, but it breaks Radix 1.1.x.
Root cause: Radix auto-generates a descriptionId via useId() (e.g. :r0:)
and stores it in the Dialog context. The DescriptionWarning effect does
`document.getElementById(descriptionId)` and warns if the element is
missing. When the description element's id is overridden to
"welcome-desc", the DOM has id="welcome-desc" but the context still has
:r0:, so the lookup misses and the warning fires on every dialog open.
Fix: drop both overrides. Let Radix manage the id and aria-describedby
itself. Screen-reader semantics are unchanged (Radix still wires
Content -> Description via the auto-generated id).
Touched: 12 modal files (11 DialogContent/DialogDescription pairs +
SettingsSheet which uses Sheet). +1 regression test in
WelcomeDialog.test.tsx that spies on console.warn and asserts the
"Missing Description" message is never emitted.
Tests: 306/306 pass (was 305).
Long-term memory: radix-aria-describedby-anti-pattern.md captures the
do-not-reintroduce rule for future agents.
The v5.0.0 React UI redesign shipped without a working dev workflow —
`npm start` only ran `electron .` which loaded the source index.html
that references raw .tsx files. The browser couldn't execute them, so
the app rendered an empty #root and the user saw 'lot of errors'.
This commit restores a real dev workflow:
1. **Vite dev server wired into npm scripts** — added `concurrently`
+ `wait-on` as devDeps. `npm run dev` now runs Vite (port 5173)
and Electron together with `wait-on tcp:5173` so Electron only
starts after the dev server is ready.
2. **Main process loads Vite URL in dev, dist/ in prod** — window
module now checks `process.env.VITE_DEV_SERVER_URL` and
`app.isPackaged` to decide what to load. Production still
loads the built `dist/renderer/index.html`.
3. **Fixed ReferenceError: app is not defined** — window module
referenced `app.isPackaged` but only imported `BrowserWindow`.
Added `app` to the destructured import.
4. **Fixed wrong icon path** — window creation used
`../../assets/icon.png` from src/main/window/ which resolved
to `src/assets/icon.png` (doesn't exist). Icon lives at
project root, so path is now `../../../assets/icon.png`.
5. **Allowed Google Fonts in CSP** — the strict CSP
(`font-src 'self' data:`, `connect-src 'self'`) blocked
the preconnect/css link to fonts.googleapis.com / .gstatic.com.
Added both domains so Plus Jakarta Sans loads.
6. **Renamed onLayout → onLayoutChange** in AppShell — react-resizable-panels
v4 renamed the prop. The v1 name was being spread to a DOM
div and React logged 'Unknown event handler property' warnings.
Test mock updated for parity.
7. **show: false → show: true** — ready-to-show was hanging on
this Wayland/container combo. Now the window is shown
immediately on create.
Verified end-to-end:
- 305/305 vitest tests pass
- Vite dev server starts on :5173
- Electron loads localhost:5173 and React mounts
- Header 'MarkdownConverter' + sidebar with 'No files open' visible
- No onLayout warnings after HMR
- All blocked errors resolved