From 62cdabcf9d9f83d02c247c8ef2b1db76363a571e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Polykanine?= Date: Fri, 25 Sep 2026 11:48:39 +0200 Subject: [PATCH 01/60] Add plan: PlanCake 1.0 --- docs/plans/001-plan-cake-v1.md | 652 +++++++++++++++++++++++++++++++++ 1 file changed, 652 insertions(+) create mode 100644 docs/plans/001-plan-cake-v1.md diff --git a/docs/plans/001-plan-cake-v1.md b/docs/plans/001-plan-cake-v1.md new file mode 100644 index 0000000..4dcf84a --- /dev/null +++ b/docs/plans/001-plan-cake-v1.md @@ -0,0 +1,652 @@ +# PlanCake 1.0: read and annotate Markdown with a screen reader + +## Overview + +PlanCake is a Windows desktop application for reading long Markdown files, above all the +implementation plans that `/planning:plan-make` writes (800 lines and more), as properly +rendered HTML, and for leaving notes on them for Claude's manual-review step. + +The problem it solves: reading raw Markdown in VS Code with JAWS means hearing "hash hash +hash" and "star star" all day, with no heading, list or table navigation. Rendering the file +fixes the reading; the hard part is annotating *a specific place* from the JAWS virtual +cursor, which a web page cannot see. PlanCake solves that by annotating **blocks**, not lines: +every rendered paragraph, list item, heading, table row and code block carries the source line +range it came from, so pressing Enter (or the Applications key) on a block tells the host +exactly where in the `.md` file the note belongs. The user never deals with line numbers. + +Notes are written straight into the `.md` file the moment the user confirms them, wrapped in a +pair of markers (default `[usernote]` … `[/usernote]`), which is the convention Debussy's +manual-review step already understands (`noteMarkers` accepts `open...close` pairs). The file +on disk is the single source of truth: there is no Save command, no side file, no export step. + +The application is dual-mode like SIC! (`C:\repos\Oire\sic`): run without a subcommand it opens +the window; with a subcommand (`list`, `check`, `clear`, `export`) it works headless, which is +what Claude uses during manual review. + +This repository was created from Oire's `winforms-template` and still carries the template's +names; Task 1 adapts it. + +## Done when + +- [ ] `plancake plan.md` opens a window showing `plan.md` rendered as HTML in WebView2; JAWS + reads it in its virtual buffer with heading, list, table and button navigation, and no + Markdown punctuation is read out +- [ ] pressing Enter (and/or the Applications key, whichever survived Task 2) on a paragraph, + list item, heading, table row or code block opens the note dialog; confirming writes + `[usernote]text[/usernote]` after that block's last source line, and the view returns to + the new note +- [ ] notes show in the document as buttons after their block and in a notes list beside it; + notes can be edited, deleted, navigated with F8 / Shift+F8, undone and redone +- [ ] when the file changes on disk the view reloads (or asks, per settings) and keeps the + reading position; a note is never written over a change the user has not seen +- [ ] File → Settings changes the language (English, Russian, Ukrainian, French, Hebrew with + right-to-left layout, German), the note markers, and the other settings listed in + Technical details; changes apply without a restart +- [ ] `plancake list [--json]`, `check`, `clear` and `export` work headless with the + output and exit codes in Technical details +- [ ] F1 opens the user manual in the current language +- [ ] the installer installs PlanCake, puts `plancake` on the PATH, checks for the WebView2 + Runtime, and the portable zip runs from any folder +- [ ] all validation commands pass + +## Validation commands + +- translations: `pwsh -NoProfile -File ./src/PlanCake/locale/scripts/Compile-Translations.ps1 -Strict` + (before building, once any `.po` exists; in Task 1 the path is still under `src/WinFormsTemplate`) +- build: `dotnet build` +- test: `dotnet test` +- format check: `dotnet format --verify-no-changes` + +## Context + +- **Template conventions are binding.** Read `CLAUDE.md` before every task: accessibility rules + (`TableLayoutPanel`, real `Label`s, keyboard path for everything), GetText.NET `.po`/`.mo` and + **no `.resx`**, GitVersion (no version literals), code style, single project, `App` not + `Application`, data folder layout, startup error handling, tests that touch `Config` or + `Localization` not running in parallel. +- **SIC! is the reference application** for everything the template does not have yet. Copy + its patterns, adapted to PlanCake names: + - dual GUI/CLI entry point: `C:\repos\Oire\sic\src\Sic\Program.cs` (`RunCli`, System.CommandLine + 2.x with `SetAction` and `parseResult.GetValue`) + - `Utils\TextDirection.cs` (`IsRightToLeft`, `Apply(Form)`) and `Utils\DialogHelper.cs` + (every message box goes through it, for right-to-left) + - `SettingsDialog.cs`: tabs, language list built from `locale\\` folders holding a + `.mo`, native culture names, save + `Localization.SetLanguage` + - `MainWindow.cs`: `NativeMenuBar` created in `OnHandleCreated` from a declarative + `NativeMenuSpec` (`BuildMenuSpec`), enabled state via `IsEnabled` on the spec items, + `ApplyLocalization()` for a live language switch (`Localizer.Revert` + `Localize` + + `TextDirection.Apply` + menu re-attach), help lookup `help\\manual.html` → + `help\\manual.html` → `help\en\manual.html` opened with `UseShellExecute` + - `AboutDialog.cs`: version, copyright, repository link, "Copy info" button + - `Services\UpdateService.cs` + `Utils\Enums\UpdateCheckInterval.cs`: NetSparkle with an + Ed25519-signed appcast + - `installer\` (`sic.iss`, `CodeDependencies.iss`, `Languages\Custom..isl`, + `Build-Installer.ps1`, `deploy.example.json`) and `manifests\` (winget, made with + `wingetcreate`; see SIC's `CLAUDE.md` for the command and the `|x64` quirk) +- **Oire.WinForms.NativeControls** (`C:\repos\Oire\winforms-native-controls`, the template + references 1.2.0): `NativeMenuBar` / `NativeMenuSpec` (`Add`, `AddCheckable`, `AddMenu`, + `AddSeparator`, shortcut text + `Keys`, `null` keys for display-only shortcuts, `Rebuild`), + `NativeContextMenu` (`AttachTo`, `Show(owner, screenLocation)` at + `src/Oire.WinForms.NativeControls/NativeContextMenu.cs:90`, `Resolver`, `Rebuild`), + `NativeListView` (`Columns`, `Items` of `NativeListViewItem` with `Cells`, `ItemActivate`, + `SelectedIndexChanged`, `EnsureVisible`). Its README explains attach/dispose ordering. +- **New dependencies:** `Microsoft.Web.WebView2` (WinForms control), `Markdig`, + `System.CommandLine` (same major as SIC), `NetSparkleUpdater.SparkleUpdater` + + `NetSparkleUpdater.UI.WinForms.NetCore` (same versions as SIC). Latest stable of each. +- **Debussy's note convention:** `C:\Users\User\.claude\plugins\marketplaces\Debussy\plugins\planning\skills\plan-exec\references\settings.md` + (`noteMarkers`: a single token runs to the end of the line; an `open...close` pair spans + everything between) and `manual-review.md` beside it. PlanCake must read notes exactly as + that convention defines them, so a note written by hand in an editor works too. + +## Development approach + +- One task at a time; the validation commands pass before the next task starts. +- Code changes come with tests for the new and changed behavior, success and error paths, unless + the change is UI-only or the user said to skip tests. The project has no e2e tests; UI tasks + end with the manual JAWS checks they list. +- A test that cannot pass until a later task is still written now, marked with a comment naming + that task. +- Keep backward compatibility unless the user asked for a breaking change. +- When scope changes, update this plan: new tasks get a "➕" prefix, blockers a "⚠️" prefix. +- The primary user is blind and uses JAWS. A UI task is not done until its JAWS checks pass; + a subagent cannot run JAWS, so it stops and asks the user to run them. +- Every user-visible string goes through `_()` or the designer + `Localizer.Localize`, from the + first task that introduces it. English only until Task 14. +- Line numbers are **1-based** everywhere a user or a CLI consumer sees them, and in the + `data-lines` attribute. Markdig's 0-based `Line` is converted at the boundary. + +## Implementation steps + +### Task 1: Adapt the template to PlanCake + +**Files:** +- Rename: `src/WinFormsTemplate/` → `src/PlanCake/`, `tests/WinFormsTemplate.Tests/` → + `tests/PlanCake.Tests/`, both `.csproj` files, `WinFormsTemplate.slnx` → `PlanCake.slnx` +- Modify: every `.cs` file (namespace), `src/PlanCake/Utils/Constants/App.cs`, + `src/PlanCake/PlanCake.csproj`, `src/PlanCake/locale/messages.pot`, + `.github/workflows/dotnet.yml`, `README.md`, `CLAUDE.md` + +- [ ] follow the README's "Starting a new application from it" checklist in order: directories, + project and solution files, `ProjectReference` and `.slnx` paths, `AssemblyName` = + `plancake` (so the published exe is `plancake.exe`), `RootNamespace` = `Oire.PlanCake`, + `InternalsVisibleTo` = `PlanCake.Tests`, the `Oire.WinFormsTemplate` namespace everywhere +- [ ] `App.Name` = `PlanCake` (data folder `%APPDATA%\Oire\PlanCake`, config `PlanCake.cfg`); + remove the unused database constants if nothing refers to them; `Product` = `PlanCake`, + `Description` = "Read and annotate Markdown files with a screen reader"; catalog name + in `messages.pot`; the translation-script path in the CI workflow +- [ ] check the gettext scripts: they take the catalog name from `AssemblyName`; with the + lowercase `plancake` assembly name the catalog becomes `plancake.po`/`.mo` — make sure + `Localization` looks for the same name (fix whichever side disagrees, and say so in + `locale/README.md`) +- [ ] replace the README's template text with a short PlanCake README (what it is, build + commands); rewrite `CLAUDE.md`'s title and structure section for PlanCake, keeping every + convention +- [ ] the existing tests (`AppConstantsTests`, `ConfigTests`, `LocalizationTests`) pass under + the new names, adjusted only where they assert on the old name +- [ ] validation commands pass + +### Task 2: Host WebView2 and run the JAWS spike + +The design depends on three things JAWS must do inside WebView2. This task builds the real +WebView2 host and a throw-away test page, and the user checks them with JAWS. **Its results +decide which note triggers the later tasks build.** + +**Files:** +- Modify: `src/PlanCake/PlanCake.csproj`, `src/PlanCake/Ui/MainWindow.cs`, + `src/PlanCake/Ui/MainWindow.Designer.cs`, `src/PlanCake/Program.cs` +- Create: `src/PlanCake/Ui/DocumentView.cs`, `src/PlanCake/web/spike.html`, + `src/PlanCake/Utils/StatusAnnouncer.cs`, `src/PlanCake/Utils/TextDirection.cs`, + `src/PlanCake/Utils/DialogHelper.cs`, `docs/jaws-spike.md` + +- [ ] add `Microsoft.Web.WebView2`; `web\**` copied to output; set + `IncludeNativeLibrariesForSelfExtract` so `WebView2Loader.dll` survives single-file + publish, and confirm a `dotnet publish -c Release` build starts +- [ ] `DocumentView` (a `UserControl` wrapping the `WebView2` control): creates the + `CoreWebView2Environment` with its user data folder under `App.DataFolder\WebView2` + (the install folder is not writable), maps the virtual host `https://app.plancake/` to + `AppContext.BaseDirectory\web`, disables default context menus, browser accelerator keys, + the status bar and (in Release) dev tools, and exposes `PostMessage(object)` plus a + `MessageReceived` event over `chrome.webview` JSON messages +- [ ] copy `TextDirection` and `DialogHelper` from SIC; missing WebView2 Runtime: catch + `WebView2RuntimeNotFoundException` at first use, show a `DialogHelper` message with the + download link (`https://go.microsoft.com/fwlink/p/?LinkId=2124703`), exit with + `ExitCode.Error` +- [ ] `StatusAnnouncer`: sets the status-strip label and raises a UI Automation notification + (`AccessibilityObject.RaiseAutomationNotification`, `ImportantMostRecent`) so JAWS speaks + status messages wherever focus is +- [ ] keys pressed while the WebView2 has focus do not pass through the host's message loop: + handle `CoreWebView2Controller.AcceleratorKeyPressed` (or the control's `ProcessCmdKey` + forwarding) and route the shortcuts to a single host command table; for the spike, log + each planned shortcut from Technical details → "Keyboard" to the status announcer +- [ ] `spike.html` per Technical details → "JAWS spike page", loaded at startup for now +- [ ] **stop and ask the user** to run the JAWS checklist in Technical details → "JAWS spike + checklist" and report the answers; write them to `docs/jaws-spike.md`; then update this + plan: mark with "⚠️" any trigger that failed and adjust Tasks 6–7 accordingly (if neither + Enter nor the Applications key reaches the page, stop and rethink with the user) +- [ ] validation commands pass + +### Task 3: Parse notes out of a Markdown source + +**Files:** +- Create: `src/PlanCake/Notes/NoteMarkers.cs`, `src/PlanCake/Notes/Note.cs`, + `src/PlanCake/Notes/NoteParser.cs`, `tests/PlanCake.Tests/NoteParserTests.cs` + +- [ ] `NoteMarkers` record: `Opening` (required), `Closing` (empty = single-token mode); + `Validate()` rejects empty opening, leading/trailing whitespace, line breaks, and + `Closing == Opening`; default `[usernote]` / `[/usernote]` +- [ ] `NoteParser.Parse(string source, NoteMarkers markers)` returns the notes (text, 1-based + start and end line, character spans) and the **stripped source** with a mapping from + stripped line numbers to original ones, per Technical details → "Note parsing" +- [ ] tests: paired note on its own line, note spanning several lines, note mid-line with text + before and after, several notes on one line, single-token mode running to end of line, + notes inside a fenced code block and a table, line mapping after stripping +- [ ] tests: unterminated opening marker (treated as running to end of file, and reported), + closing marker without an opening one (left as text), empty note, CRLF source +- [ ] validation commands pass + +### Task 4: Render Markdown with source line ranges and notes + +**Files:** +- Create: `src/PlanCake/Rendering/MarkdownRenderer.cs`, `src/PlanCake/Rendering/RenderResult.cs`, + `src/PlanCake/Rendering/BlockInfo.cs`, `tests/PlanCake.Tests/MarkdownRendererTests.cs` +- Modify: `src/PlanCake/PlanCake.csproj` (Markdig) + +- [ ] pipeline: `UseAdvancedExtensions()` (pipe tables, task lists, auto-identifiers, …) + + `UsePreciseSourceLocation()`; parse the stripped source from `NoteParser` +- [ ] walk the AST and stamp each annotatable block with `data-lines="start-end"` in + **original** line numbers, per Technical details → "Annotatable blocks"; collect a + `BlockInfo` per block (range, kind, plain-text excerpt) +- [ ] anchor every note to its block and insert its HTML into the AST where Technical details → + "Note placement in the view" says; two modes: `Interactive` (note buttons) and `Export` + (static `