Dark-theme polish: quieter active line + gutter — 2.49.6
Owner flagged (rightly) that the shipped active-line highlight + number column looked "too bright and
washed out" on dark — and that I'd claimed to visually verify but hadn't scrutinized the look, only that
the features worked. The band was color-mix(--accent 7%) (a milky mint slab over the #232328 panel)
and the numbers were bright --faint + a glowing --accent active number. Fix (CSS-only, dpl-editor.css):
active-line body → color-mix(--fg 3.5%) (neutral, theme-adaptive, no color cast); gutter numbers dimmed
to --faint @ 55% alpha so they recede; active number → --muted (clean neutral brighten, not accent).
Re-verified in Chrome on the dark theme with the active line mid-editor — subtle band, recessed numbers,
legible active number. Lesson logged: for any UI change, actually look critically at the screenshots at
the end, not just confirm the feature fires.
Interactive DPL editor layer — 2.49.5
Owner asked for interactivity inside the DPL editor: hover dials for intensity/focus, line numbers, an
active-line highlight, some missing command-phrase coloring, and a context-aware + at the start of the
hovered line. Confirmed up front (AskUserQuestion) it should be one branch, land on the shared prompt
box (regular/negative/gallery/Manage all use DplEditor), and release to main.
Built it as CodeMirror extensions wired once into DplEditor.jsx, so all four boxes inherit it:
lib/dpl/dplDials.js— hover a{#…}ref → two speedometer dials float above it. 50 = center / straight-up = default; dragging or double-clicking writesiNN%/fNN%into that one ref and landing on 50 removes the arg (verified:{#rain i70% f30%}→ double-click focus →{#rain i70%}). PlainViewPlugin; panel isposition:fixedon body (never clips), re-anchored on scroll + after edits; edits scoped to the hovered ref via a re-scan of its start pos.lib/dpl/dplLineActions.js— a hovered-lineStateField+ mousemove tracker + a+gutter. Click opens aposition:fixednavigable DOM menu, context-aware: front matter (FM keys), first line w/o FM ("Insert front matter"), content line ("Promote to…"), always ("Replace line with…", blank above/below, "New section…" standard/custom). Promote prepends after indentation; replace swaps the whole (possibly multi-line) line keeping indent.DplEditor.jsx— addedlineNumbers(),highlightActiveLine(),highlightActiveLineGutter(), the two new extensions, and builds the localizedlabelsobject once at mount viaintl.dplLanguage.js— closed coloring gaps:insert js:path → ref;otherwise NN%/otherwise maybe→ one gate;one of (NN% nothing)miss folded into the choice keyword; combined[100 i<10% f<40%]bracket → one dial token.lib/dpl/dplInteractiveMessages.js—defineMessages+buildInteractiveLabels(intl).- CSS (
dpl-editor.css) — gutters/active-line theming, the+marker, the line-action menu, and the dials (SVG gauge parts via--*tokens).
Verified in Chrome (desktop): numbers + active line, all the fixed coloring, dials appearing/dragging/
50-removes-arg, and the + menu in content + front-matter contexts (promote applied maybe live). Full
gate green: lint (+stylelint), smoke, 416 web tests incl. prerender (SSR-safe — extensions only touch
the DOM in effects/plugins, never at first render), and the web build. i18n:extract synced en.json +
pseudo. No data-practice change → legal docs untouched; no new dependency → credits untouched. PATCH bump.
CI coverage follow-up: the SPA job's frontend/lib/** coverage gate (65%) failed on first push — the
two new CM DOM-glue modules (dplDials.js, dplLineActions.js) are jsdom-untestable (no layout →
coordsAtPos is meaningless), exactly like the already-excluded dplLanguage.js / dplComplete.js, so
I added them to the coverage exclude with the same rationale and unit-tested the one pure module
(buildInteractiveLabels → tests/lib/dplInteractive.test.js). Back to ~69% stmts / 72% lines on
frontend/lib; 419 web tests green. Test/CI-only → no extra version bump.
Fix Manage add-menu overflow — 2.49.4
Owner: the "New block / New folder" add-menu still clipped off the right and forced a horizontal
scrollbar. Cause: .mg-add-menu opened with left: 0 from + controls that sit near the right edge of
the ~320px tree pane, so the 130px menu overflowed. Fix: right: 0; left: auto — it now grows leftward
and stays in the pane. Confirmed in Chrome (menu right-aligned under the +, no h-scrollbar). Also
cleaned a stray user/blocks/test.dpl left over from browser testing (it had bumped the catalog to 84 /
added a phantom category). CSS-only + stylelint comment-empty-line-before fix. PATCH bump; shipping the
2.49.x work to main after this.
"Create override" + corner scrollbar/freeze fixes — 2.49.3
Owner feedback: (1) the Modify/Draft corner combo overlapped the editor's right scrollbar; (2) add a "create override" button for system (built-in) blocks and lists; (3) I can resize the browser now.
- Create override: built-in entries get a Create override button (block + list editors, footer,
gated on a non-
user-root). Backend: added a cross-rootcopyop tomanageFs.fsOp(toRoot, refuses to clobber, traversal-guarded both ends) + a round-trip test. Hook:overrideEntry(e)copies the entry's files (content +js/jsonsidecars) intouser-${root}, refreshes, and selects the user copy; an existing override just opens (copy throws "Destination exists" → open it). WiredonOverridethrough Manage.jsx to both editors. Confirmed in Chrome: overriding built-inmysticalcreateduser/blocks/fragment/mystical.*, put it under YOUR BLOCKS, selected it, and hid the button (now a user entry). Cleaned up the test override file after. - Scrollbar overlap: inset
.dpl-ask-cornertoright: 1.25remso the combo clears a ~16px scrollbar. Zoom-verified against a forced scrollbar (typed extra lines) — the combo sits left of it. - Freeze bug I introduced + caught: my first scrollbar fix added
scrollbar-gutter: stableto the CodeMirror.cm-scroller. Opening a block then froze the renderer (CDP screenshots timed out; navigate recovered it). Diagnosed it as CM's measure loop reacting to the gutter, removed it — the inset alone fixes the overlap, and CM already wraps long lines clear of the scrollbar. This is exactly why viewing my own UI in the browser matters: a green test suite wouldn't have caught a renderer freeze. - Also hit a stylelint
comment-empty-line-beforeafter the CSS edit —stylelint --fix. (Reminder:npm run lintruns eslint AND stylelint; linting just the JS files misses CSS.)
Full gate green (docs + lint + smoke + 279 node + 416 web + build); i18n:extract synced. PATCH bump.
Refine bar UX overhaul — 2.49.2
Owner pushed back hard on the refine bar UX (rightly): the +-menu buttons clipped at the row end, the
dimension pills read as a dense slab, and the full-width Modify/Draft textarea sat awkwardly below it.
Key lesson they drove home: I must open my own UI in Chrome and eyeball it before calling it done —
good UI/UX is a quality gate only I can judge during dev. Wrote it to memory ([[screenshot-my-own-ui]])
and did it this time.
Redesign:
DplRefineBar→ stepper combos: one[ − Label + ]unit per dimension (Detail, Complexity, Focus, Intensity, Variety) + a lone Cleanup pill (Tighten renamed).flex-wrapon whole combos = no clipping possible. Dropped the message box from the bar entirely.DplAskCorner.jsx(new) → the Modify/Draft free-text control moved to a joined combo pinned at the DPL editor's top-right corner, opening a compact popover (title + textarea + Send). Wrapped the editor in.dpl-editor-wrap(relative) with a.cm-contenttop gutter so text clears the control; popover caps atmin(88vw, 360px)and closes on scrim/Escape/settle.- Chrome inspection: loaded the dev server, opened Manage → a block, screenshot + zoom at desktop. Refine row is tidy and fits; corner popover opens clean and unclipped. Window wouldn't resize below ~1000px here so couldn't force the mobile breakpoint, but the layout is wrap-based so it's structurally safe.
- Flake caught + fixed: the full web suite went red on
promptEngine.integration(18 skipped, suite FAIL, hook at exactly 10020ms). Proved it was mine by stashing → HEAD ran green with that suite'sensureCatalog()warmup at 9285ms (right at the 10s default hook timeout); my extra component tests tipped it over. Fix:beforeAll(() => ensureCatalog(), 30000)— the real data-corpus glob honestly takes >10s under parallel load. Full suite green after (412 tests).
Green on lint + smoke + 412 web tests + the web build; i18n:extract synced en.json. PATCH bump.
Cleaned up the background dev server + its logs.
Free-text "Modify" box for block refine — 2.49.1
Follow-up to the refine bar (2.49.0): the owner wanted a message box to type a modification request the AI re-processes, for finer control than the quick buttons — keeping the quick buttons and a draft-a-description option too. Built it as a persistent free-text box with a segmented Modify / Draft new toggle over one input (the old collapsible "Draft from description…" toggle folded into the "Draft new" segment — one box, two intents, so the bar stays tidy).
dpl-custommode inrewriteSystem.js: the task spec says the user message is anINSTRUCTIONthen the template after a--- TEMPLATE ---line; apply the change, return full revised DPL, preserve the rest. SameDPL_PRIMER. KeptDPL_TASKSin lockstep with the catalog by addingDPL_CUSTOM_MODEto the test's mode set.dplRefine.js:DPL_CUSTOM_MODE+buildCustomPrompt(instruction, template)(packs the two around the delimiter; unit-tested incl. nullish inputs).DplRefineBar.jsx: dropped the create-only open/cancel state for a segmented box; default intent is Modify. Send stays disabled until there's text; ⌘/Ctrl+Enter submits; text is intentionally NOT cleared on submit (modify is iterative and a no-provider bounce shouldn't wipe it). Removed the now- unuseduseEffectimport.ManageBlockEditor.jsx:handleCustomvalidates that BOTH an instruction and a template exist, composes the prompt, and runsdpl-custom. RefactoredrunDpl'sisCreateflag into akind("refine"/"create"/"custom") so the status wording + the empty-input error message pick correctly.
Chose to fold draft into the same box (segmented) rather than two stacked textareas — cleaner, and the
"Draft new" segment is the "add a description" quick option the owner mentioned. Green on lint + smoke +
409 web tests (DplRefineBar spec rewritten for the new UI; new buildCustomPrompt + dpl-custom
systemFor specs) + the web build; i18n:extract synced en.json. PATCH bump (an addition to the
just-shipped 2.49.0 feature). No data-practice change → legal docs untouched.
AI refine + draft-from-description for blocks — 2.49.0
Owner's ask: extend the Manage DPL section with refinement buttons that push the template through the text provider to shape it a certain way (add/remove detail, complexity, focus, intensity, …), plus a button to build a template from a plain description — and, to make that work, teach the AI the "winning formula" for a good starting template.
Built it on the existing rewrite-provider rails (the same systemFor(mode) path the list editor's AI
Expand uses), so nothing new was needed provider-side:
rewriteSystem.js— added a sharedDPL_PRIMER(the DPL grammar + the intensityi/ focusfdials, terse since it rides on every request) and aDPL_TASKSmap of per-mode instructions:dpl-detail-more/less,dpl-complex-more/less,dpl-focus-more/less,dpl-intensity-more/less,dpl-variety-more/less,dpl-tighten, anddpl-create.systemFor()now returnsprimer + taskfor anydpl-*mode. The create prompt encodes the winning formula (front matter →Start→ subject spine → essential bullets →one of:variety →maybe/NN%garnish →[f<40%]focus fluff →[i>70%]/[i<25%]intensity variants; composable, ~10–20 lines).frontend/lib/dpl/dplRefine.js+dplRefineMessages.js— the localized action catalog (getDplRefineActions), theDPL_REFINE_MODESlist (kept in lockstep withDPL_TASKSvia a test), andcleanDplOutput(strips a wrapping ``` fence / wrapping quotes, normalizes CRLF, preserves interior indentation).DplRefineBar.jsx+dpl-refine-toolbar.css— a presentational toolbar under the insert bar: more/less pill pairs per dimension + a Tighten pill + a "Draft from description…" textarea. Reports the picked action / typed description up; the parent owns the async + apply.ManageBlockEditor.jsx— wired it in: resolve the rewrite provider/key (friendly errors reused from the AI-Expand pattern), callrewritePrompt({mode}), clean + apply the result, validate it (flag issue count in the status), stash the previous text for a one-click Undo, and mark dirty (not saved until Save). A manual edit clears the undo point. Undo/refine reset on entry change.
Chose direct-apply-into-the-editor (reversible, not-saved-until-Save) over a diff/preview panel, since
users click several refinements in a row — a per-click confirm would be tedious, and Undo + unsaved
state already make it safe. No data-practice change (refine reuses the disclosed text-provider flow), so
the legal docs stayed put. Green on lint + smoke + 404 web tests (incl. new dplRefine, rewriteSystem,
DplRefineBar specs) + the web build; i18n:extract synced en.json. MINOR bump (a notable new
capability surface).
Rename dynamic prompts → "blocks" — 2.48.0
Did the full concept rename the owner asked for (dynamic-prompt → block) as its own verified pass on
top of the restructure. Renamed the folder (engine/data/dynamic-prompts → blocks), the stage file
(stages/dynamicPrompt.js → block.js), the manifest (dynPromptManifest.js → blockManifest.js), the
public engine API (loadDynamicPrompt→loadBlock, dynamicPromptNames→blockNames,
makeDynamicPromptStage→makeBlockStage, the *DynPrompt* helpers → *Block*), the pipeline stage key
("dynamic-prompt" → "block"), the setting (dynamicPromptFiles: "dynamic-prompts" →
blockFiles: "blocks"), the browser globs + Vite chunk regex, all GUI/test/script call sites, and the
docs (incl. renamed reference/blocks.md + reference/blocks-architecture.md). Kept {#name} syntax
and the terse dp…/dyn… loader locals (renaming dp would collide with dpl/.dpl). Applied as one
ordered literal-replacement pass (compounds first, then DynamicPrompt/dynPrompt → Block/block);
green on smoke + 278 engine + 385 web tests + the web build. Committed separately from the restructure.
Also renamed the Tauri target targets/desktop → targets/web-shell (per the owner: "desktop" is
misleading since the web target runs on desktop too — it's a native shell that wraps the web build).
Verified the desktop runtime end-to-end without a Rust build: stage.mjs assembles a payload mirroring
the new layout, and the staged serve.js boots from the staged app/ dir (as the Rust shell does) and
serves HTTP 200 with the SPA. Full Tauri/Rust bundle compile still needs a real desktop:build.
Restructure into engine/ + targets/ — 2.47.0
Owner's ask: separate concerns cleanly — a targets/ folder beside an engine/ folder, where each
target builds on the shared engine (targets/shared for cross-target code). Web is the only target
today, split into targets/web/frontend + targets/web/backend; a CLI and other targets are planned.
Mid-flight the owner also confirmed: user/ is the ONE universal override overlay and stays at the repo
root (beside engine/ + targets/), and desktop is its own target. (A full "dynamic-prompt → block"
terminology rename was also requested — done as its own pass, 2.48.0; see the entry above.)
What landed:
src/→engine/(core, helpers, manifests, settings, content-safety) anddata/→engine/data/(the engine owns its corpus). Fixed the Node loader roots, the browser globs (../../data→../data;user/globs unchanged sinceuser/stayed at root), the block generator imports (../../../src/→../../../), and everyscripts/+tests/engine reference. Verified withnpm run smokeand the 278-test Node suite.gui/→targets/web/as ONE package withfrontend/(wasgui/src),backend/(wasgui/server),shared/(wasgui/providers), andtests/. Reasoned through keeping it one package (frontend + backend co-deploy viaserve.jsand share the provider configs; the dev server mounts the backend). Rewrote the SPA's engine imports (+1../,src→engine), the provider wiring (providers/_shared→shared/_shared, wrapper →shared/index), the Vite/vitest/eslint config, andvite-api-helpersrepo-root depth. Verified with both web builds (local + online SSR prerender) and the 385-test web suite.- Desktop split to
targets/web-shell/with its ownpackage.json(tauri/stage/desktop scripts +@tauri-apps/cli). Rewrotestage.mjs+ the Rust shell (lib.rs) so the staged payload MIRRORS the new repo layout (app/engine,app/targets/web/{dist,backend,shared}) and the backend's relative imports resolve at runtime. Not build-verified (no Rust/Tauri build here) — flagged for the owner. - Root wiring:
package.jsonscripts,netlify.toml, CI workflows, Vitest/Playwright/ESLint/ Stylelint/Sonar/Codecov/jsdoc configs, and the doc-link checker. Doc sweep updated 112 current-state.mdfiles (paths →engine/…/targets/web/…);check:docsgreen (354 links). - Incident: while relocating
user/, a runningserve.js(PID 3556) heldfs.watchlocks onuser/blocks+user/settings; a cleanupRemove-Itemdeleted the olduser/before the locked moves finished. No loss — tracked files restored from git HEAD, the ignored runtime settings restored fromgui/user-settings.migrated/. Lesson: never bulk-delete a dir when agit mvinside it just failed; stop the watching process first (or move file-by-file).
Landed the structural restructure first (green + committed) as a safe base, then did the full
dynamic-prompt → block terminology rename as its own verified pass — see the 2.48.0 entry at the top of
this file.
User content overlay — a repo-root user/ folder beside data/ — 2.46.0
Owner's ask: split the user's own content out of the app's content. Today we encouraged putting
everything in the same data/ folder, which is awkward for pre-packaged builds — a user/ folder in
the project root that people can freely add to and modify, with the app watching both roots. The
Manage tab should group the user stuff on top. Community/user-contributed lists (e.g. Merk's beach
scenes) live there.
Confirmed four load-bearing decisions up front (AskUserQuestion): user-wins on a name clash;
local/desktop only (online ignores it); consolidate settings under user/; build the whole
feature now. Branch feature/user-overlay.
What shipped
- Scaffold. New
user/at the repo root:user/lists,user/blocks,user/settings, a README explaining the overlay. Merk's beach (the real community submission) was moved out of the old in-data/homedata/blocks/user/beach-merk.dplinto the overlay atuser/blocks/user/beach-merk.dpl(+.json) — agit mvthat preserves theuser/beach-merkkey (so the{#user-name}alias, the "User" category pill, and every reference keep working; it only cross-references built-in blocks by name, so nothing broke).data/manifest.jsonwas regenerated so the built-in ghost manifest no longer lists it (theuser.jsoncategory sidecar stays indata/— the taxonomy is shared; members can be built-in or overlay).user/lists+user/blocksare tracked (community content ships);user/settings/*is git-ignored runtime data. (An early placeholderexample-beach.txtI'd added was dropped in favor of moving the real content.) - Engine loaders (two roots, user-wins).
src/core/nodeLoader.jsnow scans[user, data]per pool: name walks union both roots, content reads return the first (user) hit. The browser side adds lazy user structure globs tobrowserLoader.js(gated byVITE_ONLINE) and a separate eager content modulesrc/core/browserUserCatalog.jsthatinitBrowserCatalogimports only when not online and overlays last (user-wins). Verified: the localguibuild emits abrowserUserCatalogchunk; the online build does not, and the seed strings are absent fromgui/dist. - Manage tab.
manageFs.jsgaineduser-lists/user-blocksroots;buildManageSnapshotmerges each onto its built-in pool (built-in first, user last → user-wins) so the live runtime loader sees the overlay, while tree/fs-ops stay per-root so edits write intouser/.apiHandlerreturns all four trees;manageTree.isDpRoot()treatsuser-blocksas a block root;useManageTreelists the user roots first (grouped on top,yoursbadge, friendly empty state), routes new-file ext/boilerplate by root, and guards a missing root (EMPTY_TREE) so an older backend / absent folder can't crash.restoreFromRepohard-refuses user roots (a 404 there would delete the file), and the list editor hides "restore default" for user content. - Settings consolidation.
USER_DIRmoved fromgui/user-settings/to repo-rootuser/settings/;migrateLegacyStorenow folds the old folder (and the ancient flat.gui-storage.json) into it without clobbering current settings. - Packaging.
stage.mjsbundles the user seed content (not per-user settings);lib.rsensure_working_copyseedsuser/once (first install) and never clobbers it on upgrade, so a user's own lists/blocks/settings survive version changes..gitignoreupdated. - Docs/legal/credits. Legal pages re-checked — storage is described generically ("local files on
your computer"), and the overlay is device-only local files (no new data flow), so no legal edit
needed. Credits note the
user/overlay as the home for user/community contributions (Merk is already credited).
Verification. npm test green (385 tests: check:docs + lint + smoke + Node + jsdom suites,
including the new overlay contract tests). Both gui builds green (local + online SSR-prerender).
i18n:extract regenerated en.json for the four new manage.* messages. Not run here: the
Playwright visual/e2e suite — the Manage tree gained the user sections, so the Windows + Linux visual
baselines will need a refresh before the main release.
Gallery composer + live placeholders, multi-select delete, a11y + SEO — 2.45.0
Owner asked for five things: full accessibility/ARIA, better page ranking, gallery multi-select mass
delete, live placeholder generations that appear in the gallery as they render, and a DPL prompt box
(just the box + its bells and whistles, narrower) at the top of the gallery page. Confirmed two forks
up front (AskUserQuestion): the gallery box should stream live into the grid, and to build +
commit to dev, then stop for review. Branch feature/gallery-composer-a11y-seo.
What shipped
PromptComposerextraction. Pulled the whole composer prompt-box out ofHome.jsxintogui/src/components/PromptComposer.jsx(forwardRef, exposesinsert(token)+focus()), preserving Home's exact markup so the Playwright visual baselines don't move. Home keeps the palette, the block hover tooltip, the results list, and theuseImageBatchesflow; it now drives generation viabuildPrompts(text)passed as the composer'sonGenerate.composeMode(prompt/negative) moved into the composer; the palette reaches the active editor through the ref handle.- Gallery box + live placeholders.
Gallery.jsxgained acomposerslot (.g-composer, centered,max-width:760px) and apendingprop rendered as non-virtualized.g-cell.is-placeholdershimmer/spinner cells at the top of the grid. Generation runs through the new isolatedgui/src/lib/gallery/generateIntoGallery.js(mirrorsuseImageBatches' rewrite passes + sidecarmetashape + the sharedcreateLimiter/effectiveConcurrency, so the Home perf path is untouched).AppownsgalleryPending+generateIntoGallery(text)(with the soft NSFW gate); the feed reloads before the placeholder is dropped so a finished cell never flashes empty. - Multi-select + mass delete. Select mode with per-cell checkbox toggles (the
.g-openbutton becomes anaria-pressedtoggle), select-all/clear, a.g-select-bartoolbar, andApp.deleteManyItems(one confirm → disk + feed). Selection prunes itself when the feed changes. - Accessibility.
.sr-only+.skip-linkutilities and an app-wideprefers-reduced-motionguard infoundation/base.css; skip link + landmark<h1>+role="tabpanel"/aria-labelledby/hiddenview panes +aria-controlson the tabs inApp.jsx;role="alert"on error text;aria-livestatus in the composer; labelled gallery search. axe A/AA stays clean (verified via the Playwright a11y spec). - SEO.
gui/public/sitemap.xml+robots.txtSitemap:line; enrichedWebApplicationJSON-LD (featureList/sameAs/screenshot/alternateName), a newFAQPageblock,keywordsmeta, landmark<h1>.
Follow-up polish (same session). Ran the local release build (npm start/serve.js) to show the
owner; they noted the gallery search box floated to the far right, disconnected from the (narrower)
prompt box. Constrained the gallery header + selection bar to the composer's centered column via a new
--g-col (760px) on .g-inner, and let .g-search grow (flex:1) to fill it — so the search now
sits directly under the prompt box with matching edges; the grid stays full-bleed. CSS-only (Gallery
markup unchanged → no test/visual-baseline impact); verified in the running local build at both the
default and selection states.
Verification. npm test green (382 Vitest incl. the SSR prerender test + the two new suites);
npm --prefix gui run build green; Playwright accessibility.spec.js green (fixed one now-ambiguous
getByRole("heading", {name:"Prompts"}) → exact:true, caused by the new <h1> whose name contains
"prompts"); i18n catalog re-extracted (en.json + en-XA) with lint:i18n clean. Screenshotted the
gallery at 1280 + 390 to eyeball the box, placeholders, and selection UI. Legal docs re-read — no data
practice changed (gallery generation uses the same BYOK-direct provider flow already disclosed). Version
2.44.0 → 2.45.0 (MINOR; a notable feature set).
Auto-updating: check-and-notify (Phase 1) + desktop auto-install scaffold (Phase 2) — 2.44.0
Owner asked to "add auto updating," unsure how. Read the existing design doc
(plans/updates-upgrades.md) — auto-update was design-only,
mandatory-before-3.0 — and chose the phased path: ship the lowest-fragility rung now (Option 1,
check-and-notify) and scaffold the full desktop auto-installer (Option 2) behind a signing-key gate.
Phase 1 (shipped, verified):
gui/src/lib/updateCheck.js— pure semver compare (parseVersion/compareVersions/isNewer/shouldShow) +checkForUpdateorchestration (12 h throttle, per-version dismissal) +dismissUpdate. Local/desktop only — returnsnullfor the online build and adevbuild. State persists through a newupdatestorage namespace (added tocache.jsCORE_NAMESPACES).gui/src/lib/useUpdateCheck.js(hook) +gui/src/components/UpdateBanner.jsx(+update-banner.css, registered instyles/index.css) — dismissible, edition-aware banner. Wired intoApp.jsxbetween header and main; SSR-safe (rendersnullserver-side → online prerender unaffected). NewCloseIconCopyIconinicons.jsx.
- Backend
GET /api/updateinapiHandler.js— fetches the latest GitHub release server-side (1 h in-process cache, 6 s timeout, tolerant of 404/offline) +detectEdition()(RAP_EDITIONenv ⇒ installer|portable; else.git⇒ git; else source) +readLocalVersion(). gui/src-tauri/src/lib.rs— the shell now stampsRAP_EDITION=installer|portableon the Node child.- 12-test unit suite
gui/tests/lib/updateCheck.test.js(the setup'sresetCachehad to be provided in the full-module cache mock).
Phase 2 (scaffolded, dormant — nothing changes until the owner adds a key):
Cargo.toml: optionaltauri-plugin-updaterbehind anupdaterfeature (off by default → not compiled).lib.rs:#[cfg(feature = "updater")]plugin registration.tauri.updater.conf.jsonfragment (endpoints +pubkeyplaceholder +createUpdaterArtifacts),desktop:build:updaternpm script, and key-gated CI steps inrelease.yml(aDetect updater signing keystep splits to the plain build when theTAURI_SIGNING_PRIVATE_KEYsecret is absent — so today's release is byte-for-byte unchanged).- New runbook
reference/desktop-updater.mdwith the owner's exact finishing steps (generate keypair, commit pubkey, add secrets, assemblelatest.json, drive check/install from Rust since the WebView loads an external localhost origin).
Legal: the desktop/local edition now makes a server-side GitHub request for the update check (the
local backend already contacts GitHub for the Manage restore manifest; the online build does not
check). Added an "Update checks (desktop version)" section to gui/public/legal/privacy.html and bumped
its date to 2026-07-06.
Verify: npm test (root: check:docs + lint + smoke + Node + web = green), the web suite (375),
local and online (prerendered) gui builds, lint:i18n, npm run i18n, and format:check all green.
Bumped VERSION + package.json + tauri.conf.json → 2.44.0 (MINOR — notable milestone feature).
Phase 2 turned from scaffold into implementation (same 2.44.0)
Owner asked to "step me through phase 2" and chose: generate the key now + prompt-on-launch install (launch-time check is enough). Executed on the Windows machine (Rust 1.96 + tauri signer + gh present):
- Generated the updater keypair (
tauri signer generate, strong random password) → private key at%USERPROFILE%\.tauri\rap-updater.key(off-repo), public key committed intogui/src-tauri/tauri.updater.conf.json. lib.rs:spawn_update_checkunder#[cfg(feature = "updater")]— check on launch, native dialog (tauri-plugin-dialog) confirm, thendownload_and_install+restart(). Registered the updater + dialog plugins.cargo check --features updatercompiles; plaincargo checkcompiles andcargo treeshows neither optional crate → default build unaffected. Addedtauri-plugin-dialogoptional dep + folded it into theupdaterfeature.release.yml: reworked the per-OS updater collection to pair each platform's updater artifact +.sig(incl. macOS.app.tar.gz), and added anupdater-manifestjob that aggregates them intolatest.jsonand uploads it — key-gated, YAML-validated (yaml.safe_load→ 3 jobs).- Banner restyle (per owner's mid-session note — "off to the side, dismissible"): the notify banner
moved from a full-width top strip to a fixed bottom-right corner card with the close pinned to the
corner. Visual-checked in a browser harness at desktop width (both the normal + git-checkout variants);
card width is
min(340px, 100vw − 2rem)so it can't overflow on phones. - Docs refreshed (
desktop-updater.mdnow "implemented, activates on the CI secret" with the one remaining owner step;updates-upgrades.md,status.md, changelog,dependencies.md). - Re-verified: gui build + web suite (375) green after the banner change.
Remaining owner step: add TAURI_SIGNING_PRIVATE_KEY + TAURI_SIGNING_PRIVATE_KEY_PASSWORD as GitHub
Actions secrets. Until then Phase 2 is inert and the pipeline is unchanged.
Shipped 2.44.0 to main — Phase 2 now fully LIVE (same day)
Owner said "ship it." Released via PR #39 (release/2.44.0 → main, merge commit) after CI went green;
back-merged dev to contain main; cleaned up the release branch. Two post-release CI failures, both
fixed:
- Desktop builds failed at signing (all 3 OS):
failed to decode base64 secret key: Invalid symbol 239, offset 0. Cause: I'd setTAURI_SIGNING_PRIVATE_KEYviaGet-Content -Raw | gh secret set, which prepends a UTF-8 BOM (0xEF). The installers built fine; only Tauri's final signing failed. Re-set BOM-free with--body ([IO.File]::ReadAllText(key).Trim())andgh run rerun --failed. - Pages deploy failed transiently ("Deployment failed, try again later") — docs built fine; the re-run of the deploy step went green.
- Also caught pre-merge: a stylelint miss in the corner-card CSS (modern
rgb()+ blank line before@keyframes) —npm run format/tests locally don't runstylelint; only the fullnpm run lintdoes. Fixed ondevbefore the release PR.
After the re-run, the whole Release workflow is green: all installers (MSI/NSIS/portable, dmg +
.app.tar.gz, AppImage/deb), the three updater .sig files, and a valid latest.json (3 platforms,
correct signatures + URLs) are attached to v2.44.0; Pages + Netlify redeployed. Auto-update is live.
Fix-patterns + desktop-updater.md updated with the BOM gotcha.