Skip to content

Release 1.14.0: work that can't be lost, files kept as they were, a locked-down preview - #21

Merged
VagueDustin merged 35 commits into
mainfrom
release/1.14.0
Oct 4, 2026
Merged

VagueDustin merged 35 commits into
mainfrom
release/1.14.0

Conversation

@VagueDustin

Copy link
Copy Markdown
Owner

Fixes everything in the 1.13.4 code review (6 critical, 10 high, 21 medium, 25 low findings) and adds the features it proposed. The full change list is the new CHANGELOG.md entry.

Highlights

  • Copying from the preview gives clean results: every code block has a Copy button (exact text, no trailing newline, CRLF on Windows); code copies as plain text, prose without the theme, and maths as TeX. The TeX repair pass no longer rewrites code.
  • Data safety:
    • Relative link clicks and Ctrl+R can no longer replace the app.
    • Unsaved Markdown survives Edit/View switches and disk changes.
    • Hot exit backs up unsaved work.
    • Saves are atomic.
    • A changed-on-disk bar offers Reload, Keep mine or Compare.
    • Crash-safe rendering, with a recovery screen.
  • File formats: encodings (UTF-8 with or without BOM, UTF-16 LE/BE, Windows-1252), line endings and indentation are detected, preserved and switchable from the status bar.
  • Security and privacy:
    • Locked navigation and openExternal policy.
    • fate-local serves local images only (no UNC/SMB, so no NTLM leak).
    • The sanitizer forbids styles, forms and app classes, with an SVG profile for KaTeX.
    • A build-time CSP.
    • Remote images are opt-in.
    • No Hunspell dictionary download on Linux.
    • IPC hardening: saves only to open files; settings key whitelist.
  • Features:
    • Application menu with Open Recent and the Jump List.
    • Right-click menus for content and tabs.
    • Palette modes :, @ and #.
    • Find in the reading view.
    • Links between documents.
    • Scroll-synced edit preview.
    • Large-file and log follow mode.
    • Spell check in Markdown edit mode.
    • PowerShell, batch and Dockerfile highlighting.
    • Per-document indentation.
  • Release pipeline:
    • The signing key is confined to a publish job that runs no npm or third-party code, with a fingerprint check.
    • Third-party actions are SHA-pinned.
    • Tag/version, draft and prerelease guards.
    • deb/rpm remove and posttrans scripts.
    • NSIS per-user cleanup.
    • Store file types.
    • app.asar drops from 211 MB to 13 MB.
    • A new checks.yml runs on PRs.

