Skip to content

Release v1.7.1 — QoL bug-fix batch - #855

Draft
mrangelmarino wants to merge 13 commits into
mainfrom
release/v1.7.1
Draft

Release v1.7.1 — QoL bug-fix batch#855
mrangelmarino wants to merge 13 commits into
mainfrom
release/v1.7.1

Conversation

@mrangelmarino

Copy link
Copy Markdown
Contributor

Umbrella PR for the v1.7.1 point release: 13 QoL bug fixes and small enhancements from the 2026-08-27 triage run, each landed as an individually reviewed squash commit (green CI + claude review per PR, review-fix rounds included on #836/#837).

Draft until the HITL QA loop passes. Version bump + release notes land here before undrafting.

Closes #801
Closes #827
Closes #790
Closes #832
Closes #806
Closes #830
Closes #826
Closes #829
Closes #804
Closes #799
Closes #798
Closes #797
Closes #792

Also folded in: dependabot #819 was closed in favor of a dedicated dependency pass (45 open alerts, 22 high — separate follow-up).

🤖 Generated with Claude Code

https://claude.ai/code/session_01WqmQeUBBLbtmveh54SMwza

mrangelmarino and others added 13 commits August 30, 2026 23:24
) (#845)

gray-matter bundles js-yaml@3.x which carries GHSA-h67p-54hq-rp68.
gray-matter was used in exactly one file (src/main/google/sync.ts) for
frontmatter parse + stringify during legacy migration and syncAll.

Replace with two small local helpers (parseFrontmatter / stringifyFrontmatter)
that use the top-level js-yaml (^4.x) already in the dependency graph, using
named imports compatible with js-yaml 5.x (PR #819 removes the default export).


Claude-Session: https://claude.ai/code/session_01WqmQeUBBLbtmveh54SMwza

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The two toHaveClass assertions in 'theme toggle changes dark class' used a
2_000 ms timeout while the suite norm is 5_000 ms. The theme flip routes
through an IPC settings round-trip (appearance.mode persistence) before the
renderer applies the dark class, which occasionally exceeds 2s under
full-suite load. Aligns both assertions with the suite-wide 5s budget.


Claude-Session: https://claude.ai/code/session_01WqmQeUBBLbtmveh54SMwza

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…open (#835)

When the find bar was already visible, pressing Cmd+F again called
setIsFindOpen(true) which was a React no-op — the isOpen prop didn't
change so the focus/select useEffect in FindBar never re-fired.

Now, Cmd+F dispatches a 'find:focus' CustomEvent when the bar is open;
FindBar listens for it and immediately focuses + selects the input,
letting the user type a new search term without reaching for the mouse.

Closes #827


Claude-Session: https://claude.ai/code/session_01WqmQeUBBLbtmveh54SMwza

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…rror wrapping (#838)

* chore(tools): fix provenance race, dedup ToolProvenance, wrap plain throws (#790)

- Fix documentId read-before-store-update race in executeCreateAndOpenFile:
  derive newDocumentId from fullPath directly (same derivation as executeOpenFile)
  instead of reading editorStore before its async update settles, preventing
  waitForEditorDocumentContent from polling on a stale id and dropping the annotation.
- Extract ToolProvenance interface into src/renderer/lib/tools/provenanceTypes.ts
  to eliminate the byte-identical copy in file.ts (circular import previously
  prevented importing from index.ts).
- Wrap plain-object throws in unwrapSaveFileResult / unwrapReadFileResult with
  Object.assign(new Error(...), result) so callers get stack traces; isMissingPathFileError
  still matches via isFileOperationFailure (ok/code/path properties are preserved).
- Sanitize toFileOperationFailure message with a stable POSIX-code lookup table
  instead of the raw OS error string, which can carry extra filesystem metadata.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqmQeUBBLbtmveh54SMwza

* fix(tools): revert Error-wrap in unwrapSaveFileResult / unwrapReadFileResult

The Object.assign(new Error(...), result) throw breaks Electron's
contextBridge: the structured-clone algorithm preserves only standard Error
fields (message/name/stack) and drops custom properties (ok, code, path).
Those properties are what isFileOperationFailure and isMissingPathFileError
key on, so after crossing the preload→renderer bridge the thrown value is
no longer recognised as a missing-path error, and the Save-As relocation
path is never taken (confirmed by e2e/electron.stale-paths.spec.ts failure).

Plain-object throws cross the bridge without loss because structured clone
copies all own enumerable properties on plain objects.

The toFileOperationFailure sanitisation is kept — it is safe because
isFileOperationFailure checks ok/code/path (not message), and plain objects
preserve those fields through the bridge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqmQeUBBLbtmveh54SMwza

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…Session (#839)

After any modal dismissal (Not now, Escape, overlay click), restore focus
to the editor and place the cursor at the end of the pasted range so the
user can keep typing without having to re-click into the document.

Adds a "Dismiss this Session" ghost button that sets a module-level
boolean (pasteAnnotationDismissedThisSession) so the prompt is skipped
for the rest of the app session. Follows the same unpersisted, in-memory
idiom as useAnnotationStore's createdThisSession Set — no new persistence
layer, no settings.json change.


Claude-Session: https://claude.ai/code/session_01WqmQeUBBLbtmveh54SMwza

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
)

Relying on Electron's role-inferred accelerators for zoomIn/zoomOut
left ⌘- unresponsive on macOS (⌘+ zoom-in continued to work because
macOS normalises ⌘+Shift+= → ⌘= through the OS menu layer, but ⌘-
was silently dropped before reaching the menu handler).

Fix: pin explicit `accelerator` strings on all three zoom items.
Also add two hidden duplicate items—one for CmdOrCtrl+Plus (covers
the ⌘++ chord on layouts that need Shift) and one for CmdOrCtrl+numsub
(numeric-keypad minus)—so both physical minus keys trigger zoom-out.

Privilege-boundary note: this is a src/main/ change; see PR body.


Claude-Session: https://claude.ai/code/session_01WqmQeUBBLbtmveh54SMwza

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… threads (#830) (#841)

Same-type ProseMirror marks are exclusive, so setComment/setMark on a range
fully covering an existing comment mark silently replaced it, orphaning the
old thread's store entry — the next persistence merge then deleted the
thread (replies included). Marks and store diverged in the meantime, so the
status bar and chat chips rendered contradictory counts.

- setComment now refuses ranges that would fully consume an existing
  comment mark (partial overlaps still split and survive) and fires a new
  onCommentBlocked option; Editor.tsx surfaces it as a notification.
- add_comment pre-checks the resolved range and returns COMMENT_EXISTS
  naming the existing thread id, steering the model to reply_to_comment or
  resolve_comment instead of duplicating.
- All count surfaces (status bar, chat chips) now derive from one
  countOpenThreads selector over the comment store instead of mixing live
  marks with store entries.
- restoreComments warns when legacy same-anchor threads would clobber each
  other on re-mark.
- e2e regression covering the exactly-equal-range add_comment case.


Claude-Session: https://claude.ai/code/session_01WqmQeUBBLbtmveh54SMwza

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#842)

The Comment Review and Quick Review window-level keydown handlers exempted
inputs/textareas but not the TipTap contenteditable, so with a review panel
open, typing in the document body had its arrows advancing threads, Enter
resolving a comment (or accepting a suggestion), and Backspace rejecting.
Both handlers now return early when the event target is content-editable —
review shortcuts stay active only while focus is outside the editor.


Claude-Session: https://claude.ai/code/session_01WqmQeUBBLbtmveh54SMwza

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…isk edits (#829) (#844)

Session restore hydrated every tab from the IndexedDB snapshot without ever
consulting the disk, so files edited outside Prose (another editor, git, an
agent) while the app was closed came back stale — and a save from the
restored tab silently reverted the external changes.

recoverSessionState now stats each tab's file (the existing file:stat IPC)
and compares mtime against the session's savedAt:

- disk unchanged → snapshot, exactly as before (stat failures fall back too)
- disk newer + clean tab → load the disk version (the snapshot has nothing
  the disk doesn't)
- disk newer + dirty tab → genuine conflict: keep the draft (nothing lost
  yet) and raise a persistent notification with a "Load disk version"
  action, since silently picking either side guarantees loss

Runtime watching of open files (the running-app case) is follow-up #843.


Claude-Session: https://claude.ai/code/session_01WqmQeUBBLbtmveh54SMwza

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…799) (#846)

`normalizeMarkdownFootnotesDom` used selector `sup a.footnote-ref`, but
markdown-it-footnote outputs `<sup class="footnote-ref"><a>…</a></sup>` —
the class is on `<sup>`, not `<a>`, so the selector never matched. This meant
`FootnoteReference.parseHTML()` could not find `a.footnote-ref:first-child`
with `data-id`/`data-reference-number`, causing references to fall through to
plain `link` + `superscript` marks. On re-serialize they became HTML soup
(`[<sup>\[1\]</sup>](#fn1)`) instead of `[^1]`.

Fix: change selector to `sup.footnote-ref > a` and stamp `class="footnote-ref"`
plus the required attrs onto the anchor element so the tiptap-footnotes parse
rule matches. Extend `e2e/electron.footnotes.spec.ts` with three deterministic
tests covering the setContent probe, serialization correctness, and the
save→reload file-load path.


Claude-Session: https://claude.ai/code/session_01WqmQeUBBLbtmveh54SMwza

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
#798) (#851)

Override addInputRules() in FootnoteReference to use an end-anchored regex
/\[\^([^\]]+)\]$/ and tr.replaceWith for an atomic replacement, fixing the
off-by-one that left a bare '[' when the trigger immediately followed a word
character (e.g. 'claim[^1]' rendered as 'claim[¹' instead of 'claim¹').

Also adds deterministic e2e coverage in electron.footnotes.spec.ts for both
the abutting ('claim[^1]') and spaced ('claim [^1]') insertion paths.


Claude-Session: https://claude.ai/code/session_01WqmQeUBBLbtmveh54SMwza

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…oot switch (#836)

* feat(explorer): add Duplicate to file context menu + fully clear selection on root switch (#797)

- Add `onFileDuplicate` prop (single-file only, multi-N awaits #796) wired
  through FileTreeProps → FileTreeItemProps → fileContextMenu; calls
  getApi().duplicateFile() via handleFileDuplicate in FileListPanel
- Add `clearSelectionFully` action to fileListStore that zeroes
  selectedPaths, selectedPath, and anchorPath; setRootPath now uses it so
  no residual selection carries over to the new root (clearMultiSelect
  retains the anchor-collapse behavior for post-delete UX)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqmQeUBBLbtmveh54SMwza

* fix(explorer): surface duplicate failures as a toast, route setRootPath through clearSelectionFully (#797)

Review follow-ups on #836: duplicate errors were console-only — now they
show via notificationStore like other file operations (file-op failures
cross the bridge as plain objects, so the message is extracted defensively).
setRootPath now calls clearSelectionFully instead of duplicating its field
assignments inline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqmQeUBBLbtmveh54SMwza

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(editor): add bullet list marker preference (dash vs asterisk)

Adds a bulletListMarker setting to Settings → Editor so users can choose
between '-' (default, preserving existing behaviour) and '*' (asterisk).
The preference wires through the Settings type, default settings, the
tiptap-markdown Markdown extension config, and a live useEffect that
patches the extension's options object so the change takes effect on the
next save without an app restart.

Closes #792

* fix(editor): write bulletListMarker to editor.storage.markdown, not extension options (#792)

Review follow-up on #837: tiptap-markdown's onBeforeCreate shallow-copies
options into editor.storage.markdown, and the bullet-list serializer reads
that copy at serialize time (verified in tiptap-markdown source). Mutating
the extension's options object was a no-op — live setting changes only took
effect after an editor remount. Target the storage copy instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WqmQeUBBLbtmveh54SMwza

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@mrangelmarino

Copy link
Copy Markdown
Contributor Author

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@mrangelmarino

Copy link
Copy Markdown
Contributor Author

/review

Auto-triggered after E2E tests passed.

@claude

claude Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Code Review

PR #855 — Release v1.7.1 QoL batch. Covers 13 fixes across comments, footnotes, file ops, session restore, and editor UX. Reviewing for correctness, security, and performance.

Security

  • src/shared/fileOperationResult.ts — The new sanitizeFileErrorMessage replacing raw OS error strings is a positive security hardening. POSIX error codes are mapped to stable, path-free messages, preventing accidental filesystem metadata leakage through IPC error surfaces. ✓

  • src/main/google/sync.tsgray-matter removed in favor of a local parseFrontmatter using the already-present js-yaml v4. The yamlLoad() call uses the default safe schema in v4 (no !!js/function type), so YAML deserialization is safe. The regex FRONTMATTER_FENCE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/ makes the trailing newline after the closing --- optional (\r?\n?). This is slightly more permissive than gray-matter (which requires the closing delimiter to be on its own line with a trailing newline), but the behavioral difference only appears on malformed input that gray-matter would reject anyway. Acceptable. ✓

  • No Electron privilege-boundary files touched (contextIsolation, nodeIntegration, preload, BrowserWindow configs unchanged). Standard security checklist: ✓

Correctness

findFullyCoveredCommentIds + setComment guard (comment clobber fix, #830)

The logic in extension.ts is correct: a comment ID is "fully covered" iff every ProseMirror text node carrying that mark lies entirely within [from, to]. The intersecting/survives set tracking handles split-able marks (partial overlap) cleanly. The dual-check (tool executor in document.ts + extension in extension.ts) is intentional: the executor returns a structured COMMENT_EXISTS error to the Claude model; the extension fires onCommentBlocked for the UI notification path. Not redundant — they serve different call sites.

Session snapshot staleness check (#829)

isSnapshotStale calls window.api?.fileStat, which is correctly defined in the preload (src/preload/index.ts:433) and main (ipc.ts file:stat handler). session.savedAt is already a field on SessionState (persistence.ts:54). The graceful fallback (return false on missing API or error) is correct: an undetectable-stale snapshot falls back to existing behavior. ✓

parseFrontmatter round-trip

stringifyFrontmatter produces ---\n[yaml]\n---\n[content]. The regex matches this and recovers the yaml block correctly. Verified round-trip is lossless for well-formed input. ✓

isSnapshotStale — clean tab fast path

if (!tabDraft.isDirty) {
  const disk = await readDocumentFromDisk(tabDraft.path)
  if (disk) { restoredContent = disk.content; restoredFrontmatter = disk.frontmatter }
}

When the disk read fails, this silently falls through to the snapshot. That is the correct behavior per the comment ("Falls back to the snapshot on read failure"). ✓

Footnote selector fix (#799)

sup a.footnote-refsup.footnote-ref > a with anchor.classList.add(footnote-ref) stamped onto the anchor. This matches what markdown-it-footnote actually renders and what FootnoteReference.parseHTML() expects (a.footnote-ref:first-child). The test suite directly probes ProseMirror node types post-setContent, which is the right isolation level for this bug. ✓

New FootnoteReference input rule (#798)

The override uses find: /\[\^([^\]]+)\]$/ (anchored to $) and tr.replaceWith(range.from, range.to, type.create({ data-id: crypto.randomUUID() })). The UUID is for the node identity, not the displayed footnote number — numbers are assigned dynamically by the extension view. The atomic replaceWith avoids the deleteRange + addFootnote cursor mis-placement of the base package. ✓

executeCreateAndOpenFile annotation fix

Using generateIdFromPath(fullPath) instead of reading editorStore before its async update settles is the right fix. Eliminates the race where the annotation attached to the wrong documentId. ✓

ContentEditable guard in review panels (#826)

if (e.target instanceof HTMLElement && e.target.isContentEditable) return

Added to both QuickReviewPanel and CommentReviewPanel. Correctly gates keyboard shortcuts so typing in the TipTap editor body does not trigger Accept/Reject/Next actions. ✓

Minor Observations

  1. opeChange and "Not now" button — The "Not now" inline onClick and the dialog onOpenChange both attempt cursor restoration. Because React batches the state update from setPendingPasteAnnotation(null) before onOpenChange re-renders, the onOpenChange closure sees pendingPasteAnnotation === null for button-triggered closes (so toPos = null, no double-restoration). Escape-key closes correctly see the old non-null value. The ordering is safe, though it reads like a subtle trap.

  2. onFileDuplicate?.(item.path) in FileTree.tsx:860 — The ?. optional call is redundant since the enclosing && already guards truthiness. Harmless style nit.

  3. node!.id in the E2E clobber test (electron.comment-threading.spec.ts:31) — If node is undefined (document has no text node longer than 5 chars), the test throws a JS error rather than a clean Playwright assertion failure. Acceptable risk for a controlled fixture file, but a expect(node).toBeDefined() guard would produce a clearer failure message.

Summary

The implementation is solid. All the bug fixes are targeted and the rollout strategy (dual-check, graceful fallbacks, store-as-source-of-truth for comment counts) reflects good defensive engineering. No blocking issues found.

@github-actions

Copy link
Copy Markdown
Contributor

✅ Ready to Merge

Code review found no significant issues. This PR is ready for human approval and merge.

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