Port master's v4.5.0 monospace font embedding feature (JetBrains Mono +
Fira Code TTF assets, embedded into PDF/DOCX/EPUB/HTML exports) and
rename branch identity to Markdown Converter React so dev build coexists
with the installed markdown-converter deb without appId/productName
collisions.
Amit Haridas
- batch.showConverter: open BatchMediaConverterDialog for image/audio/video
types (uses existing universal-convert-batch handler with ImageMagick/FFmpeg)
- tools.documentCompare: line-by-line diff of two open tabs with confirmation
dialog, shows diff count in toast and full diff in console
- No more 'coming soon' toasts for any menu item
simple-git returns objects with Map/Set fields that cannot be cloned
over Electron IPC. Flatten all git operation results into plain
JSON-serializable objects to fix 'An object could not be cloned' error.
- App.tsx: call electronAPI.file.rendererReady() on mount so main process
opens pendingFile (files passed via CLI or file associations)
- FindReplaceBar: move updateMatchCount declaration before executeCommand
to fix 'Cannot access d before initialization' crash in minified build
(executeCommand referenced updateMatchCount via closure before its
const declaration in the same scope)
The main process list-directory handler returns {path, entries} but
the file store was calling .map() directly on result.data. Since
arrays have a .entries iterator method, the nullish coalescing
fallback didn't work. Now properly extracts the entries array with
Array.isArray check.
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.
- verify-features.mjs: dismiss FirstRunWizard if visible before clicking
toolbar (wizard overlay blocked previous toolbar clicks); write a
firstRun=true settings.json to test wizard visibility; emit
'updater:status' from main via Playwright IPC.
- package.json: dev:electron now passes --no-sandbox to match the
.claude/skills/run-desktop driver scripts.
Brainstormed with the user (ULTRATHINK context). Sub-project 1 of 4 in the
v5+ roadmap (production polish → cloud/licensing → plugins → perf+a11y).
Spec covers:
- Two-channel distribution: GitHub Releases (default) + ConcreteInfo self-hosted
- Light, skippable first-run wizard (theme + update channel + starter template)
- Auto-migrate v4.4.1 settings with v4 backup and toast on success/failure
- Local crash dump capture (no third-party) with a CrashReportModal
- Non-blocking update banner with user-confirmed restart-to-install
- IPC allowlist additions for updater:* and crash:*
- GitHub Actions CI extension (latest.yml generation, PR test suite)
Defers to v5.1: code signing, Sentry, delta updates, beta tracks.
After user review and approval, write the implementation plan via the
writing-plans skill.
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.
Pandoc 3.9 macOS zip uses an arch-suffixed inner directory
(pandoc-3.9.0.2-x86_64/) where Linux/Windows use the bare
pandoc-3.9.0.2/ dir. Hard-coding the intermediate path broke
the macOS build in v5.0.1-rc1.
Replace the hard-coded path.join(tmpDir, PANDOC_VERSION, ...)
with a recursive walk that locates the binary by name. This
is robust to future pandoc layout shifts (e.g., universal
binaries renaming the inner dir).
Verified:
- Linux: download + extract produces a working pandoc 3.9.0.2
- macOS: walker finds pandoc in pandoc-3.9.0.2-x86_64/bin/
- Windows: walker finds pandoc.exe in pandoc-3.9.0.2/
- 306/306 tests pass
User prefers light mode. next-themes previously defaulted to dark
which made first-load users (no localStorage) see a dark UI. With
'enableSystem' on, 'system' is still available as a choice — only
the initial value changed.
Existing users with 'dark' in localStorage keep dark until they toggle.
New users get light.