Skip to content

feat(editor): a desktop markdown editor with Claude editing and gist drafts - #324

Merged
soroushm merged 18 commits into
mainfrom
editor-app
Aug 4, 2026
Merged

feat(editor): a desktop markdown editor with Claude editing and gist drafts#324
soroushm merged 18 commits into
mainfrom
editor-app

Conversation

@soroushm

@soroushm soroushm commented Aug 2, 2026

Copy link
Copy Markdown
Member

The first half of apps/editor: a desktop markdown editor that edits through the local Claude Code
CLI and keeps gist work in a local sandbox. Split out of one oversized branch — streaming, cancel,
writing from an existing gist, and the rest of the sandbox work follow in a second PR.

Closes #296, #297, #298, #299, #300, #320, #321 — all under Epic #295.

What it does

The document. Electron + electron-vite + React 19, rendered through @soroush.tech/markdown and
themed by @soroush.tech/design-system. Native New/Open/Save/Save As, unsaved-change tracking with a
prompt on close, coalesced undo/redo, and edit / preview / split / live-edit modes.

Editing with Claude. Select text — or nothing, for the whole document — describe the change, and
the local claude CLI rewrites it in place.

Gists. Connect an account with a fine-grained PAT, encrypted at rest through Electron's
safeStorage. Browse your gists, open their files, and edit against a local sandbox: adds,
deletes, edits and the description are all staged, and only Publish sends them — as one request.
Drafts persist to disk, so unfinished work survives quitting.

Notable decisions

  • Security baseline is non-negotiable: contextIsolation + sandbox + no nodeIntegration,
    invoke/handle IPC only behind a typed contextBridge, every response Result-wrapped, IPC
    arguments validated, CSP set through response headers. The gist token never crosses back to the
    renderer, and avatars are inlined as data: URIs rather than widening img-src.
  • The CLI is spawned with fixed flags from the OS temp dir, all user content on stdin and tools
    disabled, so no repo's CLAUDE.md/hooks/MCP config bleeds into a request and nothing
    user-controlled reaches the shell. Deliberately not --bare: reusing the signed-in login is the point.
  • Nothing reaches GitHub until Publish. The unsaved-changes prompt offers "Save as draft" and
    "Discard changes" — keeping the work costs nothing, so there is no cancel, and Escape keeps it.

Review findings, and what changed for them

SonarQube raised 12 and CodeRabbit 16. All are addressed; the ones that changed behaviour rather
than shape:

  • Main only writes where the user pointed. file:save used to accept any path the renderer sent.
    It now writes only to a path a dialog in this process handed out.
  • Gist ids are checked against the shape GitHub issues, before they reach a request URL or a key
    in the drafts file; a raw_url is followed only when it is GitHub's own host.
  • A truncated gist file is no longer passed off as whole. It used to fall back to the partial
    content, which — edited and published — would have cut the gist down to it. The read fails instead.
  • Drafts are written atomically and one at a time. Overlapping stages each read the same snapshot
    and the later write dropped the earlier one; an interrupted write truncated the file, losing every
    gist's staged work silently. Read-modify-write now happens as a single queued step through a
    temporary file and a rename.
  • A save no longer marks newer text clean, an answer from Claude splices into the document as it
    is when it arrives, and a draft change announced for another gist no longer blanks the open one.
  • Ctrl+Z belongs to the field it was pressed in — it used to run the document's undo from
    anywhere, wiping what had been typed into the Claude instruction.
  • The main process survives its own failures: the window reference is cleared when the window
    closes, and neither startup nor the close prompt can end the process as an unhandled rejection.

Verification

  • 408 unit + integration tests, 100% coverage on every file
  • 5 Playwright-Electron e2e tests, with no fixed waits left in them — each synchronises on something
    observable instead
  • pnpm lint, pnpm typecheck and pnpm build clean