Testing

  • npm run lint: clean, now including electron/**/*.cjs and scripts/**.
  • npm test: 269/269 node --test cases. They cover file formats, atomic saves, live reload, backups, the settings whitelist, link security, the app menu, spellcheck, launch arguments, palette modes, language detection, shortcuts, indentation, preview paths, maths, deb/rpm scripts, the signing keyring, the appx manifest and the Windows registration lists.
  • vite build passes.
  • Real-app end-to-end runs on Linux against the integrated tree:
    • copy suite: 18/18
    • file I/O suite: 35 checks
    • renderer data-safety scenarios
    • format round-trips on pristine fixtures
    • app-ux suite: 56 checks
    • per-workstream suites for preview, main shell and components
  • A dedicated QA pass is running on the final tree.

Only a real release can prove

  • The new publish job end to end.
  • Snap Store upload.
  • deb/rpm scripts on real upgrades (posttrans after 1.13.x).
  • The NSIS uninstaller on Windows.
  • Store certification with the extra file types.

Windows-only paths (UNC refusal, the native spellchecker, Explorer arguments) are unit-tested but not run.

Not done

actions/setup-node and actions/upload-artifact are still referenced by @v4 (marked TODO(pin)). Resolving their SHAs was blocked in this environment.

🤖 Generated with Claude Code

VagueDustin and others added 30 commits October 4, 2026 05:54
…g code

Copying from the reading view carried the theme: Chromium's native copy
inlined every computed style into text/html (navy background, near-white
text, gold border), so Word, Outlook, Teams and mail pasted dark boxes or
white text on white. And the TeX "repair" pass ran over the whole
document, so a tab before `auth`/`ext…` became a literal `\t` and `\ `
became `\\ ` inside code blocks and inline code, which is what a copy
then put on the clipboard.

- Code blocks get a Copy button: exact source, no trailing newline, CRLF
  on Windows. Selections inside code copy plain text only; other
  selections copy clean, unstyled HTML; KaTeX copies as TeX source.
- TeX repairs run only inside maths (output byte-identical on 16 maths
  cases); a leading BOM no longer hides the first heading.
- Right-click menu (Cut/Copy/Paste/Select All, Copy Link Address);
  Ctrl+A in the reading view selects the document, not the window.
- Memoize dangerouslySetInnerHTML objects: React 19 compares them by
  identity and rewrote every preview on each re-render, wiping Mermaid
  diagrams and the user's selection on window focus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API surface the 1.14.0 work builds against, fixed up front so the
main-process, renderer and preview changes can land in parallel:

- preload: file formats (encoding/BOM/EOL owned by main), onFileDeleted,
  reopenWithEncoding, hot-exit backups, openExternal, showItemInFolder,
  showTabContextMenu, onMenuCommand/updateMenu, setSpellcheck.
- CodeEditor exposes getView(); MarkdownEditView moves to its own module
  and passes the last-saved text (savedContent) as the editor baseline.
- MarkdownView receives isVisible (active tab or split pane).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three Electron-free modules for the main process, each with node --test
coverage in test/ (run: node --test test/):

- fileFormat.cjs: decode/encode with the file's format
  { encoding: utf8|utf16le|utf16be|windows1252, bom, eol }. BOM sniffing,
  BOM-less UTF-16 by its NUL pattern (conservative), strict UTF-8, else
  Windows-1252; line breaks normalised to \n with the dominant EOL
  recorded. The binary sniff now runs after UTF-16 detection, so
  PowerShell's UTF-16 output is no longer refused as binary. Round trips
  are byte-exact (unpaired surrogates and the five undefined 1252 bytes
  included); Windows-1252 refuses unencodable text with UNENCODABLE,
  naming the character and line. (H1, H4)
- textFiles.cjs: async readTextFile (size cap, NOT_FILE/TOO_LARGE/BINARY)
  and writeFileAtomic: temp file in the same folder, fsync, rename over
  the target. Symlinks resolved (the link stays a link), mode bits and
  owner kept, the Windows mark of the web carried over, read-only files
  still refused; in-place fallback for hard links, refused renames and
  folders that won't take a temp file; never leaves the temp file. (H5)
- fileWatch.cjs: per-path watchers that coalesce bursts (one reload per
  atomic save by another editor), follow replaced files, retry deleted
  ones with backoff and report them once if they stay gone, survive
  'error' events, deliver only the newest read, and hold reads while
  FATE's own save is writing. (M10, low: three change events)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… errors

- backups.cjs: userData/backups/<id>.json written atomically (0700
  folder, 0600 files). Ids validated against /^[A-Za-z0-9_-]{1,64}$/,
  fields type-checked, sizes and count capped, calls per id serialised so
  a remove can't be overtaken by an in-flight write. list() skips corrupt
  or foreign files instead of failing; only remove() and clear() delete.
- rendererSettings.cjs: the keys store-get/store-set accept from the
  renderer (today's settings plus spellcheck and remoteImages), each
  with a cheap type check and a size cap. Main-only keys such as
  recentFiles and registrationStamp are out of reach. (M14)
- fsAccess.cjs: plain wording for common errors (disk full, read-only
  drive, file in use, ...) so save failures never show Node's raw text
  or an atomic save's temp path; every result also carries a short
  `reason` and the snap `advice`, for one-dialog summaries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eload

Wires the new modules into main.cjs's file I/O regions:

- Open, reload and reopen decode via fileFormat: the renderer gets \n
  text without a BOM plus meta.format; fileFormats (per watchKey) is
  recorded on open, save and reload and dropped on close-file. A CRLF
  file rewritten with the same text no longer sends file-changed (the
  watcher compares a digest of normalised text). (H1, H4)
- save-file / save-file-as take (..., content, format?): the passed
  format, else the recorded one, else the platform default; they resolve
  with the format written, or { ok: false, code, error } in plain words
  (UNENCODABLE for Windows-1252). Saves are atomic and async. (H5)
- New IPC reopen-with-encoding(path, encoding); the choice sticks for
  the tab's later reloads.
- Live reload via fileWatch: one event per atomic save, deleted files
  retried then reported with file-deleted, re-armed after our own saves,
  held while saving so a racing reload can't echo. (M10)
- No sync file access on hot paths: opens, reloads, saves and the
  recents existence checks are async (parallel, 1.5 s cap). Opens are
  delivered in request order so restored tabs keep their order, with a
  2 s cap so one dead network share can't hold the rest back. (M12)
- save-file only writes paths FATE has open (any open route, or chosen
  in Save As); store-get/store-set only take renderer settings keys. (M14)
- backup-write/remove/list/clear IPC over backups.cjs (hot exit).
- Recents: app.emit('fate-recents-changed', list) on every change, Jump
  List entries on Windows (cleared with the list), malformed stored
  entries ignored.
- Session restore failures are collected into one summary dialog,
  keeping the snap guidance, instead of one modal box per file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keeps main.cjs free of a module-level `crypto` binding, which would
collide with a `const crypto` added by another change to the same file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… sniffing

Command palette
- New src/paletteModes.js: createPaletteModes(ctx) returns the ":" (go to
  line[:col], clamped), "@" (symbols from the Lezer tree, or Markdown
  headings scanned from the text in a reading view) and "#" (search every
  open tab, 20 hits per tab, 200 in all) modes. Pure helpers are exported
  and unit-tested against the real bundled grammars.
- CommandPalette takes `modes` and `initialQuery`, shows the active mode as
  a badge and a quiet ": go to line · @ symbol · # search open tabs" footer
  on an empty query, and understands disabled/error rows.
- Dialog semantics (aria-modal), combobox + listbox with
  aria-activedescendant/aria-selected, focus trap and focus return.

Settings
- initialSection prop; a new Markdown page with the spell check and remote
  images switches (with the Linux dictionary note and the privacy note).
- Live bindings everywhere ("New files (Ctrl+T)", not a hard-coded Ctrl+N).
- Sidebar width commits on blur/Enter only, clamped to 200-600; Escape
  reverts; junk input is ignored.
- Real modal: role/aria-modal/aria-labelledby, focus in on open, Tab
  trapped, focus back to the editor on close, Escape handled by the dialog
  (it no longer closes a diff behind Settings first), background inert.
  Every switch, select, slider and field is named by its label.
- The shortcut recorder stops when Settings closes; Escape cancels it.

Shared
- src/dialogFocus.js: the modal focus hook both dialogs use.
- settingsMeta.js: parseShortcut, normalizeBinding, bindingFromEvent,
  matchesShortcut, isAllowedShortcut, shortcutParts, formatShortcutLabel,
  toElectronAccelerator, clampSidebarWidth. The + key is stored as "Plus"
  (legacy "Control++" parses and is migrated by resolveShortcuts, which
  also drops bindings that would fire while typing).
- languageDetect.js: logs starting "[2026-10-04 …]" or "[INFO]" are no
  longer sniffed as JSON; isJsonWithComments(fileName) for the linter;
  JSONC names the registry doesn't know open as JSON.

DiffView and Starfield
- DiffView: leftFileName/rightFileName pick each side's language. It also
  called EditorState.appendConfig (undefined), so no diff was ever
  highlighted; now StateEffect.appendConfig.
- Starfield pauses on window blur as well as when hidden, draws at most
  30 fps with time-based motion, and builds its gradients and the meteor
  sprite once per build instead of every frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A "#" hit's detail is a file name and line ("notes.md:3"); the section
labels' small caps turned it into "NOTES.MD:3", which misreads a
case-sensitive name. Only an error row's icon turns red now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reload, backups)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d, shortcut helpers)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, spellcheck privacy

Links and navigation (C1, low: target=_blank and mailto:)
- will-navigate allows only the exact entry document (dev: the Vite origin);
  a relative link such as [x](CONTRIBUTING.md) no longer replaces the app.
- One link policy (electron/linkSecurity.cjs) for navigations, window.open,
  the print preview and the new 'open-external' IPC: http(s) after the
  existing confirmation, mailto: directly, everything else refused.
- The print preview gets a navigation guard and its own menu (Ctrl+W).

Application menu (C2)
- electron/appMenu.cjs: File, Edit, View, Help with Open Recent; no Reload or
  Force Reload; DevTools only when unpackaged. Renderer items send
  'menu-command' and show the user's bindings from 'update-menu' as
  display-only labels. Rebuilt on 'update-menu' and 'fate-recents-changed'.
- will-prevent-unload asks before a blocked reload or close; after the
  save-or-discard walk the close goes ahead without asking twice.
- Check for Updates shares the IPC's logic and reports why nothing happens.

fate-local (C5): network, relative and non-image paths are refused before any
file system access; the scheme drops supportFetchAPI, bypassCSP and stream.

Single instance (M11): lock taken right after the userData override; the
losing instance does no whenReady work and creates no window. Every argument
opens, relative to the sender's working directory, file: URIs included, and a
handoff that arrives while the page loads is queued.

Spellcheck: off by default outside Windows. Measured on Electron 42, only an
empty language list stops the Hunspell download from Google's servers;
setSpellCheckerEnabled(false) and webPreferences.spellcheck do not. New
'set-spellcheck' IPC; suggestions and Add to Dictionary in the context menu.

Also: updater promises caught (M13), stale temp files removed at launch,
one in-flight font enumeration, show-item-in-folder and show-tab-context-menu
IPC, safeFileStem regex without control-character literals.

Unit tests: node --test (60 tests for the five new pure modules).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… files

CodeEditor measures "dirty" against a baseline, the text last known to be
on disk, instead of whatever it was mounted with:

- `savedContent` prop: the baseline when it differs from the initial text.
  A Markdown tab coming back into Edit mode with unsaved text now starts
  dirty and says so at mount (C4), so isDirty(), getSavedContent(), the
  close guards and the quit walk agree.
- getDocSnapshot()/markSaved(snapshot): a save marks exactly the document
  it wrote as clean; keys typed while the write is in flight stay dirty (M4).
- replaceContent(text, baseline?) replaces only the span that changed
  (src/textSpan.js), and nothing at all for identical text, so a reload
  keeps the caret, folds and undo history (M5). setBaseline() and
  scrollToEnd() serve the changed-on-disk choices and log following.
- `largeFile` drops lint, bracket matching, autocompletion and
  selection-match highlighting; `plainText` never loads a language.
- The syntax-error linter really stops at 200 diagnostics, and only runs
  for languages chosen by file name, not sniffed from content (M8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dary

- fileKinds: .txt is no longer Markdown; it opens in the editor. Markdown
  over 2 MB opens as plain text (kindForOpen) and large-file mode starts
  at 5 MB. couldBeMarkdown() says when "Render as Markdown" is offered.
- renderSafely(): rendering that never throws; a failure degrades to the
  escaped text in a <pre> (H3: a 10 MB log overflowed marked's stack
  inside a state update and blanked the window).
- A root error boundary shows a recovery screen instead of a blank
  window. As it catches, it writes every unsaved buffer to the hot-exit
  backups (hotExit.flushForCrash), so Reload loses nothing, and it lets
  the window close without the unmounted app's quit walk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
App.jsx (renderer-docs workstream, 1.14.0):

- C3: a dirty tab keeps its text when its file changes on disk or is
  opened again, in Reading view too, and shows a changed-on-disk bar:
  Reload, Keep mine, Compare (diff with file names for highlighting).
  A deleted file gets "Keep editing" / "Close tab"; Save recreates it.
- C4: Markdown tabs track savedSource (the text on disk) and hand it to
  the editor as its baseline; leaving Edit mode keeps the dirty state.
- H3: no rendering inside state updaters; failures fall back to plain
  text with a status message. "Render as Markdown" for .txt,
  extensionless and oversized .md tabs; Follow (status bar) for logs.
- H6: docsRef/activeIdRef/splitIdRef update synchronously with state,
  so two opens of one file before a commit make one tab; session paths
  are de-duplicated when saved and when restored.
- Hot exit: each dirty tab is backed up about a second after it changes
  and the backup removed on save, close, discard or becoming clean;
  startup restores backups (even with session restore off), file ones
  as each file's tab opens, Untitled ones as tabs. An unload guard
  blocks reloads over unsaved work; the quit walk stands it down.
- Line endings and encodings: tabs carry `format` (from meta.format, or
  the default for Untitled), pass it on every save and adopt the result.
  Status bar: EOL toggle, encoding menu (Save with / Reopen with
  encoding), UNENCODABLE errors; a format-only disk change is followed.
- M3: file drags are handled in the capture phase, so CodeMirror no
  longer pastes a dropped file into the editor. M4 save snapshots.
- L14: Save As onto a path open in a dirty tab asks first; keeping keeps
  that tab as an unsaved copy. L15: closing a dirty tab offers Save /
  Don't save / Cancel. L17: Edit mode prints the rendered document.
- Application menu commands, updateMenu({ shortcuts }), and the tab
  right-click menu (copy path, reveal, close, close others/to the right).
- saveFile's NOT_OPEN (a restored tab whose file was gone) falls back to
  Save As; Save As onto another tab's path never sends closeFile for it.

New: components/DocBar.jsx, components/FormatStatus.jsx, docFormat.js,
and node --test coverage in test/rendererDocs.test.mjs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, hot exit, disk-change bar, formats, large files)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…urity, single instance, spellcheck privacy)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… find, scroll sync

Links (C1): every click on a link in a preview is handled by FATE instead of
navigating the window. #fragments scroll within the preview (GitHub-style
heading slugs, or the user-content- ids the sanitiser now makes); relative
and absolute paths open as tabs, resolved against the document
(data-doc-path), and other.md#frag lands once that tab shows; http(s) and
mailto go to openExternal; anything else, network paths included, is
ignored. Middle and modified clicks no longer open windows.

Sanitiser (H2, H7): SVG profile on, so KaTeX's radicals, vector arrows,
accents, braces and tall delimiters render. No style tags, forms or
controls (inert task-list checkboxes excepted), dialogs, popovers, invoker
commands, frames, objects, link/meta/base or SVG image. style and class
survive only on KaTeX's own output (stamped per run, so raw HTML can't
claim the exemption), class also on highlighted code; ids and names become
user-content-*. Mermaid's SVG goes through DOMPurify too, keeps only CSS
scoped to the diagram, and can't fetch anything while it renders.

Images (C5, M6): //host and UNC paths are never local; remote images are
placeholders (alt text + host) until loaded from the new bar ("Load
images" / "Always load"), and renderMarkdown returns remoteImageCount.
Post-processing runs in an inert document, so rendering fetches nothing.
Local paths are decoded once with ?query/#hash dropped; Windows drive paths
work.

CSP: a <meta> injected into the built index.html (not in dev). Mermaid
needs no unsafe-eval, and Electron's insecure-CSP warning is gone.

Maths: the `\ ` repair applies only to formulas with other corruption (M9);
"costs $5 and $10" stays prose (L7). PowerShell, batch and Dockerfile
fences are highlighted.

Layout: Mermaid renders in the split pane and the edit preview (one shared
hook, cached per source, debounced in edit mode) and leaves nothing behind
when a diagram fails (L1); the contents resizer measures from its own pane
(L2); the active heading is worked out from a real layout when a pane shows
(L6); contents entries are keyboard-reachable buttons.

Find in the reading view: Ctrl+F or the fate:find event; "3 of 12",
Enter/Shift+Enter, Escape closes the bar without reaching App's Escape
binding; CSS Custom Highlight API; hidden in print.

Edit mode: the preview follows the editor's scrolling block by block
(data-line annotations), interpolated within blocks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Open Recent goes through openRecentFile (which prunes a missing entry),
Clear Recently Opened through clearRecentFiles (the Windows Jump List
too), and Open Containing Folder only reveals files FATE has open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t-in, find, scroll sync)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every renderer package sat in "dependencies", so electron-builder copied
each one into app.asar as node_modules although Vite had already bundled
it into dist/. app.asar was 211.7 MB; it is 13.4 MB now. The main process
requires only electron-store and electron-updater (grep over
electron/*.cjs); everything else moves to devDependencies. The lockfile's
root entry and dev flags follow (npm ls --package-lock-only is clean, and
its production tree is exactly those two packages and their own deps).

Measured with electron-builder --linux dir on both dependency sets, and the
packaged app was started on a Markdown file: it renders, KaTeX and Mermaid
included, with nothing on stderr but electron-updater's AppImage notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Linux (H9, M15)
- deb.afterRemove / rpm.afterRemove replace electron-builder's default
  after-remove template, so they repeat it (update-alternatives removal,
  AppArmor unload and delete) and add FATE's part. The .deb's postrm
  deletes /etc/apt/sources.list.d/fate.sources on remove and purge when it
  carries a FATE header (the package's own, or the one the README has
  people download): its Signed-By keyring goes with the package, and
  apt update failed after every uninstall. The .rpm's postun does nothing
  during an upgrade ($1 >= 1), which is what deleted /usr/bin/FATE on
  dnf upgrade; on erase it removes the alternative and the fate.repo the
  package wrote.
- A posttrans scriptlet (--rpm-posttrans) puts /usr/bin/FATE back after
  the unguarded postun of 1.13.x and older, so the upgrade to this version
  keeps the command too.
- The public signing keyring is committed (build/linux/fate-archive-keyring
  .asc/.gpg, identical to the assets on the apt release). The packages
  ship it from there, so a fresh clone builds them without downloading
  anything.

Windows (M16, M19)
- customUnInstall removes the per-user registration the app's self-heal
  writes, on a real uninstall only: the HKCU FATE.<ext> ProgIds and
  Open-with values, the HKCU "Markdown Document" open command when it
  still points into $INSTDIR, and the per-user install key.
- preInit is empty in the uninstaller-building pass, which electron-builder
  runs on the build machine.
- scripts/appx-manifest.cjs (replacing escape-appx-manifest.cjs) gives the
  Store package .markdown, .txt and every code type installer.nsh
  registers, read from installer.nsh at build time; never .bat or .cmd.
  NSIS is untouched: it still takes the default for .md only.

npm test (node --test) runs the new tests: the maintainer scripts executed
against a scratch root with stubbed tools, the Store manifest transform,
and the code-type lists across main.cjs, fileKinds.js and installer.nsh.
installer.nsh also compiles with makensis 3.0.4 in both passes.

The AppStream entry for 1.13.2 gets its real date (the tag, 2026-09-14).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… PRs

Build Linux (C6, H8, M18)
- build (npm ci, lint, test, electron-builder; no secrets), snap (snapcore
  action-build; no secrets), publish (the only job with the signing key:
  downloads the artifacts, signs, attaches, writes the apt/dnf indexes;
  runs no npm and no third-party action), snap-store (snapcore
  action-publish with the store token only), test-repos, flatpak.
- Permissions default to contents: read; only the publish jobs get write.
  Checkouts use persist-credentials: false. Actions are pinned by commit
  SHA, except actions/setup-node and actions/upload-artifact, which are
  still on @v4 (marked TODO(pin): their SHAs could not be resolved here).
- Guards: tag == v + package.json version before building; a recovery run
  checks out the tag; inputs reach scripts only through env. apt, dnf and
  the Snap Store are updated only for a published, non-prerelease release
  that is the repository's latest; a semver prerelease must be a GitHub
  prerelease. A draft gets its files, then the job stops with an error.
- The signing secret must hold exactly the keys named in SIGNING_KEYS and
  each must be in the committed keyring (M21); otherwise nothing is signed.
  Several keys are supported for a rotation (all sign the indexes, the
  first signs the rpm).
- Assets already on a release are kept unless the force input is set. The
  AppImage, .deb, .rpm and latest-linux.yml go up as one unit, binaries
  first and latest-linux.yml last; the indexes are built from the packages
  on the release. latest-linux.yml restates the signed rpm's sha512/size.
- One fixed concurrency group for publishing, timeouts on every job,
  artifact names without "/", and the Flatpak lint fails on any error but
  the four every non-Flathub build reports (listed with reasons).

Build Windows: the same guards and split (build on Windows read-only, attach from
an ubuntu job that runs nothing but gh), the .exe.blockmap is attached for
differential updates, latest.yml goes last, the PR paths cover the
lockfile, electron/**, fileKinds.js, the icon scripts and brand/, and the
build checks the Store manifest's file types.

Checks (new, M17): npm ci, lint, npm test, vite build, an unpacked Linux
package whose app.asar must hold the main process's packages and none of
the renderer's, and a start under xvfb. ESLint now covers electron/*.cjs
and scripts/ (2 errors in electron/main.cjs at base, left for the owners).

Scripts: setup-signing-key.sh captures `gh secret list` instead of piping
it into grep -q, stops when gh fails, never writes over a key backup, and
gains --new-key-only for rotations. setup-msstore-credentials.ps1 runs on
Windows PowerShell 5.1 and no longer claims a workflow submits to the
Store.

Docs: the release flow and guards (README), the signing key, its
fingerprint and the rotation procedure (SECURITY.md), the snap's limits
with hidden and system files (M20), npm test and the checks
(CONTRIBUTING). flathub.json enables automerge for the bot's PRs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a draft

The snapbuild artifact is read only by the snap job, but one day was
shorter than a "re-run failed jobs" may come. A publish run that stopped
at a draft can be finished by publishing the release and re-running that
job, which the error message and the README now say.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The publish jobs run no npm and no third-party action, but they do use
GitHub's own checkout and download-artifact (pinned); the header comments
now say so instead of "only gh".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…age scripts, smaller packages, PR checks)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…SONC

- indentDetect.js (pure, unit-tested): tabs or spaces and the width, by the
  majority of indented lines and the most common indent step; Makefiles and
  Go always use tabs; otherwise the "Indent size" setting (M7).
- CodeEditor takes the document's indentation (indentUnit and tabSize, live
  through a compartment), and spell checking from EditorPrefsContext behind
  its own compartment, so toggling needs no remount. JSON-with-comments files
  (tsconfig.json, .jsonc, VS Code settings) are no longer syntax-checked.
- StatusMenu: FormatStatus's status-bar popover (portal, keyboard, Escape)
  as a shared component; IndentStatus ("Spaces: 4" / "Tab size: 4") uses it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mbing

- M1: a bare Escape only acts as a shortcut when focus is on nothing in
  particular; editors, fields, buttons, menus, find bars and dialogs keep it.
- M2/L9: the shortcut recorder only listens while Settings is open, takes
  bindingFromEvent/isAllowedShortcut, and the shared matchesShortcut and
  label helpers replace App's own parser (the + key works as "Plus").
- M7: each document's indentation is detected on open and shown and
  changeable in the status bar; Save As to a Makefile or Go file uses tabs.
- L3: the focused split pane's document is the one commands act on (save,
  close, Edit/View, print/export, find, undo, the header, the status bar and
  Ln/Col) without changing the active tab; activating the split pane's
  document swaps the panes instead of leaving a one-pane split.
- L5: a diff closes when the tab changes, and with its documents.
- Palette ":" "@" "#" modes with revealLine (editor caret, or the reading
  view scrolled to the block's data-line); one openPalette/openSettings
  mechanism for every entry point.
- Settings: spellcheck and remoteImages at boot, setSpellcheck at boot and
  on change, spell check on the Markdown editor only, remote images through
  every render and MarkdownView's remoteImagesAllowed/onAllowRemoteImages.
- L18: the boot effect runs once per page, and updateSetting's updater is pure.
- Tab strip follows the WAI-ARIA tabs pattern (roving tabindex, arrows,
  Home/End, Enter/Space, Delete), with the close button outside role="tab".
- showItemInFolder errors reach the status bar; the home screen's recent
  files refresh when the window regains focus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since 1.14.0 the renderer opens .txt as plain text, but moving its
Windows registration to another ProgID would orphan the default users
already gave FATE for .txt. Document that, and let the consistency test
expect exactly this difference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VagueDustin and others added 2 commits October 4, 2026 08:36
…it-view focus, palette modes, settings plumbing)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ocked-down preview

Version 1.14.0, its changelog entry, AppStream release and the README
shortcut table (Find in the reading view, Escape's new scope, palette
modes, the menu bar).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread .github/workflows/build-linux.yml Fixed
Comment thread src/paletteModes.js Fixed
Comment thread src/useMermaid.js Fixed
VagueDustin and others added 3 commits October 4, 2026 08:47
QA found Ctrl+S on an unedited file with mixed line endings normalised
it to its majority ending: saving a clean tab rewrote the file. A save
with nothing to write now writes nothing (deleted or changed-on-disk
files still save). A document's invalid Mermaid diagram is the
document's mistake, so it logs a warning rather than an error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- checks.yml: Actions runs steps with `bash -e`, so the expected 124 from
  `timeout` ended the step before its status was read. Capture it with
  `|| status=$?`.
- Release workflows no longer cache npm: a Run workflow recovery checks
  out a tag the dispatcher chose, and its cache would land in the
  default branch's scope (CodeQL actions/cache-poisoning).
- useMermaid: escape the diagram id fully before building the scope
  regex (js/incomplete-sanitization).
- paletteModes: strip heading tags until none are left, and an unclosed
  one at the end (js/incomplete-multi-character-sanitization).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Code scanning (actions/cache-poisoning) flagged the build jobs: a Run
workflow recovery checked out a ref named in the `release_tag` input
and ran its code in a job whose caches land in the default branch's
scope. A recovery run is now started from the tag itself (Use workflow
from → Tags) and builds what it was started from; a `release_tag` that
names another tag is refused. Tag pushes, the release path, are
unchanged. README, comments and error messages describe the new way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@VagueDustin
VagueDustin merged commit f37b7e2 into main Oct 4, 2026
6 of 7 checks passed
@VagueDustin
VagueDustin deleted the release/1.14.0 branch October 4, 2026 16:23
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.

2 participants