Skip to content

PlanCake 1.0: read and annotate Markdown with a screen reader - #1

Merged
Menelion merged 60 commits into
masterfrom
plan-cake-v1
Sep 27, 2026
Merged

Menelion merged 60 commits into
masterfrom
plan-cake-v1

Conversation

@Menelion

Copy link
Copy Markdown
Contributor

What and why

PlanCake 1.0: a Windows desktop application that shows a Markdown file (above all a long implementation plan) as rendered HTML in WebView2 and lets you leave notes on any paragraph, list item, heading, table row or code block. Notes go straight into the .md file between [usernote] … [/usernote] markers, the convention Claude's manual-review step already understands. There is no Save command and no side file: the file on disk is the single source of truth.

Reading raw Markdown in an editor means wading through punctuation with no heading, list or table navigation; rendering fixes the reading. Annotating a specific place from a web page's reading cursor is the hard part, and PlanCake solves it by annotating blocks: every rendered block knows the source lines it came from, so Enter or a click on it puts the note in exactly the right place in the file.

The same executable also works headless (plancake list|check|clear|export), which is what Claude uses during manual review.

Tasks completed

  • Task 1: adapt the template to PlanCake (names, catalog, README, CLAUDE.md, license, repository metadata)
  • Task 2: WebView2 host and the JAWS spike (results in docs/jaws-spike.md, which reshaped Tasks 6 and 7)
  • Task 3: parse notes out of a Markdown source (paired and single-token markers)
  • Task 4: render Markdown with source line ranges and notes
  • Task 5: write notes to the file safely (stale-file check, encodings, undo/redo)
  • Task 6: the document view, page protocol, links, zoom, back and forward between files
  • Task 7: add, edit and delete notes; Markdown inside notes; the button note style dropped
  • ➕ Task 7a: toggle task-list items, with partially checked parents
  • Task 8: the notes list beside the document (F6, F9 / Shift+F9)
  • Task 9: menu bar, keyboard shortcuts and About dialogs, Tab cycle between panes
  • Task 10: follow changes on disk (keeping the reading place), one window per file
  • ➕ Task 11: open from the clipboard and from a link
  • Task 12: settings, including safe handling of files that are not UTF-8
  • Task 13: command-line mode
  • Task 14: updates (NetSparkle, Ed25519-signed appcast)
  • Task 15: translations (Russian, Ukrainian, French, Hebrew with right-to-left layout, German)
  • ➕ Task 15a: move from block to block with the keyboard (Alt+Shift+Down / Up) and resize the panes from the View menu
  • Task 16: user manual in six languages, written for a general audience
  • Task 17: installer, portable zip, winget instructions
  • Task 18: documentation (README, CLAUDE.md, CHANGELOG)

Every UI task was checked with JAWS by the maintainer; the results and the changes they caused are recorded in the plan (docs/plans/completed/001-plan-cake-v1.md) and docs/jaws-spike.md.

Review

  • Round 1, full sweep with six lenses (quality, implementation, testing, simplification, documentation, smells): 70 findings, 56 confirmed and fixed, 14 refuted. The important fixes: links and "Open in editor" could run a program or script without asking (now a confirmation offering File Explorer instead); downloads kept any extension and had no Mark-of-the-Web (now always saved as Markdown, with the zone mark); files with a BOM but broken bytes, or UTF-8 with one stray byte, could be misread and corrupted by conversion (now read-only); UI-thread exceptions were neither logged nor handled; deleting an unterminated note could wipe the rest of the file; plus test gaps, dead code and documentation.
  • Refuted in round 1, each with a reason in the progress log: the installer's post-install launch runs as the original user (not elevated); markers inside code are a deliberate design; the README install channels and one class's location follow the plan; the rest were matters of taste with no project convention behind them.
  • Round 2 (critical re-check): 5 confirmed and fixed (a link to a local script could still run; a new note could merge with an unterminated one; Delete all notes could delete notes never seen; a task checkbox could disagree with the file after a refused toggle; settings saved in one window did not reach the others).
  • Round 3: 2 confirmed and fixed (a failed settings re-read reset a window to defaults and could overwrite the file).
  • Codex, three rounds: a race where two launches on the same file could both open a writer (registration now gates opening); background update checks not re-claimed when the owning window closes, and then taken over with a stale interval; a trailing blank line in messages.pot. One claim (a var pattern that "always matches") was refuted with a compiled check and the line was rewritten plainly anyway.
  • Final re-check: one more real fault found and fixed (the single-instance pipe listener spun at full CPU after a client left without asking, which made the file impossible to open elsewhere), then clean.
  • After the maintainer's final hands-on check, View → Notes list was made the same switch as the "Show the notes list" setting.

Validation

All passing at the head of the branch:

  • pwsh -NoProfile -File ./src/PlanCake/locale/scripts/Compile-Translations.ps1 -Strict (5 languages, no fuzzy entries)
  • dotnet build (0 warnings)
  • dotnet test (858 tests)
  • dotnet format --verify-no-changes
  • installer/Build-Installer.ps1 builds the installer and the portable zip; a real install of v1.0.0.57 was run and checked (wizard read fully with JAWS, Program Files, machine PATH, Start menu, plancake check)

To check by hand

  • A full JAWS pass on a real plan of 800+ lines: read it end to end, annotate twenty blocks of every kind, let Claude act on them through manual review, confirm the reload keeps the place.
  • Debussy: set "noteMarkers": ["[usernote]...[/usernote]"] in ~/.claude/debussy.json. If !USERNOTE! stays in that list for hand-written notes, plancake check will not count those, since PlanCake reads only its own markers. A separate change in Debussy: plan-make's manual-review hand-off can start plancake <plan> when it is on the PATH, and manual review can use plancake list --json and plancake check.
  • Add Set-Alias pk plancake to the PowerShell profile (and alias pk=plancake in bash).
  • Set up plancake.oire.dev for the appcast and downloads (deploy.json), create the GitHub release, and submit the winget manifest with wingetcreate at the first release.
  • PlanCake has no icon yet; shortcuts use the default application icon.
  • Idea for later: a notes import in Notika that reads PlanCake's notes JSON.

Lock navigation to in-page anchors and block script through a CSP, split interface and document language (View submenus plus a default in Settings), strip multi-line note prefixes on read, refuse to write non-UTF-8 files, make CLI output UTF-8 with redirect-aware console attach, add --single-token, define anchor edge cases, and close the hidden setting dependencies between tasks.
Add opening from the clipboard and from a link (new Task 11, later tasks renumbered), notes JSON export, a note style setting (role=note by default, or button), an Edit menu, an Advanced tab for converting non-UTF-8 files, keyboard-and-mouse and LTR/RTL as a rule for every UI task, and repository metadata. Drop the > prefix from written notes, confirm plancake.oire.dev, and give the key generation commands in the updates task.
Take the ANSI fallback from Windows (injectable for tests), defer dialogs and menus started from WebView2 events with BeginInvoke, keep full block text for position restore and copy, route every open through one OpenFile, pass localized strings into the renderer, fix console encoding when redirected, harden clipboard and link opening, complete the deferred menu items, and repair the Notika path.
Rename the project, test project, solution and namespaces from
WinFormsTemplate to PlanCake. The executable is plancake.exe, App.Name is
PlanCake (data folder, PlanCake.cfg, PlanCake.po/.mo), and the unused
database constants are gone.

Get-CatalogName.ps1 now reads App.Name from App.cs, the same constant the
app loads its catalog by, instead of the AssemblyName.

Replace the template README with a PlanCake one, rewrite the CLAUDE.md
title and structure section, and add the Apache 2.0 LICENSE.
Add the WebView2 host (DocumentView) with its user data folder under the
app data folder, the web folder served from https://app.plancake/, the
browser's own menus, accelerators and status bar off, and JSON messages
both ways. Program checks for the WebView2 Runtime before creating the
window and offers the download page when it is missing.

Host shortcuts go through one table (HostCommands). Keys pressed in the
document never reach ProcessCmdKey: the WinForms control raises its own
KeyDown from the browser's AcceleratorKeyPressed, so DocumentView passes
those on and MainWindow handles both paths the same way.

StatusAnnouncer speaks status messages through a UI Automation
notification. TextDirection and DialogHelper come from SIC. spike.html is
the throw-away page for the JAWS checklist; F9 focuses its third
paragraph. Directory.Build.targets drops the package's WPF reference,
whose WindowsBase clashes with WinForms (MSB3277).
docs/jaws-spike.md records each checklist answer and the decision taken.

