Address findings from automated security review:
1. Validate rootPath via validatePath/isPathAccessible before recursing,
matching the pattern used by read-file/write-file.
2. Cap query length at 1024 chars to bound regex compilation cost.
3. Reject regexes matching classic ReDoS shapes (nested quantifiers
and similar) before invoking RegExp.
4. Use realpathSync to resolve symlinks and verify containment under
rootPath; drop symlinks that point outside the search root.
5. Cap total files traversed at 10000, in addition to existing
1000-result and 2MB-per-file caps.
Add regression tests for each guard.
CrashReportModal crashed on dumps.map because ipc.crash.read returns
{ ok, data } and the consumer was assigning the wrapper to state. Now
unwraps .data and falls back to [] on error.
ipc.file.search was miswired to ipc.file.pickFile — clicking 'Search'
popped a file picker instead of searching. Add real 'search-in-files'
handler in src/main/files/search-in-files.js with regex support,
case-sensitivity toggle, .git/node_modules/dist skip, 2MB file cap, and
1000-result limit. Wire preload bridge + TS declaration + allowlist.
Also:
- Drop dead ipc.file.open wrapper that was miswired to pickFile
- Rename FileEntry field 'modified' to 'modifiedAt' (number) so the
IPC payload matches the declared type
- Update CrashReportModal tests to use the safeCall envelope shape
The list-directory handler returned { path, entries } while the
renderer typed result.data as FileEntry[]; the file-store had to fall
back to raw.entries via Array.isArray. Extract the entries builder into
src/main/files/list-directory.js so it can be unit-tested, return a
plain array, and skip entries that can't be stat'd (broken symlinks,
permission errors) instead of throwing.
Also tighten jest config so dist/*.snap electron-builder artifacts are
not matched as test suites.
GitOperations.getStatus returns { files: [...] } but the renderer's
ipc.file.gitStatus type declares Array<...> and calls result.data.map().
The mismatch made GitStatusPanel.tsx throw 'n.map is not a function'
on mount, which blanked the entire React tree.
Unwrap result.files in the IPC handler so it returns the array the
renderer expects. Add tests covering the success, empty, and non-git
branches so this regression cannot recur.
Refs: blank-screen-on-md-open
- useMonospaceClasses hook calls electronAPI.monospace.getSettings()
and toggles body.mono-{jetbrains-mono,fira-code} and
body.mono-ligatures-{on,off} classes
- Add --font-mono-active and --font-mono-feature tokens in globals.css
- Apply to code/pre/kbd/samp so all code rendering picks up the active
monospace font + ligature settings
Two related bugs surfaced during end-to-end verification:
1. The 'isAlreadyV5' short-circuit in both the main-side and renderer-side
v4-to-v5 transforms returned the persisted object as-is when a
migration.version=5 marker was present. A previously-shipped v4 build
had written theme: 'ayu-light' (and similar) under the v5 marker;
the transform trusted the marker and passed the invalid theme through,
which then failed the renderer's zod schema on every launch and
reset user settings to defaults.
Fix: in both transforms, when isAlreadyV5 matches, normalize theme
against the v5 enum (light/dark/system) and validate the full result
with the v5 schema before returning. Out-of-range values are replaced
with the v5 default ('system').
2. The renderer's settings-store onRehydrateStorage callback called
useSettingsStore.setState() to reset the store on validation failure.
At that moment the store is still being constructed, and setState
could hit a TDZ ReferenceError (the one we already wrapped in a
try/catch in v5.0.0, which only hid the symptom).
Fix: return the normalized state object from onRehydrateStorage
instead. Zustand's persist middleware applies the returned value
*after* construction completes, so there is no TDZ.
Tests: 334 vitest + 208 jest = 542 passing.
E2E: 12/12 verify-features.mjs steps green; no console errors.
Amit Haridas
1. ThemeSettings: change 'auto' radio value to 'system' to match
the v5 settingsSchema enum. Storing 'auto' silently wipes all
settings on next launch.
2. UpdateBanner: render an 'available' branch so users see a CTA
when a new version is detected (the store already had this state,
but the banner was silent). Added a test.
3. feed-config: remove the unused resolveFeedUrl / FEEDS exports.
feedConfigFor is the actual implementation used by the IPC
handler. Updated tests to match.
4. main/index.js: tighten app:open-external regex to https:// only.
The v5.0.0 React UI shipped with a working surface but had a number of
features left as 'wired in a later phase' or broken by missing IPC
plumbing. This commit completes the wiring end-to-end and proves the
behaviour with the run-desktop driver.
Toolbar format buttons (Toolbar.tsx)
- Bold, Italic, Unordered/Ordered list, Code, Link now dispatch real
commands instead of being permanently disabled.
New module: src/renderer/lib/editor-commands.ts
- Module-level singleton holding the active CodeMirror EditorView.
- toggleBold/Italic/Code/CodeBlock, toggleUnorderedList/OrderedList,
insertLink, setHeadingLevel, scrollToLine, undo, redo, insertSnippet.
- CodeMirrorEditor mounts the view via setActiveView on mount and
nulls it on unmount, so commands land on the focused buffer.
register-menu-commands.ts
- file.confirmClose: prompts before closing a dirty tab via the
confirm modal; otherwise closes silently.
- app.quit: prompts before quitting if any buffer is dirty; uses
the new ipc.app.quit channel.
- short-cuts.show: shows the keyboard shortcut list in a confirm dialog.
- editor.bold/italic/code/list.*/link/undo/redo/heading.*: drive the
active editor.
- find.toggle: dispatches mc:find-toggle for any listening component.
- view.sidebarPanel/bottomPanel: navigate between sidebar sections
via the new data-testid buttons; bottom panel toggles the REPL.
- font.size: increase/decrease/reset on editorFontSize (10..28).
- theme.loadCustomCss/clearCustomCss: pick and clear the custom CSS
path on the settings store.
- template.load: inserts a bundled markdown skeleton at the cursor.
- print.preview/previewStyled: emit mc:print and mc:print-preview.
- file.clearRecent: clears open tabs.
- file.new: creates an untitled buffer and tab.
- editor.gotoHeading: scrolls to a given line (used by Outline and
Breadcrumb).
- view.toggleSidebar: already wired.
- file.opened: already wired (prior fix).
Sidebar Outline + Breadcrumb
- Both now click through to 'editor.gotoHeading' with the line
number extracted from the active buffer.
- Sidebar exposes data-testid for the 'view.sidebarPanel' menu action.
editor-commands sets the active view; scroll metrics flow through to
Minimap which now reads scrollRatio/visibleRatio from the editor.
Export dialogs (PDF/DOCX/HTML)
- Replaced the broken ipc.export.{pdf,docx,html,batch} calls (those
channels were not implemented in main and only CHANNEL_MISSING
was returned) with renderer-side pipelines.
- PDF: generateHtml() with @page CSS for size + margins, then
ipc.print to the main process for native print-to-PDF.
- DOCX: generateDocx() from the existing lib, then ipc.file.writeBuffer.
- HTML: generateHtml() then ipc.file.writeBuffer.
- All three dialogs use ipc.app.showSaveDialog for output path
(new IPC handler) and ipc.file.writeBuffer for the binary write
(new preload channel).
- PrintPreview now uses MarkdownRenderer instead of a raw <pre>.
New settings fields
- editorFontSize: 10..28 px (drives CodeMirror theme font-size)
- customCssPath: string|null (Theme menu wires 'load-custom-css' and
'clear-custom-css' to it).
New IPC channels
- 'app:quit', 'app:open-external', 'app:show-save-dialog',
'write-buffer' (already existed; now exposed in preload).
Tests
- Updated Toolbar test: format buttons are no longer disabled and
dispatch their command ids.
- Updated Export*D*Dialog tests to mock the new ipc surface
(ipc.print, ipc.app.showSaveDialog, ipc.file.writeBuffer) and
assert the new flow.
- Updated PrintPreview test to use ipc.print.doPrint.
- Updated phase8-toasts integration: PDF flow now goes through
ipc.print and asserts 'Sent <title> to printer'.
- register-menu-commands test: re-register 'template.load' AFTER
Harness renders so the captured-args handler is the live one.
Verification
- npm run build:renderer: success (1.6s)
- npx vitest run: 308 passed (up from 305; 3 new + unchanged)
- npx jest: 189 passed (unchanged)
- .claude/skills/run-desktop/verify-features.mjs: drove the live app
via Playwright, exercised every wired feature end-to-end, captured
11 screenshots. All verified working (editor + preview render the
file content, toolbar buttons dispatch, dialogs open, theme
toggles, outline shows headings, italic button works in editor).
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.
- useRegisterMenuCommands: registers file.open, file.save, file.closeTab,
tab.next, tab.prev, view.toggleSidebar, view.togglePreview in command store
- useBridgeNativeMenu: useMenuAction for 13 native-menu channels
(file-save, toggle-preview, load-template-menu, etc.)
- AppShell calls both on mount
- integration test mock extended to include ipc.menu.on
- 145/145 tests pass (was 105)
- bridge between main-process menu events and the command dispatcher
- optional transform: convert raw IPC payload to command args
- extends ipc.ts with ipc.menu.on() helper
- TDD: 5 unit tests covering subscription, payload transform, no-op, unmount
- 'mod+s' / 'mod+shift+s' / 'Tab' / etc.
- mod = meta (Mac) or ctrl (others)
- suppresses when <input>/<textarea>/contentEditable focused
- preventDefault on match
- TDD: 15 unit tests covering parser, match, suppression, unmount
- useCommandStore: register/unregister/registerMany/dispatch/get
- keyed by id (e.g. 'file.open', 'view.toggleSidebar')
- TDD: 10 unit tests covering register, dispatch, unregister, batch
- foundation for Phase 6 native menu + toolbar wiring