Skip to content

feat: local HTTP API, quick capture, mermaid/math plugins + ASAR fix#231

Merged
tomymaritano merged 4 commits into
developfrom
feature/desktop-power-features
Apr 24, 2026
Merged

feat: local HTTP API, quick capture, mermaid/math plugins + ASAR fix#231
tomymaritano merged 4 commits into
developfrom
feature/desktop-power-features

Conversation

@tomymaritano

@tomymaritano tomymaritano commented Apr 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

New Features

  • Local HTTP API (port 29168): REST API with bearer auth for Alfred/Raycast/curl integration
  • Quick Capture (Cmd+Shift+N): frameless floating window, always-on-top, auto-focus
  • Mermaid diagrams: code block renderer with "Open in Mermaid Live" button
  • Math/LaTeX: styled code block renderer with copy button
  • Vim mode: plugin stub with toggle command (ready for @codemirror/vim)

Bug Fix

  • ASAR crash fix: ERR_PACKAGE_PATH_NOT_EXPORTED — workspace packages now bundled by electron-vite instead of externalized to node_modules

Verified

  • Note window already shows only editor (no sidebar) — confirmed correct

Test plan

  • pnpm typecheck — 17/17 pass
  • pnpm build (desktop) — builds successfully

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Quick Capture window with Cmd/Ctrl+Shift+N for rapid note entry (floating, frameless).
    • Local HTTP server controls (start/stop/status/get token) accessible from the app.
    • Math & LaTeX plugin: render math blocks and copy LaTeX.
    • Mermaid plugin: render Mermaid blocks and copy source.
    • Vim Mode plugin: toggleable Vim Mode command and status indicator.

…AR crash

New features:
- Local HTTP API server (port 29168) with bearer token auth
  GET/POST/PUT /api/notes, /api/notes/search, /api/notes/quick, /api/status
- Global Quick Capture (Cmd+Shift+N): frameless floating window for fast notes
- Mermaid diagrams: code block renderer with "Open in Mermaid Live" button
- Math/LaTeX: code block renderer with styled display
- Vim mode: plugin stub with toggle command (ready for @codemirror/vim)