Plan changes: Enter and a mouse click cannot be told apart, so both
activate a block or note and double-click does nothing extra. Blocks and
notes lose their permanent tabindex, which put JAWS in forms mode on
Enter; the page adds it only while moving focus, and a keydown Enter on
a block is handled like a click. JAWS takes F8 for extended select, so
note navigation moves to F9 / Shift+F9. The announce page message is
dropped, since UI Automation notifications are heard in the document.
New Task 7a makes task-list checkboxes toggleable, behind a
ConfirmTaskToggle setting.

The shortcut table follows: notes on F9 / Shift+F9, the spike's focus
command on F12.
Add NoteMarkers (default [usernote]/[/usernote], single-token mode when
the closing marker is empty, Validate returning an error code for the
caller to localize), the Note record and NoteParser. Parse returns the
notes with 1-based lines and character spans, and the source with the
notes stripped line by line plus a map from stripped to original lines.
Unterminated notes run to the end of the file and are flagged;
continuation lines lose the indentation PlanCake writes.
Add Markdig and a MarkdownRenderer that renders the note-stripped source,
stamps every annotatable block (paragraph, heading, list item, code block,
table row) with its original 1-based line range and dir="auto", and collects
a BlockInfo per block with its plain text and an 80-character excerpt.

Notes are anchored to their block and inserted after it: as user notes or
buttons in interactive mode, as static role="note" elements in a standalone
export document carrying a script-free content security policy. Localized
labels come in through RenderStrings, so the renderer never touches the
catalog.
Add MarkdownFile, which detects a file's encoding (UTF-8 or UTF-16 by BOM,
else strict UTF-8), BOM and dominant line ending and writes it back the same
way, atomically through a temporary file, retrying a locked file for about a
second. A file that is not valid in its encoding is decoded with the ANSI
code page and opened read-only, or converted once to UTF-8 without BOM when
ConvertToUtf8 is on.

Add NoteStore: add, edit, delete and clear notes, each refusing with
StaleFileException when the file differs from the rendered text. New notes go
after the block's last line and after the notes already there, indented to a
list item's content column, never with a quote marker. Note text is
validated against the markers, and every change can be undone and redone
unless the file changed in between.
Replace the JAWS spike page with the real document page: index.html with
a strict content security policy, app.js implementing the page protocol
(render, strings, focus and note navigation; activate, context menu,
position, links and dropped files back to the host) without permanent
tabindex so Enter does not switch JAWS to forms mode, and app.css with
focus, note, high contrast and dark mode styles.

MainWindow.OpenFile reads a file with MarkdownFile, renders it, sets the
window title, announces a read-only or converted file and an unterminated
note, and keeps the reading position across re-renders through
PositionRestorer. Links go through LinkResolver; every navigation after
the page load is canceled. Files open from the command line and by drag
and drop; Ctrl+Plus, Ctrl+Minus and Ctrl+0 zoom in 10% steps.
The window keeps a browser-like history of visited files. Opening another
file by any route records the file being left with its reading position
and clears the forward list; Alt+Left and Backspace go back to the
previous file at the same block, Alt+Right goes forward. The ends and
files that no longer exist are announced, and missing files are dropped
from the history. Backspace never fires from a text box, and the page
reports it as goBack when the browser does not pass it to the host.

The plan records the Task 6 JAWS results, the new keys and View menu
items, the Task 7 check for the history, and how Task 10's one window
per file treats a history entry open in another window.
Enter or a click on a block opens the new note dialog and writes the
note after the block; Enter on a note edits it. The Applications key,
Shift+F10 and a right-click open a native context menu at the element
(Add note, Edit note, Delete note, Copy block text). Deleting asks
first. Ctrl+Z and Ctrl+Y undo and redo note changes. Each change is
announced and the view returns to the note.

NoteActionRunner turns every store outcome into what the user is told:
a file changed on disk is shown again and the typed text kept for the
retry, a write failure reopens the dialog with the text, and a
read-only file is refused with the reason. The page now sends its
devicePixelRatio with the context menu rectangle.
The Task 7 screen reader check found Markdown in a note shown raw, with
backticks read out. A user note and an exported note now render their
text as Markdown with the document's extensions: every inner block gets
dir="auto" and no data-lines, a heading becomes a bold paragraph so it
stays out of heading navigation, typed line breaks are kept, raw HTML is
shown as text and footnotes are left out. A note shown as a button holds
phrasing content only: inline Markdown, links as their text. The delete
confirmation shows the note as plain text. The file keeps exactly what
the user typed.
Every note is now a role="note" user note. Long button labels are hard
to listen to, and the note element already works with Enter and with
F9 / Shift+F9, so the Button style is gone: the NoteStyle enum, the
button rendering path with its inline-only Markdown helper, the "Note:"
label in RenderStrings, and the button-only rules in app.css.

The plan and the JAWS spike notes record the decision, and the Task 7
screen reader results now include Back and Forward.
Task-list check boxes in the document are now enabled: Space, Enter or
a click toggles one, and the page sends the item's lines and new state
to the host. After a confirmation that says the file on disk will be
changed (on by default until the setting arrives), the host rewrites
that item's [ ] or [x] and nothing else, through the same stale-safe
write and undo history as notes. The view re-renders with the focus on
the same check box and announces "Task checked" or "Task unchecked".
Cancelling, a read-only file or a failed write sets the check box back
to the file's state. Exported HTML keeps the check boxes disabled.

TaskToggle holds the pure marker rewrite; Undo and Redo announce
toggles like note actions.
An unchecked task item with some, but not all, of its nested task items
checked (at any depth) now shows its check box as partially checked.
The renderer marks it data-mixed and the page sets indeterminate after
every render and every reverted toggle. A checked parent shows checked
whatever its children say. Nothing new is written to the file:
toggling such a parent writes [x] on its own line only. Exported HTML
keeps disabled check boxes and marks a partially checked one
aria-checked="mixed", since no script runs there.
Following a relative .md link landed at the end of the new file. A file
opened fresh had no saved position, so the page was sent no focus and
only scrolled to the top; the followed link went away with the old
content and JAWS kept its old place in the virtual buffer, which in a
shorter file is the end. A document just opened now puts the focus on
its first block, unless Back or Forward brings a saved position for
it. A re-render of the same file still returns to where the user was.

The plan records the Task 7a screen reader results and adds the
partially checked and link checks to Task 8's list.
- SplitContainer with the document and a NativeListView of the notes
  (Lines, Block, Note as plain text), labeled "Notes", refilled after
  every render; unchanged rows are left alone and the selection stays on
  the same note (PositionRestorer.FindNote)
- F6 switches between the document and the list; View > Notes list shows
  or hides it (first NativeMenuBar, View menu only)
- Enter in the list jumps to the note, Delete deletes it, context menu
  with Edit note and Delete note; actions started in the list keep the
  focus there
- F9 / Shift+F9 move the list selection when the list has focus; the
  page tracks focus reaching links and check boxes as the current block
- The list view's system proxy ignores the window text AccessibleName
  sets, so JAWS heard no name: the list window is also named through
  MSAA annotation (WindowAccessibleName); AccessibleName is _("Notes")
  and the visible label stays. CLAUDE.md records the exception.
- The notes list selects the note the document reports (activateNote,
  context menu, position, F9, a note added or edited there) without
  moving the focus (PageMessages.FindNote).
- A different file gets a freshly loaded page and is rendered on its
  ready message, so JAWS starts it at the top instead of keeping its
  old buffer offset; FocusDocument no longer refocuses a document that
  already has the focus. Page messages and renders are logged at debug.
The notes list, F6, F9, the list menu and hiding the list passed; the list name, following the document and the link landing at the top pass on re-check.
The window gets its full menu bar: File, Edit, View, Notes and Help, built
from the host command table so every shortcut shown is the one that runs,
from the document too. Items that need a file or a note are disabled
without one, and separators left with nothing to separate are dropped.

View offers the interface language (saved and applied live) and the
document language (this document only). File opens files and the editor,
View reloads, Edit deletes all notes after asking. Help shows every
shortcut in one list, and the About dialog.

Tab and Shift+Tab now leave the notes list for the document, so Tab
cycles through both panes in either direction.
A FileWatcher watches the open file's folder, debounces events by
300 ms and ignores text PlanCake itself shows or wrote. An outside
change reloads the view at the same block and announces it (the ask
first branch is built, AutoReload hard-coded until the settings task).
A deleted or renamed file disables note commands, Reload and Open in
editor until it comes back, then reloads.

SingleInstance claims each open file through a named pipe named after
the SHA-256 of its normalized path. Starting PlanCake on a file that is
already open, or opening it from a link, the history or File > Open,
activates the window showing it instead.
An outside edit sent the JAWS virtual cursor to the top: every reload
was posted with focus on the block the file opened at, because the page
never sees the virtual cursor move, and replacing the page content
destroyed the nodes the cursor was on. Renders the user did not ask for
by acting on a block now carry no focus, and every re-render of the same
file updates the page in place (web/morph.js), keeping unchanged nodes.
Its matching logic is tested through Jint.

A Yes/No message box has no cancel, so Escape did nothing in the
confirmations. Every yes-or-no question now goes through
DialogHelper.Confirm, a task dialog where Escape and the close button
answer No.
Outside edits keep the reading position and confirmations close with Escape on re-check; the other Task 10 and deferred Task 9 checks passed in the first round.
File > Open from clipboard (Ctrl+V) opens the first Markdown file among
files copied in Explorer, a path copied as text (quotes from "Copy as
path" stripped) or an http(s) link; anything else is announced. The
decision is a pure ClipboardClassifier, tested without a clipboard.

File > Open from link (Ctrl+L) asks for a link in a small dialog,
pre-filled from the clipboard. MarkdownDownloader turns a GitHub blob
link into its raw form, downloads with a 30-second timeout and a 10 MB
limit, refuses error statuses and web pages, and saves the file to the
Downloads folder, numbering the name when it is taken. The window then
opens that local copy and announces where it was saved.
The start of a download announced the file name guessed from the link,
so a link to a web page was announced as "Downloading Oire.md" before
being refused. It now names the host ("Downloading from github.com...");
the file name comes only with "Downloaded and saved to", once the
content is accepted. Task 11's JAWS checks are recorded in the plan.
File > Settings (Ctrl+comma) opens a dialog with General, Notes and
Advanced tabs. Config gains the [General], [Notes] and [Advanced]
sections of the plan and drops ConfirmExit; each value is read on its
own, and an unreadable enum, unusable markers or an unknown language
falls back to its default alone.

Every hard-coded default of earlier tasks now follows the setting and
applies on OK without a restart: interface and document language, note
markers (re-render), confirmations, the external change action, the
notes list, Enter on a block (the page now sends the block's rectangle
with activate, for its context menu), Enter in the note dialog, and
conversion to UTF-8 (a read-only file open at the time is reopened).
On a machine whose ANSI code page is UTF-8 (65001), a Windows-1251 file
was decoded "as ANSI" into replacement characters, and Convert to UTF-8
wrote them over the file. A file that is not UTF-8 is now decoded with
the first legacy encoding that decodes it cleanly: the UTF.Unknown
charset detector when it is confident on enough text, then the code
page of the document language, then the ANSI code page unless it is
65001. When none fits, the file is read-only, never converted and never
written, and the announcement says the encoding was not recognized.
View > Document language reads such a read-only file again.

File > Settings reads the settings file again before showing the
dialog, so a setting saved in another window is shown, kept and
applied. The Closing marker box gets an always visible hint, also read
as its description.
The separate hint under the Closing marker box, also given as the box's
description, was not read by the screen reader. The explanation is now
part of the box's own label: "Closing marker (leave empty for a single
marker that runs to the end of the line):". The refusal reason works as
before. The plan records the Task 12 check results.
App.UpdatePublicKey now holds the Ed25519 public key of the pair in
keys/ (gitignored), so update checks run and verify the appcast.
All four checks pass: the update settings read with their labels and defaults, Help > Check for updates reports its result and returns to the document, and the startup check leaves focus and reading alone.
Add Russian, Ukrainian, French, Hebrew and German catalogs for every
message of the interface and the command line, with mnemonics unique on
every menu level and in every dialog, and ship the six languages.

Tests build the real menu bar, the context menus and every dialog in each
language and fail on a repeated mnemonic, check that every window is
mirrored in Hebrew, and that each catalog loads and translates every
message of the template. New-Language.ps1 no longer writes the Language
header onto the Language-Team line.
All four checks pass: the switch to Russian applies live, Russian menus and mnemonics work, Hebrew mirrors the window and dialogs, and the document keeps its own language.
The manual covers reading, notes, tasks, settings, opening files, the command line and the keyboard and mouse reference. A base glossary for its translations sits beside it and is kept out of the build output.
Russian, Ukrainian, French, Hebrew and German manuals, with UI names and key names taken from each language's catalog.
Add the Inno Setup installer (from SIC's), with a new permanent AppId, the
six interface languages, the .NET 10 Desktop and WebView2 runtimes as
dependencies, and plancake on the machine PATH (taken off on uninstall).

Build-Installer.ps1 compiles the translations, publishes, builds the
installer and a portable zip carrying the same files plus an empty
userdata folder, and optionally a signed appcast and an SCP deploy.
CLAUDE.md documents the release steps and the wingetcreate command.
README for a general audience: what PlanCake is, installing (website,
installer, portable zip, winget), reading and annotating, the command
line, the pk alias, reviewing plans with Claude and Debussy, building.

CLAUDE.md gains the architecture, the page protocol, the WebView2 and
JAWS findings that shape the code, one window per file and the
command-line output caveat; adding a language lists every place a
language lives, and the update key is no longer called a placeholder.

CHANGELOG.md and changelogs/1.0.0.md carry the 1.0.0 feature list. The
command-line help opens with the tagline, in every catalog.
Security and safety:
- Links never run a program or script: LinkResolver sorts them into
  LinkKind.Program (deny list, PATHEXT, AssocIsDangerous) and the window
  offers to show the file in File Explorer. UNC and file://host targets on
  another host than the document's are Unsupported and never touched.
- Open in editor uses the "edit" verb or Notepad for non-Markdown files,
  never the default verb.
- Downloads are always saved under a .md name and get a Zone.Identifier
  stream; a dropped connection or corrupt body maps to a network failure,
  and DownloadAndOpen catches everything.
- Application.ThreadException is handled: logged, and the user decides
  whether to keep PlanCake open.
- A file whose BOM does not decode, or UTF-8 with a few damaged bytes, is
  read-only with the line of the first bad byte, never decoded as a legacy
  code page; TryDecodeCleanly rejects NUL. A failed conversion to UTF-8
  leaves the file open read-only instead of failing to open.
- Editing, deleting or clearing a note without a closing marker is refused
  (GUI and plancake clear), since it would take the rest of the file.
- The --output guard compares file identity (volume serial and file ID).
- PlanCake.cfg is written through a temporary file and moved; Load retries;
  View > Interface language re-reads the file first (Config.SaveLanguage).
- The file watcher backs off and stops after bounded retries, and decodes
  the file with the window's document language.
- Ctrl+Shift+= zooms in too (Ctrl+Plus on US and UK layouts).

Cleanups: removed the "not yet available" scaffolding, MenuBuilder (now
MenuSpecExtensions), the placeholder update-key check (NotConfigured is
Unavailable with an accurate message) and FocusLinesMessage; shared the
marker-error texts, render strings, encoding name, culture check and
EnsureWritable; one list-marker parser; one Nearest helper; App.WebFolder;
String. for static members enforced through IDE0049.

Tests: link kinds, downloads, damaged encodings, conversion failure,
unterminated notes, file identity, config atomic save and SaveLanguage,
watcher back-off and legacy decoding, CSP of index.html, every manual
copied, mnemonics of every form, injected ANSI code page in CLI tests,
tighter marker, restorer and config tests.

Docs: README build order and behaviors, locale README, CLAUDE.md (tests,
new dialogs, manuals, focus mark, data folder, link safety), JAWS
findings of Tasks 12 and 15a, plan Task 15a record, six manuals, catalogs
in five languages.
- Links: open only a folder or a passive document (plain text, PDF, a
  picture) with the system; any other local file is offered in File
  Explorer instead, since its default action may run it (.py with the
  Python launcher, .sh with Git for Windows, .rdp, .theme, .iso, ...).
  Those types also join the runnable list.
- Notes: refuse to add a note after the start of a note without a
  closing marker (store, runner and before the dialog opens), since its
  closing marker would close the stray one and a later delete would take
  the rest of the file.
- Delete all notes: take the rendered text before the question, so an
  outside change while it is open makes the delete stale instead of
  removing notes the user never saw.
- Task check boxes: every render sets each check box from the file's
  state; a refused toggle (stale file, or a toggle from a replaced
  render) sets the check box back and says the file changed.
- Settings: a window re-reads PlanCake.cfg when activated and it changed
  since it was last read or written, so settings saved in another window
  (note markers above all) apply there too.
- New strings translated in de, fr, he, ru and uk; the manuals describe
  the link rule and the refused add.
- Config: split reading the settings file from applying it. The sections
  and the change stamp are replaced only after a read succeeds. The new
  Config.Reload, used by ReloadIfChanged, File > Settings and View >
  Interface language, keeps the settings in memory when the file exists
  but cannot be read or parsed, logs it, and leaves the stamp alone so
  the next activation tries again. Only the startup Load falls back to
  the defaults; a missing file is still written out with them.
- SaveLanguage does not save when the re-read failed: the language
  applies for the session and the file is left as it is.
- Tests for a locked file on activation, a malformed file on a re-read,
  at startup and in SaveLanguage, and a deleted file on a re-read.
- Claim the per-file pipe before opening a file: SingleInstance.TryClaim
  registers, or else activates the window holding the file, retrying when
  the holder lets go in between. Two windows opening the same file at once
  can no longer both write it. The initial file of a window that loses the
  race closes that window; a holder that does not answer is reported and
  the file is not opened. Tests cover concurrent claims.
- Let the remaining windows take the background update checks over when
  the window doing them closes: a window without the claim retries it every
  minute and starts the checks at the interval Settings asks for.
- Extract-Strings.ps1 writes messages.pot with LF endings and no blank line
  at the end, which git diff --check reported; the template is regenerated.
- New message for a file held by a window that does not respond, translated
  in every catalog.
- A window taking the background update checks over no longer starts them
  at the interval it read earlier: it reads the settings file again first,
  as on activation (another window may have set Never since), and a file
  that cannot be read keeps the settings in memory.
- The takeover waits while a dialog of the window is open, so the action
  that opened it finishes with the settings the user saw; the claim is let
  go for the next attempt.
- UpdateService.TryTakeOver holds the decision apart from NetSparkle, with
  tests: nothing is read while another window holds the claim, the interval
  read at takeover wins, and a window that cannot read it now lets go.
- Write the takeover check as a plain null check and a deconstruction.
  The positional var pattern it replaces already failed on null (its
  parenthesized designation checks for null), so behaviour is unchanged:
  a window that cannot take the update checks over keeps retrying.
- A client that connected and closed without sending its request left the
  per-file pipe broken: Disconnect was skipped because IsConnected was false,
  and the listener then failed every wait at once, spinning a core, flooding
  the log and making the file impossible to open in any other window. The
  pipe is now disconnected after every connection, connected or broken.
- A client that came and went before the listener waited made the wait fail
  with "The pipe is being closed" while the stream still counted itself as
  waiting to connect; the pipe is now reset through DisconnectNamedPipe.
- Consecutive listener failures back off from 50 ms to 1 s, and only the
  first of a run is logged as a warning, so no unexpected state can become a
  busy loop.
- Tests: activation still works after a client left without asking, both
  before and while the window waits for it.
xUnit runs synchronous tests on thread pool threads, so on a small CI
runner a few blocking tests hold every thread the pool starts with.
Timer callbacks and the pipe listener's continuations then wait for
the pool to grow, which the fixed windows of two tests did not allow.

MarkdownFile gets an OnRetry option, and the unlock test frees the
lock when the first attempt has failed instead of on a timer.

The single-instance listener no longer depends on a free pool thread
to recover or to finish a request: it listens again at once after a
single failed connection and backs off only on failures in a row, it
waits for the client to close instead of blocking in WaitForPipeDrain,
and TryActivate connects on the calling thread instead of blocking a
pool thread in ConnectAsync. Tests place the broken client through
listener hooks instead of a sleep, run the blocking calls on threads
of their own as the app does on the window's thread, and run apart
from the other tests.
@Menelion
Menelion merged commit 05d8bdd into master Sep 27, 2026
1 check passed
@Menelion
Menelion deleted the plan-cake-v1 branch September 27, 2026 17:45
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.

1 participant