Known gaps

  • No e2e coverage for the GitHub work; it would need a stubbed fetch in main.
  • SidebarItem.icon in the design system takes only an IconName, so the rail's rows are hand-rolled
    Pressables and the GitHub glyph is a local SVG. Worth its own task.

Summary by CodeRabbit

  • New Features

    • Introduced a desktop Markdown editor with live editing, source, split, and preview modes.
    • Added file open/save, Save As, undo/redo, unsaved-change prompts, and dark-theme styling.
    • Added Claude-powered selection and document rewriting.
    • Added GitHub authentication and gist browsing, editing, drafting, publishing, and reset workflows.
    • Added secure credential storage and local draft persistence.
  • Tests

    • Added comprehensive unit and end-to-end coverage for editor, file, Claude, GitHub, gist, and authentication workflows.

soroushm added 10 commits August 1, 2026 17:11
New @soroush/editor workspace app (electron-vite + React 19): hardened
BrowserWindow (contextIsolation, sandbox, no nodeIntegration), CSP response
headers via a unit-tested builder, placeholder renderer, and
lint/typecheck/vitest coverage wiring. Adds the root dev:editor script and
fixes the workspace build-script allowlist key (allowBuilds ->
onlyBuiltDependencies) so electron's binary postinstall is allowed to run.


Result-wrapped ipcMain handlers (native dialogs + injectable fs) with a
shared channel/type contract, a sandboxed CJS preload exposing a typed
editorAPI.file surface via contextBridge, a useDocument renderer hook with
dirty tracking mirrored to main, and a close intercept that prompts before
discarding unsaved changes.
… design-system theming - close #298

Replaces the plain-element shell with the markdown Control/Toolbar/Editor/
Preview compound, themed by createTheme(baseTheme, { syntax: syntaxDark })
since CodeBlock requires theme.syntax. Adds the AppToolbar (Button/
ButtonGroup/Typography) for file actions, self-hosted webfonts plus the
package global reset via a GlobalStyles component, and full unit coverage
for the new components.
…299

useUndoRedo keeps past/future snapshot stacks over the controlled document
value, committing an undo step only after edits settle for 500ms so typing
coalesces instead of producing one entry per keystroke. Bound to
Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z, and Ctrl/Cmd+Y plus enabled-aware toolbar
buttons; history resets when a different file is loaded.
…lose #300

Main-process bridge spawns one-shot `claude -p --output-format json
--permission-mode dontAsk --allowedTools ""` from the OS temp dir so no
repo CLAUDE.md/hooks/MCP config bleeds in, reusing the signed-in user's
own subscription (no --bare, no API key). Every flag is constant and all
user content travels over stdin, so nothing user-controlled reaches the
shell. The ClaudePanel side panel shows the live editor selection, takes
an instruction, and splices the rewrite back over the selection as a
single undo step; CLI-missing/exit/parse failures surface in the panel.
Verified end-to-end against the real CLI.
…n menu

- Playwright e2e: per-test Electron launch fixtures, four specs (render,
  file round-trip, undo/redo, dirty-close prompt), V8 coverage from both
  processes (page.coverage + NODE_V8_COVERAGE) aggregated through
  @soroush.tech/playwright-coverage into coverage/e2e/lcov.info, gated at
  100% on the two unit-coverage carve-outs via coverage:check:e2e
- main-process wiring extracted to unit-tested bootstrap.ts so
  src/main/index.ts stays branch-free glue the e2e can fully cover
- File (New/Open/Save/Save As) and Edit (Undo/Redo + clipboard roles)
  commands moved into the application menu over a typed menu:action IPC
  channel; toolbar buttons and AppToolbar removed; View/Window kept as
  native role menus
- fix dev-mode white page: the CSP now allows the inline react-refresh
  preamble Vite injects in dev (script-src 'unsafe-inline'); production
  stays 'self'-only
- ClaudePanel is now a full-width bar under the editor: with a selection
  Claude rewrites it in place; with none it edits the whole document —
  an empty document generates fresh content from the instruction alone
  (empty selections accepted by the bridge, CLI prompt extended). A
  caption under Ask Claude explains what enables it.
- DocumentEditor gains a Live edit / Edit / Split / Preview toggle group
  (split by default), wiring @soroush.tech/markdown's new LiveEdit into
  the app; selection reporting stays scoped to the source-textarea modes
- the document row scrolls with padding for TextInput's focus ring, and
  the source field caps at maxRows so its border box stays visible
Each cherry-picked editor commit carried feat/editor's lockfile, which
predates the LiveEdit dependencies and the wrangler-tools extraction, so
replaying them onto this branch reverted main's entries for
packages/markdown, packages/wrangler-tools and workers/bench.

Regenerated with pnpm install --lockfile-only, then formatted with
prettier to match the checked-in style. pnpm install --frozen-lockfile
passes again.
…ess token - close #320

- sign-in is a pasted fine-grained PAT scoped to "Gists: Read and write":
  no OAuth app to register and no client id to ship. fetchAccount resolves
  the account through GET /user, which validates the token in the same
  call, so nothing is stored until GitHub accepts it; a 401 names what to
  check instead of reporting a bare status
- credentialStore encrypts { login, token, avatar } with the asynchronous
  safeStorage API - non-blocking, key-rotation aware via shouldReEncrypt
  (the file is rewritten under the new key), and the synchronous pair may
  be deprecated. It refuses to write at all when the OS reports no
  encryption rather than falling back to plaintext, so only ciphertext
  ever reaches userData/github-credentials.bin
- the token crosses the bridge once, on sign-in, and never travels back:
  status returns only { login, avatar }. openTokenSettings always opens
  main's constant URL, so a renderer-supplied URL cannot reach
  shell.openExternal
- the avatar is fetched at ?s=64 and inlined as a data: URI, keeping the
  renderer CSP at img-src 'self' data: with no remote image host allowed
  and no request leaving the app when the rail paints
- the app gains a left Sidebar rail with the account row pinned to its
  bottom: GitHub's mark while signed out, the avatar once connected, and
  the rail's panel column holding the token form. The row is a Pressable
  rather than a SidebarItem - that takes its icon by registry name and the
  mark is a local asset, since a brand logo does not belong in a Material
  Symbols set - so it ports into the panel through the same public
  SidebarContext contract SidebarItem uses
…cal sandbox - close #321

- the rail gains Files and Gists above the account row, all three sharing one
  panel column: picking a gist switches the panel to its files, and opening a
  file loads it into the document tagged with where it came from
- every change to a gist is staged, never sent: Save on a gist file stages its
  content (Save As still writes to disk), Add file stages an addition, the
  row's cross stages a deletion and a staged row's undo clears it. The
  description reads as text behind an Edit button, then edits over several
  lines with explicit Save and Cancel - Enter belongs to the text there.
  Rows carry A/M/D with a change count, Publish sends the whole draft as one
  PATCH, and Reset discards it behind a confirmation: it destroys work that
  exists nowhere else, which staging no longer does
- the unsaved-changes prompt is two buttons, Save as draft and Discard, so
  switching files never forces a choice between losing work and publishing -
  several files can be staged in turn and published together. Escape keeps the
  work, and the document stays put whenever the save could not be completed
- drafts persist to userData/gist-drafts.json keyed by gist id, as plain JSON:
  this is the user's own draft text, not a credential. A draft written before
  the description could be staged is still read, so no staged work is lost.
  The draft is dropped only once GitHub has it, so a failed publish keeps it
- main announces every draft change back to the renderer, because the editor
  and the panel each hold a view of it and only one of them made the change -
  without it, saving in the editor left the panel showing a stale count and
  no way to publish
- empty content publishes as a blank line: GitHub answers 422 for a gist file
  whose content is the empty string, so a file added and published before
  anything was typed into it could never have succeeded. A gist also cannot
  have zero files, which the 422 message now says
- a truncated file is refetched whole from its raw URL rather than loaded
  partially, since saving a partial file would silently drop the rest
- the document name and the window title follow the open file, with an unsaved
  marker, and Save is a visible button rather than only an accelerator
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds a complete Electron Markdown editor with secure startup, typed IPC, Claude editing, GitHub authentication and gist workflows, document editing, renderer UI, and unit/E2E test infrastructure.

Changes

Editor application

Layer / File(s) Summary
Workspace and Electron foundation
apps/editor/*, package.json, pnpm-workspace.yaml
Adds Electron/Vite, TypeScript, ESLint, Vitest, Playwright, coverage, workspace, and security configuration.
Main-process services and IPC
apps/editor/src/main/*, apps/editor/src/shared/*, apps/editor/src/preload/*
Adds secure window creation, CSP handling, file operations, menus, Claude CLI execution, GitHub services, gist storage, IPC handlers, and the typed window.editorAPI bridge.
Renderer document and integration workflows
apps/editor/src/renderer/*
Adds Markdown editing modes, document state, undo/redo, Claude controls, GitHub authentication, gist browsing, draft staging, publishing, and the application shell.
Validation and coverage
apps/editor/src/**/*.test.*, apps/editor/src/test/e2e/*
Adds unit, component, integration, Electron E2E, and coverage support for the new application behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

  • core#295 — Covers the desktop Markdown editor architecture implemented by this PR.
  • core#300 — Covers the Claude CLI bridge, IPC handlers, preload exposure, and renderer panel.
  • core#297 — Covers typed file IPC and useDocument dirty-state behavior.
  • core#321 — Covers gist browsing, local drafts, persistence, and publishing.
  • core#294 — Covers the Electron Claude-integrated desktop writing application.

Possibly related PRs

  • soroush-tech/core#318 — Provides design-system sidebar, pressable controls, and Markdown live-edit functionality consumed by the editor.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR satisfies the workspace, scripts, Electron security, CSP, root command, and verification requirements, but it omits the split TypeScript configuration requested in #296. Add separate app, Node, and scripts TypeScript configurations, or update #296 with an explicit rationale for using one shared configuration.
Out of Scope Changes check ⚠️ Warning The PR adds Claude editing, GitHub gist workflows, draft persistence, and extensive renderer features beyond the scaffolding and security baseline in #296. Move feature implementation to linked follow-up issues, or link issues that explicitly require the editor, Claude, GitHub, and draft features.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the desktop editor, Claude editing, and gist drafts, which are the main changes.
Docstring Coverage ✅ Passed Docstring coverage is 82.22% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch editor-app

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.

@socket-security

socket-security Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedvite@​7.3.6981008298100
Addedelectron-vite@​5.0.09310010084100
Added@​vitejs/​plugin-react@​5.2.010010010095100
Addedelectron@​43.1.110010010098100

View full report

The address for the rest of a truncated file arrives in a response rather than
being written here. It was checked and then used as it came; now only its path
is kept and the request is rebuilt from the host this file names, so anything
else the URL carried - a query, a fragment, a host smuggled into the
credentials - is dropped rather than sent. The fetch also refuses redirects,
which was the one remaining way the request could have left GitHub.

Also drops a second act() that only started a promise: there was nothing to
flush, and the awaited calls after it do that work.
@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@soroush-bench

soroush-bench Bot commented Aug 3, 2026

Copy link
Copy Markdown

Benchmark results

Baseline case: previous · minimum speed ratio: 80%

packages/styled-system/bench/color.bench.ts — ✅ passed

case avg p75 alloc/iter vs fastest
styled-system color() :: local-cjs 428 ns 427 ns 248 B (least) fastest
styled-system color() :: previous 433 ns 432 ns 248 B (+0.0%) +1.2%
styled-system color() :: local-mjs 435 ns 434 ns 248 B (+0.0%) +1.5%

@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
apps/editor/src/renderer/src/common/ClaudePanel/ClaudePanel.tsx (1)

40-45: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve instruction text entered during loading.

The button is disabled while loading, but TextInput is not. A user can enter a second instruction during the first request. Line 44 then clears that new instruction when the first request succeeds.

Disable TextInput while isLoading, or clear only the instruction captured for the completed request.

Suggested fix
         <TextInput
           multiline
           fullWidth
           minRows={2}
+          disabled={isLoading}
           value={instruction}

Also applies to: 78-86

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/editor/src/renderer/src/common/ClaudePanel/ClaudePanel.tsx` around lines
40 - 45, Update the instruction handling in the submit flow around editSelection
and the corresponding TextInput rendering so text entered while a request is
loading is not cleared when the earlier request completes. Prefer disabling
TextInput whenever isLoading is true, or capture and clear only the instruction
submitted by that request, while preserving the existing successful-apply
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/editor/src/renderer/src/common/ClaudePanel/ClaudePanel.tsx`:
- Around line 16-20: Update the request lifecycle around beginEdit and applyEdit
to capture the target document identity, revision, and selected text when Claude
starts, then validate those values against the current editor state before
applying the result. Discard or safely rebase stale results when the document,
revision, or target text changes, preventing edits from being applied to another
document or shifted range.

---

Outside diff comments:
In `@apps/editor/src/renderer/src/common/ClaudePanel/ClaudePanel.tsx`:
- Around line 40-45: Update the instruction handling in the submit flow around
editSelection and the corresponding TextInput rendering so text entered while a
request is loading is not cleared when the earlier request completes. Prefer
disabling TextInput whenever isLoading is true, or capture and clear only the
instruction submitted by that request, while preserving the existing
successful-apply behavior.
🪄 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: CHILL

Plan: Pro Plus

Run ID: ed87018b-936e-4c74-8c82-5f83451bf9d9

📥 Commits

Reviewing files that changed from the base of the PR and between a64c315 and 1065869.

📒 Files selected for processing (10)
  • apps/editor/src/main/github/fetchGistFiles.test.ts
  • apps/editor/src/main/github/fetchGistFiles.ts
  • apps/editor/src/main/github/patchGist.ts
  • apps/editor/src/main/github/toGistId.test.ts
  • apps/editor/src/main/github/toGistId.ts
  • apps/editor/src/main/ipc/gistHandlers.ts
  • apps/editor/src/renderer/src/App.test.tsx
  • apps/editor/src/renderer/src/App.tsx
  • apps/editor/src/renderer/src/common/ClaudePanel/ClaudePanel.tsx
  • apps/editor/src/renderer/src/hooks/useGistDraft.test.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • apps/editor/src/main/github/fetchGistFiles.test.ts
  • apps/editor/src/main/ipc/gistHandlers.ts
  • apps/editor/src/main/github/patchGist.ts
  • apps/editor/src/renderer/src/App.tsx
  • apps/editor/src/renderer/src/App.test.tsx
  • apps/editor/src/renderer/src/hooks/useGistDraft.test.ts
  • apps/editor/src/main/github/fetchGistFiles.ts

Comment thread apps/editor/src/renderer/src/common/ClaudePanel/ClaudePanel.tsx
…d on

The request only remembered the range it asked about, so an answer was
spliced into whatever was at those offsets when it came back. Typing above
the selection, undoing, or opening another file while the request ran put
the rewrite at the wrong place — or into a document Claude never saw.

The text that was sent is now held with the range, and the answer is
applied only while that text is still exactly there. Otherwise it is
dropped and the panel says so, keeping the instruction for a second try.
Static analysis reads the rebuilt address string as carrying whatever the
gist response held, and cannot see that the host was checked in another
function. The protocol and host are now checked against allowed lists
immediately before the fetch, and the parsed URL is passed to it rather
than a string assembled from its path.

Same requests and same rejections as before: only https on GitHub's own
raw host, query and fragment dropped, redirects refused.

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/editor/src/renderer/src/common/ClaudePanel/ClaudePanel.tsx`:
- Around line 24-28: Track the document identity or revision when the Claude
request starts and validate it in App.applyEdit/onApply before applying
rewritten text, so switching to another document with identical content returns
false and preserves STALE_MESSAGE. If document navigation cannot be observed,
prevent document changes while the request is pending. Add an App.test.tsx
regression covering two documents with identical content.
🪄 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: CHILL

Plan: Pro Plus

Run ID: b1f67160-9203-4ab7-b4bc-a4001d6a0d37

📥 Commits

Reviewing files that changed from the base of the PR and between 1065869 and fbc4252.

📒 Files selected for processing (6)
  • apps/editor/src/main/github/fetchGistFiles.test.ts
  • apps/editor/src/main/github/fetchGistFiles.ts
  • apps/editor/src/renderer/src/App.test.tsx
  • apps/editor/src/renderer/src/App.tsx
  • apps/editor/src/renderer/src/common/ClaudePanel/ClaudePanel.test.tsx
  • apps/editor/src/renderer/src/common/ClaudePanel/ClaudePanel.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/editor/src/renderer/src/common/ClaudePanel/ClaudePanel.test.tsx
  • apps/editor/src/renderer/src/App.tsx
  • apps/editor/src/renderer/src/App.test.tsx
  • apps/editor/src/main/github/fetchGistFiles.test.ts

Comment thread apps/editor/src/renderer/src/common/ClaudePanel/ClaudePanel.tsx
Holding the text that was sent catches an answer that no longer fits, but
not one that fits a document it was never about: an empty document and a
new one always hold the same thing, and two files can as well. Opening
another one mid-request would take the rewrite instead.

The document now counts the times it is replaced — a new document, a file
opened, a gist file loaded — and an answer is applied only while that
count is the one the request started on. Editing and saving stay on the
same document, so neither moves it on.

@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: 1

Caution

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

⚠️ Outside diff range comments (2)
apps/editor/src/renderer/src/App.tsx (2)

73-107: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate the target position, not only its text.

current.slice(from, to) === text does not prove that the range still identifies Claude’s target. For foofoo, if Claude receives the second foo at 3..6 and the user prepends bar, the old range still contains foo, but it now selects the first occurrence. This code then replaces the wrong text.

The simpler safe behavior is to reject every response after any content change. If edits after to must remain valid, capture the prefix through start and require it to match before splicing. Add a regression test for this repeated-text shift.

Proposed fix
-  const asked = useRef({ start, end, text: targetText, revision })
+  const asked = useRef({
+    start,
+    end,
+    prefix: content.slice(0, start),
+    text: targetText,
+    revision,
+  })
   const beginEdit = () => {
-    asked.current = { start, end, text: targetText, revision }
+    asked.current = {
+      start,
+      end,
+      prefix: content.slice(0, start),
+      text: targetText,
+      revision,
+    }
   }

-    const { start: from, end: to, text, revision: asWas } = asked.current
+    const { start: from, end: to, prefix, text, revision: asWas } = asked.current

+    if (current.slice(0, from) !== prefix) return false
     if (current.slice(from, to) !== text) return false

As per coding guidelines, run pnpm test:coverage and verify 100% coverage on all touched files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/editor/src/renderer/src/App.tsx` around lines 73 - 107, Update applyEdit
to reject responses whenever the document content has changed, rather than
relying only on current.slice(from, to) matching the requested text; use the
captured target state in asked and live to validate the complete content before
splicing. Preserve whole-document handling and selection updates for unchanged
content, and add a regression test covering repeated text shifting to ensure the
wrong occurrence is never replaced. Run pnpm test:coverage and verify 100%
coverage for touched files.

Source: Coding guidelines


36-37: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset undo history from document boundaries.

filePath does not cover New or Save As. newDocument() and save(true) can replace the document while filePath is still null, so useUndoRedo can persist the previous undo stack under the same path. Depend on revision for document replacements, and add coverage for undo history after New on an untitled document and after Save As.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/editor/src/renderer/src/App.tsx` around lines 36 - 37, Update the
useEffect that calls reset() so its dependency tracks the document revision
rather than only filePath, ensuring New and Save As document replacements clear
the undo history even when the path remains null or unchanged. Add coverage for
undo history reset after New on an untitled document and after Save As.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/editor/src/renderer/src/hooks/useDocument.ts`:
- Around line 28-32: The asynchronous save flow must not apply results to a
replacement document. Use the existing revision state and add a synchronous
document-identity ref that advances before every replacement state update;
capture its value when save starts, and guard post-await error, dirty-state,
origin, and path updates in save so stale results are ignored while preserving
content comparison for edits to the same document. Add deferred file.save and
gists.stage tests covering replacement before completion, then run pnpm
test:coverage and ensure touched files remain fully covered.

---

Outside diff comments:
In `@apps/editor/src/renderer/src/App.tsx`:
- Around line 73-107: Update applyEdit to reject responses whenever the document
content has changed, rather than relying only on current.slice(from, to)
matching the requested text; use the captured target state in asked and live to
validate the complete content before splicing. Preserve whole-document handling
and selection updates for unchanged content, and add a regression test covering
repeated text shifting to ensure the wrong occurrence is never replaced. Run
pnpm test:coverage and verify 100% coverage for touched files.
- Around line 36-37: Update the useEffect that calls reset() so its dependency
tracks the document revision rather than only filePath, ensuring New and Save As
document replacements clear the undo history even when the path remains null or
unchanged. Add coverage for undo history reset after New on an untitled document
and after Save As.
🪄 Autofix

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: CHILL

Plan: Pro Plus

Run ID: 2cd73b71-1503-46e0-8c58-f6818e1c709c

📥 Commits

Reviewing files that changed from the base of the PR and between fbc4252 and 7ed1209.

📒 Files selected for processing (4)
  • apps/editor/src/renderer/src/App.test.tsx
  • apps/editor/src/renderer/src/App.tsx
  • apps/editor/src/renderer/src/hooks/useDocument.test.ts
  • apps/editor/src/renderer/src/hooks/useDocument.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/editor/src/renderer/src/hooks/useDocument.test.ts

Comment thread apps/editor/src/renderer/src/hooks/useDocument.ts Outdated
The merged coverage step ran the root `test:coverage`, which is recursive:
every package, both workers and the editor app ran inside the web job, each
already covered by a job of its own. The editor's jsdom tests shared the
runner with the browser and storybook tiers and timed out at five seconds —
two of them failed there while passing everywhere else.

The step now runs the web app alone. Each app's suites are addressable on
their own for it: the web app's merged pass, and the editor's unit and e2e
tiers with their coverage and per-file gate.

The editor has no CI job yet, so its tests run nowhere in CI until it gets
one; it is still linted and typechecked with the rest of the workspace.
A save reads the path, origin and content it is about, then waits on the
main process. The document can be replaced while it waits, and what came
back was applied to whatever was there: a write finishing after a gist file
was opened handed that file the old path and dropped its origin, so the
next save wrote gist content to disk and there was nothing left to publish.
A failure did the same with its message.

The count of replacements is now a ref, advanced as the replacement is made
rather than through state, which a resolving promise can outrun. A save
holds the count it started on and stops before touching anything when it no
longer matches, and an answer from Claude is held to the same test.
@sonarqubecloud

sonarqubecloud Bot commented Aug 4, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Task] Scaffold apps/editor with Electron security baseline

1 participant