Bug fix:
- Fix ERR_PACKAGE_PATH_NOT_EXPORTED crash in packaged app
  electron-vite was externalizing @readied/* workspace packages, but
  ASAR doesn't have compiled JS for them. Now workspace packages are
  bundled into the main/preload output instead of externalized.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Apr 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
readide Ready Ready Preview, Comment Apr 24, 2026 2:19pm

Request Review

@github-actions github-actions Bot enabled auto-merge (squash) April 24, 2026 03:11
@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@tomymaritano has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 37 minutes and 53 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 37 minutes and 53 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7897caa1-f15b-4a8b-88be-bf4e6e448726

📥 Commits

Reviewing files that changed from the base of the PR and between ec02e67 and d3162ed.

📒 Files selected for processing (9)
  • apps/desktop/src/main/handlers/localServerHandlers.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/main/services/localServer.ts
  • apps/desktop/src/renderer/components/QuickCapture.tsx
  • apps/desktop/src/renderer/plugins/math.tsx
  • apps/desktop/src/renderer/plugins/mermaid.tsx
  • apps/desktop/src/renderer/plugins/tables.tsx
  • apps/desktop/src/renderer/plugins/vimMode.tsx
  • vercel.json
📝 Walkthrough

Walkthrough

Adds a local HTTP server with bearer-token auth and IPC handlers, a floating quick-capture window with global shortcut, preload APIs, renderer Quick Capture UI, three new renderer plugins (Mermaid, Math, Vim Mode), and tweaks to Electron Vite build and local CLI permission allowlist.

Changes

Cohort / File(s) Summary
Config & Build
\.claude/settings.local.json, apps/desktop/electron-vite.config.ts
Extended CLI permission allowlist entries; customized Vite externalizeDepsPlugin per build target with explicit exclude lists for several @readied/* packages.
Local Server Core
apps/desktop/src/main/services/localServer.ts, apps/desktop/src/main/handlers/localServerHandlers.ts
Added LocalServer class (start/stop/status), token management (api-token.txt), request routing for notes (list/search/get/create/update), JSON/body limits, auth checks, and IPC handler registration for server lifecycle and data operations.
Main Process Integration
apps/desktop/src/main/index.ts
Registered local-server handlers on startup/shutdown, added singleton frameless quick-capture BrowserWindow, global shortcut (Cmd/Ctrl+Shift+N), and IPC for opening/closing quick-capture; unregisters shortcuts and stops server on quit.
Preload API
apps/desktop/src/preload/api/localServer.ts, apps/desktop/src/preload/api/app.ts, apps/desktop/src/preload/api/index.ts, apps/desktop/src/preload/index.ts
Exposed LocalServerAPI (start/stop/status/getToken) and new WindowsAPI methods (openQuickCapture, closeSelf) via preload; wired window.readied.localServer.
Renderer — Quick Capture UI
apps/desktop/src/renderer/components/QuickCapture.tsx, apps/desktop/src/renderer/components/QuickCapture.module.css, apps/desktop/src/renderer/main.tsx
New QuickCapture React component and styles, notebook selection, keyboard shortcuts (Esc, Cmd/Ctrl+Enter), save/cancel flows, and route/query-param support to render quick-capture view.
Renderer — Plugins
apps/desktop/src/renderer/plugins/index.ts, apps/desktop/src/renderer/plugins/mermaid.tsx, apps/desktop/src/renderer/plugins/math.tsx, apps/desktop/src/renderer/plugins/vimMode.tsx
Added mermaidPlugin, mathPlugin, and vimModePlugin; each injects styles, registers code-block renderers/commands, and provides dispose logic; registered in built-in plugin list.

Sequence Diagram(s)

sequenceDiagram
    actor Client as HTTP Client
    participant LocalServer as LocalServer (main)
    participant IPC as Main IPC Handlers
    participant Repo as Note Repository

    Client->>LocalServer: POST /api/notes (Bearer token + JSON)
    LocalServer->>LocalServer: Validate Bearer token (const-time)
    alt token valid
        LocalServer->>IPC: invoke createNote handler (adapter)
        IPC->>Repo: create note in storage
        Repo-->>IPC: created note object
        IPC-->>LocalServer: adapted response
        LocalServer-->>Client: 201 Created + JSON
    else token invalid
        LocalServer-->>Client: 401 Unauthorized
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

dependencies, package:plugin-api

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main features added in this PR: local HTTP API, quick capture window, new plugins (mermaid/math), and an ASAR packaging fix.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/desktop-power-features

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 51124ab642

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/main/services/localServer.ts
Comment on lines +126 to +127
const notes = await repo.list();
return notes.filter(n => !n.isDeleted).length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compute API note count without list() default limit

This count is derived from repo.list(), but SQLiteNoteRepository.list defaults to limit = 50, so /api/status underreports as soon as a user has more than 50 active notes. The status endpoint should return a true total (e.g., via repo.count()), otherwise integrations that rely on this health metadata get incorrect results.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Fixed: uses repo.count() instead of list().

Comment thread apps/desktop/src/main/services/localServer.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 22

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.claude/settings.local.json (1)

1-91: ⚠️ Potential issue | 🟠 Major

Remove .claude/settings.local.json from version control and add it to .gitignore.

This file contains user-specific absolute paths (e.g., /Users/tomasmaritano/...) and follows the .local.json naming convention indicating per-developer local overrides—similar to VS Code's workspace-level overrides. Other .claude/ subdirectories (.claude/plugins/ and .claude/statsig/) are already gitignored.

Add /.claude/settings.local.json to .gitignore. If shared Claude permissions are needed, create a committed .claude/settings.json with non-machine-specific paths and keep .settings.local.json for local development only.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/settings.local.json around lines 1 - 91, The committed
.claude/settings.local.json contains machine-specific absolute paths and should
be removed from version control and ignored; remove the tracked file (e.g., git
rm --cached .claude/settings.local.json or delete and commit), add
"/.claude/settings.local.json" to .gitignore, and commit that change; if a
shared, non-machine-specific config is needed, add a committed
.claude/settings.json with sanitized paths (keeping per-developer overrides in
.claude/settings.local.json) so .claude/plugins/ and .claude/statsig/ remain
ignored as intended.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.claude/settings.local.json:
- Around line 81-88: Remove the now-redundant specific ls allowlist entries
("Bash(ls
/Users/tomasmaritano/Documents/Github/readied/readide/apps/desktop/src/renderer/components/NoteListFilterBar*)"
and "Bash(ls
/Users/tomasmaritano/Documents/Github/readied/readide/apps/desktop/src/renderer/components/*.module.css)")
because the broader "Bash(ls:*)" pattern already covers them; update the
allowlist to keep only the general "Bash(ls:*)" entry and verify there are no
other duplicated specific "Bash(ls ...)" lines to clean up.

In `@apps/desktop/src/main/handlers/localServerHandlers.ts`:
- Around line 60-61: Module-level mutable singletons `server` and `apiToken`
cause state leakage and duplicate ipc handlers; change to encapsulate state in a
factory (e.g., a function that creates and returns {
registerLocalServerHandlers, autoStart, stop }) that holds its own `server` and
`apiToken` instances instead of module globals, and have
`registerLocalServerHandlers` call `ipcMain.removeHandler(channel)` for each
channel before calling `ipcMain.handle(...)` to avoid "second handler
registered" on re-import/hot-reload; reference symbols to change: replace module
`server` and `apiToken`, update `registerLocalServerHandlers`, and use
`LocalServer`/`ipcMain.removeHandler` accordingly.
- Around line 148-151: The stop handler currently awaits server.stop() without
error handling so rejections bubble to the renderer; wrap the
ipcMain.handle('localServer:stop', ...) implementation in a try/catch like the
'localServer:start' handler: try { await server.stop(); return { ok: true }; }
catch (err) { return { ok: false, error: String(err?.message ?? err) }; } so the
renderer always receives a consistent { ok, error } shape and no unhandled IPC
rejection occurs.
- Around line 136-145: Validate and coerce the incoming port before calling
server.start: in the ipcMain.handle('localServer:start') handler, treat the
optional port param as unknown, coerce it to a number (e.g., via Number(...) or
parseInt), check Number.isInteger and that it falls within 1–65535 (allow
undefined to mean "use default"); if invalid, return { ok: false, error:
'invalid port' } (or a descriptive error) instead of passing it through; then
pass the validated port to server.start (functions to locate:
ipcMain.handle('localServer:start'), server.start, server.isRunning,
server.getPort, getOrCreateApiToken).
- Around line 174-179: The function autoStartLocalServer currently only
pre-warms an API token and ignores most of LocalServerHandlerDeps
(noteRepository, noteToSnapshot) and does not actually start the server; also
getOrCreateApiToken can throw and will bubble up to app startup. Rename and
narrow the function to prepareLocalServerToken (or similar) accepting only
DataPaths (or dataPaths.root) instead of LocalServerHandlerDeps, wrap the
getOrCreateApiToken call in a try/catch that logs the error and
returns/continues (so failures don't reject main startup), and keep actual
server startup to the existing localServer:start flow; update any callers in
main/index.ts to call the new prepareLocalServerToken and ensure server.start
remains only invoked via the setting-driven path.

In `@apps/desktop/src/main/index.ts`:
- Around line 892-898: The ipcMain handler "window:closeSelf" currently allows
any renderer to close its BrowserWindow via ipcMain.handle and
BrowserWindow.fromWebContents(event.sender); change this to restrict closing to
the quick-capture window by either renaming the channel to
"window:closeQuickCapture" and only handling that channel, or keep the channel
and add an explicit check comparing event.sender to the stored
quickCaptureWindow.webContents (e.g., if event.sender !==
quickCaptureWindow.webContents then return { ok: false }); only call win.close()
when the sender matches the known quickCaptureWindow and win is not destroyed;
ensure the handler still returns a consistent result object.
- Around line 457-462: The blur handler currently unconditionally closes
quickCaptureWindow and can drop user input; change it so the 'blur' event does
not directly call quickCaptureWindow.close(). Instead, have the renderer expose
its "dirty" state via IPC (e.g., channel names like 'quick-capture-dirty' /
'quick-capture-clean' or a one-time query), and only close on blur when the
renderer reports clean/empty; additionally make Escape keypress the primary
dismiss action (listen for 'keydown' or register an accelerator in the renderer
and send an IPC 'quick-capture-close' to the main process), and optionally gate
blur-close behind a user setting or skip it in dev (check process.env.NODE_ENV
or isPackaged) so DevTools won't trigger automatic close.
- Around line 420-425: The existing quickCaptureWindow may be hidden even when
focused; update createQuickCaptureWindow so that when quickCaptureWindow exists
and is not destroyed you call quickCaptureWindow.show() before
quickCaptureWindow.focus(), mirroring the behavior used for settingsWindow to
ensure an always-on-top/frameless window is made visible as well as focused.
- Around line 880-883: globalShortcut.register for 'CommandOrControl+Shift+N'
can return false if the accelerator is already taken; update the registration
code where globalShortcut.register is called (the block that invokes
createQuickCaptureWindow) to check the boolean return value and, on false, emit
a visible diagnostic: log a warning via the existing main-process logger (or
processLogger) including the accelerator string and that registration failed,
and optionally send an IPC message to renderer(s) or the quick-capture UI to
surface the problem to the user; ensure the new logic references
globalShortcut.register, the accelerator 'CommandOrControl+Shift+N', and
createQuickCaptureWindow so it’s easy to locate and test.

In `@apps/desktop/src/main/services/localServer.ts`:
- Around line 194-222: The dynamic id routes for GET/PUT (/api/notes/:id)
currently capture reserved literal segments like "quick" (and "search"), causing
misrouting; update the routing logic so those literals are handled explicitly:
either add explicit branches before the regex routes for /api/notes/quick and
/api/notes/search that return 405 for unsupported methods (e.g., GET/PUT on
/api/notes/quick) and forward POST/search to their handlers, or keep the dynamic
branch but validate the extracted noteId in handlers.getNote/handlers.updateNote
(or right after const noteId = pathname.split('/').pop()!) and return a 400/405
with sendJson when noteId equals reserved values like "quick" or "search";
reference symbols: handlers.getNote, handlers.updateNote, this.readBody,
this.sendJson.
- Around line 268-282: The readBody function currently accumulates unlimited
chunks causing DoS risk; change it to enforce a max size (e.g., 5 * 1024 * 1024
bytes or make configurable) by tracking total bytes on each 'data' event and
immediately stop processing when exceeded: remove listeners, destroy the
request/stream, and reject the Promise with an error indicating payload too
large (HTTP 413) so the caller can send a 413 response; keep the existing JSON
parse/resolve logic on 'end' and ensure 'error' cleans up listeners too.
- Around line 61-73: In getOrCreateApiToken, ensure the token file is created
with owner-only permissions and the parent directory is restricted: before
writing tokenPath (TOKEN_FILE) call mkdir for the parent directory with mode
0o700 (recursive:true) if needed, write the token using fs.writeFile with the
option { mode: 0o600 }, and if an existing token is read, check and fix its
permissions (fs.chmod tokenPath to 0o600) when they are more permissive;
reference tokenPath, TOKEN_FILE and getOrCreateApiToken when making these
changes.
- Around line 142-147: Replace the direct inequality check on
req.headers.authorization with a constant-time, length-checked comparison: first
verify the header exists and starts with the literal "Bearer " prefix, then
extract the token substring and compare its Buffer to Buffer.from(this.token)
using crypto.timingSafeEqual only if lengths match; if any check fails call
this.sendJson(res, 401, { error: 'Unauthorized' }); also add the required import
for Node's crypto module. Ensure you reference the existing authHeader variable,
this.token, and this.sendJson when making these changes in localServer.ts.
- Around line 97-107: When starting the server in start(), ensure the temporary
server reference is cleared if listen fails and that the 'error' handler is
removed once listen succeeds: attach a named error handler to this.server
(created via createServer and used in this.server!.on('error', ...)) that on
error sets this.server = null and rejects the start() promise; in the listen
callback remove that error listener so later runtime errors don't trigger the
startup rejection, and on successful listen resolve normally. Reference the
this.server field, start() method, the createServer(...) callback
(handleRequest), the server.on('error', reject) usage, and the
server.listen(...) callback when making these changes.

In `@apps/desktop/src/renderer/components/QuickCapture.module.css`:
- Around line 3-14: The .container CSS sets border-radius: 12px but the
BrowserWindow is created with frame: false and backgroundColor: '#0a0b0d' which
prevents rounded corners from being visible; update the BrowserWindow options in
the window creation (the new BrowserWindow({...}) call) to add transparent: true
and remove the opaque backgroundColor (or set backgroundColor to 'transparent')
so the .container border-radius can show, or alternatively remove the
border-radius from QuickCapture.module.css .container if you prefer an opaque
rectangular window.

In `@apps/desktop/src/renderer/components/QuickCapture.tsx`:
- Around line 35-54: The catch block in handleSave silently swallows errors
causing no user feedback or logs when window.readied.notes.create fails; update
handleSave to catch the error as e, call setSaving(false), log the error (e.g.,
console.error or processLogger) and set a user-visible error state (e.g.,
setError or setSaveError) so the UI can display a message, and ensure the
dependency array includes any new state setters; retain handleClose on success
only.
- Around line 20-24: In QuickCapture's useEffect that calls
window.readied.notebooks.list(), add error handling and an unmount guard: wrap
the promise chain with .catch to log or surface the error (using the component's
logging mechanism or window.api) so failures don't silently drop, and use an
isMounted flag (or AbortController) inside the effect to ensure setNotebooks is
only called when the component is still mounted; reference the useEffect,
window.readied.notebooks.list(), and setNotebooks to locate the code to modify.
- Around line 57-75: The keydown handler onKeyDown inside the useEffect should
ignore IME composition events so Enter/Escape don't prematurely submit or close:
at the start of onKeyDown, return early if (e as any).isComposing is true or
e.keyCode === 229; keep the existing Escape and Cmd/Ctrl+Enter branches
unchanged otherwise. Update the window event listener logic in the same
useEffect (where onKeyDown, handleClose and handleSave are referenced) so
composition events are skipped before calling handleClose or handleSave.

In `@apps/desktop/src/renderer/plugins/math.tsx`:
- Around line 1-14: The header comment incorrectly states that inline ($...$)
and display ($$...$$) math are handled by a remark plugin even though the module
only registers fenced code-block renderers via activate(); fix by either
removing that remark-plugin claim from the docstring or by actually implementing
and registering a remark plugin: if removing, edit the top comment to mention
only fenced code-block renderers and the copy UI; if implementing, add a remark
plugin that wraps inline/display math in spans (e.g., producing elements with
classes like "language-math-inline"/"language-math-display") and register it in
the module's activate() PluginManifest so the existing CSS selectors (e.g.,
.markdown-preview code.language-math-inline) become reachable.

In `@apps/desktop/src/renderer/plugins/mermaid.tsx`:
- Around line 13-17: injectMermaidStyles currently sets a module-level flag
(styleInjected) and appends a <style> element but never removes it; change it to
store the created element in a module-level variable (e.g., mermaidStyleEl), add
a removeMermaidStyles() that removes mermaidStyleEl from document.head and
resets styleInjected/mermaidStyleEl, and call removeMermaidStyles() from the
plugin's dispose() so reactivation works and no orphaned nodes remain; apply the
same pattern for injectMathStyles/styleInjected in math.tsx (create a
mathStyleEl, removeMathStyles(), reset flag, and call from dispose()).
- Around line 90-100: The openInLive callback currently uses btoa(state) and
window.open(..., '_blank') which breaks on Unicode, produces non‑URL‑safe
base64, and allows tabnabbing; replace the btoa call in openInLive with a UTF‑8
safe encoder (e.g., UTF‑8 encode the JSON then base64 it), convert the resulting
base64 to URL‑safe alphabet by replacing '+'→'-', '/'→'_' and stripping any '='
padding (matching mermaid.live's serde), and update the window.open call to
include noopener/noreferrer (e.g., window.open(url, '_blank',
'noopener,noreferrer') or use an anchor with rel="noopener noreferrer") so that
code (the variable used in the state) is correctly encoded and opened securely.

In `@apps/desktop/src/renderer/plugins/vimMode.tsx`:
- Around line 34-71: The activation path currently calls enable()
unconditionally when the saved config is true, causing redundant
context.config.set('enabled', true), repeated context.log.warn messages and
showing the VIM status even when the dependency is missing; change activate() so
it separates "apply current state" from "first-time enable" side-effects: detect
whether `@codemirror/vim` is present (e.g., try a dynamic import or a boolean
hasVim flag), then on activation if enabled is true only call a new applyState()
that registers extensions (using unregisterExt) and calls showStatus()
conditionally (show a disabled/unavailable indicator or skip showStatus when
hasVim is false) but do NOT call context.config.set('enabled', true) or
context.log.warn; keep enable() as the explicit user action that sets
context.config.set and emits the warn when attempting to enable without the dep.

---

Outside diff comments:
In @.claude/settings.local.json:
- Around line 1-91: The committed .claude/settings.local.json contains
machine-specific absolute paths and should be removed from version control and
ignored; remove the tracked file (e.g., git rm --cached
.claude/settings.local.json or delete and commit), add
"/.claude/settings.local.json" to .gitignore, and commit that change; if a
shared, non-machine-specific config is needed, add a committed
.claude/settings.json with sanitized paths (keeping per-developer overrides in
.claude/settings.local.json) so .claude/plugins/ and .claude/statsig/ remain
ignored as intended.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 261edb40-3dae-4c44-8afb-b3e574b47f85

📥 Commits

Reviewing files that changed from the base of the PR and between 67a0831 and 51124ab.

📒 Files selected for processing (16)
  • .claude/settings.local.json
  • apps/desktop/electron-vite.config.ts
  • apps/desktop/src/main/handlers/localServerHandlers.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/main/services/localServer.ts
  • apps/desktop/src/preload/api/app.ts
  • apps/desktop/src/preload/api/index.ts
  • apps/desktop/src/preload/api/localServer.ts
  • apps/desktop/src/preload/index.ts
  • apps/desktop/src/renderer/components/QuickCapture.module.css
  • apps/desktop/src/renderer/components/QuickCapture.tsx
  • apps/desktop/src/renderer/main.tsx
  • apps/desktop/src/renderer/plugins/index.ts
  • apps/desktop/src/renderer/plugins/math.tsx
  • apps/desktop/src/renderer/plugins/mermaid.tsx
  • apps/desktop/src/renderer/plugins/vimMode.tsx

Comment thread .claude/settings.local.json
Comment thread apps/desktop/src/main/handlers/localServerHandlers.ts
Comment thread apps/desktop/src/main/handlers/localServerHandlers.ts
Comment thread apps/desktop/src/main/handlers/localServerHandlers.ts
Comment thread apps/desktop/src/main/handlers/localServerHandlers.ts Outdated
Comment thread apps/desktop/src/renderer/components/QuickCapture.tsx
Comment thread apps/desktop/src/renderer/plugins/math.tsx
Comment thread apps/desktop/src/renderer/plugins/mermaid.tsx Outdated
Comment thread apps/desktop/src/renderer/plugins/mermaid.tsx Outdated
Comment thread apps/desktop/src/renderer/plugins/vimMode.tsx
Security:
- Token file written with mode 0o600 (owner-only)
- Constant-time token comparison via crypto.timingSafeEqual
- 5MB request body limit (DoS prevention)
- Server state cleared on listen failure (EADDRINUSE)
- window:closeSelf rejects calls from main window

API:
- Note count uses repo.count() not list() with limit
- Empty string content accepted in PUT (allows clearing notes)
- /api/notes/quick route checked before :id (no ambiguity)
- Port validation (1-65535), stop handler error handling

Quick Capture:
- Removed blur→close (prevented copying from other apps)
- IME composition guard on keyboard handlers
- Save errors displayed to user
- Notebook fetch guarded against unmount
- Border-radius removed (needs transparent:true)

Plugins:
- Mermaid: copy-to-clipboard instead of broken base64 URL
- Mermaid/Math: style elements cleaned up on dispose
- Vim: don't show indicator when dep missing, don't re-persist config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/main/index.ts (1)

929-937: ⚠️ Potential issue | 🟡 Minor

before-quit does not await async shutdown.

void stopLocalServer() is fire-and-forget inside the before-quit handler; Electron will proceed to close the app regardless of whether the HTTP server has finished closing its sockets or draining in-flight responses. The OS will eventually reap the listener, but any active request gets dropped mid-response. If clean shutdown matters, switch to the async pattern where you call event.preventDefault(), await stopLocalServer(), then call app.quit() (guarded by a flag to avoid recursion).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/index.ts` around lines 929 - 937, The before-quit
handler currently fire-and-forgets stopLocalServer(), so modify the
app.on('before-quit') handler to perform an async graceful shutdown: add a
module-level boolean (e.g., isQuitting) to guard against recursion, make the
handler accept the event, call event.preventDefault() if not already quitting,
set isQuitting, await stopLocalServer(), optionally await stopPluginWatcher() if
it becomes async, then close db (db.close()) and log via
getLogger().info('Database closed'), and finally call app.quit() to complete
shutdown; ensure the guard prevents re-entering the handler.
♻️ Duplicate comments (2)
apps/desktop/src/renderer/plugins/math.tsx (1)

19-24: ⚠️ Potential issue | 🟠 Major

Style element still leaks on dispose() — fix not applied here.

The sibling change in mermaid.tsx now tracks the <style> node in a module-level variable and removes it on dispose, but math.tsx still only uses the styleInjected boolean without a reference to the element. Consequently dispose() (lines 155–160) only unregisters the renderers — the injected <style> stays in document.head for the lifetime of the document, and because styleInjected is never reset, a subsequent activate() becomes a no-op. Please mirror the fix already done in mermaid.tsx.

♻️ Proposed fix
-// Inject styles once
-let styleInjected = false;
+// Inject styles once; keep a ref to remove on dispose
+let styleInjected = false;
+let styleElement: HTMLStyleElement | null = null;
 function injectMathStyles() {
   if (styleInjected) return;
   styleInjected = true;
   const style = document.createElement('style');
+  styleElement = style;
   style.textContent = `
@@
   document.head.appendChild(style);
 }
@@
     return {
       dispose() {
         unregisterMath();
         unregisterLatex();
+        if (styleElement && styleElement.parentNode) {
+          styleElement.parentNode.removeChild(styleElement);
+          styleElement = null;
+          styleInjected = false;
+        }
       },
     };

Also applies to: 103-104, 155-161

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/plugins/math.tsx` around lines 19 - 24, The
injected <style> element in math.tsx is only guarded by the boolean
styleInjected and never removed on dispose(), causing a leak and preventing
re-activation; modify injectMathStyles and module state to store the created
<style> node in a module-level variable (e.g., mathStyleEl), set styleInjected
when inserted, and in dispose() remove mathStyleEl from document.head (if
present) and null it and reset styleInjected so subsequent activate() can
re-insert; update any references to styleInjected/injectMathStyles accordingly
to use and clear the stored element.
apps/desktop/src/renderer/plugins/vimMode.tsx (1)

50-81: ⚠️ Potential issue | 🟡 Minor

Gate VIM badge + warnings by real Vim availability

enable() still shows the VIM status unconditionally, and activation still re-warns on every startup when enabled=true. This keeps signaling “active” behavior while keybindings are unavailable.

💡 Proposed fix
   activate(context) {
     let enabled = context.config.get<boolean>('enabled') ?? false;
+    // TODO: flip to true once `@codemirror/vim` is installed and wired.
+    const hasVimDependency = false;
+    let warnedMissingDep = false;
 
     const showStatus = () => {
       context.layout.addComponent('editor-status-bar', {
         id: 'vim-mode:status',
         component: VimStatusIndicator,
@@
     const hideStatus = () => {
       context.layout.removeComponent('vim-mode:status');
     };
+
+    const applyState = () => {
+      if (enabled && hasVimDependency) {
+        showStatus();
+      } else {
+        hideStatus();
+      }
+    };
+
+    const warnMissingDepOnce = () => {
+      if (hasVimDependency || warnedMissingDep) return;
+      warnedMissingDep = true;
+      context.log.warn(
+        'Vim mode enabled but `@codemirror/vim` is not installed. ' +
+          'Add it to apps/desktop/package.json to activate Vim keybindings.'
+      );
+    };
 
     const enable = () => {
       enabled = true;
       context.config.set('enabled', true);
-      // TODO: When `@codemirror/vim` is installed, register the extension here:
-      // unregisterExt = context.registerExtensions('vim-keymap', [vim()]);
-      context.log.warn(
-        'Vim mode enabled but `@codemirror/vim` is not installed. ' +
-          'Add it to apps/desktop/package.json to activate Vim keybindings.'
-      );
-      showStatus();
+      warnMissingDepOnce();
+      applyState();
     };
 
     const disable = () => {
       enabled = false;
       context.config.set('enabled', false);
-      hideStatus();
+      applyState();
       context.log.info('Vim mode disabled');
     };
@@
-    if (enabled) {
-      // Don't re-persist: just show the warning and status
-      context.log.warn(
-        'Vim mode enabled but `@codemirror/vim` is not installed. ' +
-          'Add it to apps/desktop/package.json to activate Vim keybindings.'
-      );
-      // Don't show VIM indicator when the dependency is missing —
-      // it would mislead the user into thinking vim mode is active.
-    }
+    // Apply persisted state without re-emitting warning noise on startup.
+    applyState();
#!/bin/bash
set -euo pipefail

# Verify whether Vim deps are present in manifests/lockfiles
fd -HI '^package\.json$' . | xargs rg -n '"@codemirror/vim"|"@replit/codemirror-vim"' || true
fd -HI 'pnpm-lock.yaml' . | xargs rg -n '"@codemirror/vim"|"@replit/codemirror-vim"' || true

# Verify current plugin behavior paths (status + warning)
rg -n -C3 'const enable =|showStatus\(|if \(enabled\)|log\.warn\(' apps/desktop/src/renderer/plugins/vimMode.tsx
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/plugins/vimMode.tsx` around lines 50 - 81, The
plugin currently shows the VIM status and logs a warning on every startup even
if `@codemirror/vim` isn't installed; change the flow to detect whether the Vim
extension was actually registered (use the existing TODO spot where
unregisterExt = context.registerExtensions('vim-keymap', [vim()]) would be set)
and gate UI/warnings on that: in enable() only call showStatus() when the
registered-extension flag/unregisterExt is truthy and emit context.log.warn('Vim
mode enabled but ...') only when a user action enables while the extension is
missing; remove or stop re-emitting the startup-wide context.log.warn inside the
if (enabled) startup branch and avoid showing the VIM indicator there unless the
extension is present (check the same registered-extension flag/unregisterExt).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/main/handlers/localServerHandlers.ts`:
- Around line 168-173: The ipc handler for 'localServer:getToken' can throw an
unhandled rejection because getOrCreateApiToken (which does fs I/O) is called
without try/catch; update the handler defined in localServerHandlers.ts so the
async function wraps the apiToken assignment and
getOrCreateApiToken(dataPaths.root) call in a try/catch and return a consistent
result shape (e.g., { ok: true, value: apiToken } on success and { ok: false,
error: <message or error> } on failure) to match other handlers; ensure you
still cache apiToken on success and include the original error message in the
error field.

In `@apps/desktop/src/main/index.ts`:
- Around line 891-906: The current window:closeSelf handler infers allowed
senders by finding a "main" window which is fragile; instead maintain an
explicit allowlist of webContents for windows you intend to allow closing (e.g.,
quick-capture and settings). Add a Set (e.g., allowedClosableWebContents) when
you create quickCaptureWindow and settingsWindow (in their create functions such
as createQuickCaptureWindow/createSettingsWindow or where quickCaptureWindow and
settingsWindow are assigned) and add their webContents.id (or webContents) to
the Set; remove the entry on the window's 'closed' event. In the
ipcMain.handle('window:closeSelf', ...) handler check event.sender (or
event.sender.id) against allowedClosableWebContents and only call win.close() if
it's explicitly present, otherwise return { ok: false, error: 'Not permitted to
close this window' }. This replaces the BrowserWindow.getAllWindows() find()
logic and ensures only explicitly opened quick-capture/settings windows can
request self-close.

In `@apps/desktop/src/renderer/components/QuickCapture.tsx`:
- Around line 53-67: The call to window.readied.notes.create returns a Result
type and currently handleClose() is invoked unconditionally; change the await
result handling in the QuickCapture component (the window.readied.notes.create
call) to check result.ok before closing: if result.ok is true then call
handleClose(), otherwise do not close, call setError(...) with a message derived
from result.error.type or result.error.error (prefer result.error.error if
present, fallback to result.error.type), and ensure setSaving(false) is called
on failure so the UI exits saving state; keep the existing catch for transport
exceptions but move handleClose() inside the success branch.

In `@apps/desktop/src/renderer/plugins/math.tsx`:
- Around line 109-117: The copySource callback sets setCopied(true) then
schedules setCopied(false) with an unreferenced setTimeout which may run after
unmount; fix by storing the timeout id in a ref (e.g. copyTimeoutRef) and clear
any existing timeout before creating a new one inside copySource, and add a
useEffect cleanup that clears copyTimeoutRef.current on unmount so
setCopied(false) won't run after the component unmounts; reference the
copySource callback, setCopied state setter, and useCallback/useEffect for
locating where to add the ref/cleanup.
- Around line 7-10: Remove the unreachable inline CSS for math from the
docstring and any duplicated blocks in
apps/desktop/src/renderer/plugins/math.tsx: specifically delete the
`.language-math-inline` (and related inline-only rules) CSS that is commented as
unusable without remark-math; if you want to preserve intent, instead add a
short TODO note referencing that these styles should be reintroduced when
`remark-math` is actually registered, or conditionally emit the CSS only when a
runtime flag or plugin-registration (the math plugin registration code in this
file) confirms `remark-math` is present.

In `@apps/desktop/src/renderer/plugins/mermaid.tsx`:
- Around line 93-111: copySource and copyForLive schedule
setCopied(false)/setLiveHint(false) with setTimeout but don't clear timers,
causing potential "state update on unmounted component" warnings; fix by storing
timeout ids in a ref (e.g., copyTimeoutRef) and clearing any existing timeout at
the start of each handler and on unmount via useEffect cleanup, then assign the
new timeout id to the ref so you can call clearTimeout(ref.current) before
setting state or when the component unmounts; update both copySource and
copyForLive to use this pattern and reference setCopied and setLiveHint
respectively.
- Around line 1-7: The plugin header and manifest.description in
apps/desktop/src/renderer/plugins/mermaid.tsx claim an "Open in Mermaid Live"
one‑click action but the UI only provides "Copy" and "Copy for Mermaid Live"
clipboard buttons (see the button labels and the renderer registration in this
file); either update the docstring and manifest.description to accurately state
that the plugin copies content to the clipboard (remove references to opening
mermaid.live) or implement the advertised behavior by changing the "Copy for
Mermaid Live" button handler to open the Mermaid Live editor URL (e.g.,
https://mermaid.live/edit#pako:... with the compressed payload) in a new
window/tab; modify the manifest.description and the top-of-file comment to match
whichever approach you choose and update the click handler for the "Copy for
Mermaid Live" button (or its helper function) accordingly so the code and
description are consistent.

---

Outside diff comments:
In `@apps/desktop/src/main/index.ts`:
- Around line 929-937: The before-quit handler currently fire-and-forgets
stopLocalServer(), so modify the app.on('before-quit') handler to perform an
async graceful shutdown: add a module-level boolean (e.g., isQuitting) to guard
against recursion, make the handler accept the event, call
event.preventDefault() if not already quitting, set isQuitting, await
stopLocalServer(), optionally await stopPluginWatcher() if it becomes async,
then close db (db.close()) and log via getLogger().info('Database closed'), and
finally call app.quit() to complete shutdown; ensure the guard prevents
re-entering the handler.

---

Duplicate comments:
In `@apps/desktop/src/renderer/plugins/math.tsx`:
- Around line 19-24: The injected <style> element in math.tsx is only guarded by
the boolean styleInjected and never removed on dispose(), causing a leak and
preventing re-activation; modify injectMathStyles and module state to store the
created <style> node in a module-level variable (e.g., mathStyleEl), set
styleInjected when inserted, and in dispose() remove mathStyleEl from
document.head (if present) and null it and reset styleInjected so subsequent
activate() can re-insert; update any references to
styleInjected/injectMathStyles accordingly to use and clear the stored element.

In `@apps/desktop/src/renderer/plugins/vimMode.tsx`:
- Around line 50-81: The plugin currently shows the VIM status and logs a
warning on every startup even if `@codemirror/vim` isn't installed; change the
flow to detect whether the Vim extension was actually registered (use the
existing TODO spot where unregisterExt =
context.registerExtensions('vim-keymap', [vim()]) would be set) and gate
UI/warnings on that: in enable() only call showStatus() when the
registered-extension flag/unregisterExt is truthy and emit context.log.warn('Vim
mode enabled but ...') only when a user action enables while the extension is
missing; remove or stop re-emitting the startup-wide context.log.warn inside the
if (enabled) startup branch and avoid showing the VIM indicator there unless the
extension is present (check the same registered-extension flag/unregisterExt).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: da98e230-15d6-4d5a-b205-b5c1e8f0aaf6

📥 Commits

Reviewing files that changed from the base of the PR and between 51124ab and ec02e67.

📒 Files selected for processing (8)
  • apps/desktop/src/main/handlers/localServerHandlers.ts
  • apps/desktop/src/main/index.ts
  • apps/desktop/src/main/services/localServer.ts
  • apps/desktop/src/renderer/components/QuickCapture.module.css
  • apps/desktop/src/renderer/components/QuickCapture.tsx
  • apps/desktop/src/renderer/plugins/math.tsx
  • apps/desktop/src/renderer/plugins/mermaid.tsx
  • apps/desktop/src/renderer/plugins/vimMode.tsx

Comment thread apps/desktop/src/main/handlers/localServerHandlers.ts
Comment thread apps/desktop/src/main/index.ts
Comment thread apps/desktop/src/renderer/components/QuickCapture.tsx
Comment thread apps/desktop/src/renderer/plugins/math.tsx
Comment thread apps/desktop/src/renderer/plugins/math.tsx
Comment thread apps/desktop/src/renderer/plugins/mermaid.tsx
Comment thread apps/desktop/src/renderer/plugins/mermaid.tsx
CI fixes:
- tables.tsx: cast ReactElement with generic params for .props access
- localServer.ts: attach cause to re-thrown errors (preserve-caught-error)

Review findings:
- localServerHandlers: getToken wrapped in try/catch with result shape
- index.ts: closeSelf uses explicit allowlist Set instead of fragile find
- index.ts: graceful before-quit with async shutdown + isQuitting guard
- QuickCapture: check result.ok before closing, show error on failure
- math.tsx: timeout ref cleanup, style element cleanup in dispose, removed dead CSS
- mermaid.tsx: timeout ref cleanup, description corrected
- vimMode.tsx: no VIM indicator when dep missing, no unnecessary startup warn

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tomymaritano

Copy link
Copy Markdown
Collaborator Author

All review findings addressed in 52edc7b

CI Fixes

  • tables.tsx: ReactElement cast with generic params for .props access
  • localServer.ts: cause attached to re-thrown errors

Review Findings

Finding Fix
getToken unhandled rejection try/catch with { ok, value/error } shape
closeSelf fragile sender check Explicit Set<number> allowlist for closable windows
QuickCapture unconditional close Checks result.ok before closing, shows error
math.tsx timeout + style leak Ref-based cleanup + dispose removes style element
mermaid.tsx timeout leak + description Ref cleanup + accurate description
vimMode.tsx indicator when dep missing No indicator/warn when extension not registered
before-quit fire-and-forget Async graceful shutdown with isQuitting guard

Verification: 0 lint errors, 17/17 typecheck pass

Vercel's pnpm install fails because electron/node-gyp is referenced
via SSH URL (git@github.com:) which Vercel can't resolve. The git
config rewrite converts to HTTPS before install runs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tomymaritano tomymaritano disabled auto-merge April 24, 2026 14:20
@tomymaritano tomymaritano merged commit d4ce973 into develop Apr 24, 2026
14 of 15 checks passed
@tomymaritano tomymaritano deleted the feature/desktop-power-features branch April 24, 2026 14:21
github-actions Bot pushed a commit that referenced this pull request Apr 24, 2026
## Release PR

Merges `develop` into `main` to trigger a new release via
semantic-release.

### Highlights since last release

- **feat: allow disabling built-in plugins** + MCP server FTS5 fix
(#237)
- **fix: move @readied/* to devDependencies** — ASAR startup crash
(#235)
- **fix: exclude @readied/* from ASAR** (#233)
- **feat: local HTTP API, quick capture, mermaid/math plugins** (#231)
- **fix: replace removed lucide brand icons** (#229)
- Multiple CI, type compat, and dependency fixes

### MCP server: sql.js → better-sqlite3
- FTS5 triggers now work (was crashing with `no such module: fts5`)
- WAL mode for safe concurrent access with the desktop app
- Runtime FTS5 check at startup with descriptive error

### Plugin system fixes (from code review)
- Built-in plugins no longer briefly activate before enabled state loads
- IPC listener cleanup no longer nukes unrelated listeners

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Built-in plugins can now be toggled on/off in settings;
enabled/disabled states persist across sessions
* Improved markdown editor URL auto-linking for more reliable link
creation

* **Bug Fixes**
* Fixed URL auto-linking to correctly identify URLs when document
content changes

* **Tests**
  * Added comprehensive test suite for database trigger functionality

* **Chores**
  * Migrated database backend for improved performance and stability

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant