Skip to content

ZenNotes 2.78.0 - #1000

Merged
adibhanna merged 19 commits into
mainfrom
v2.78.0
Oct 27, 2026
Merged

adibhanna merged 19 commits into
mainfrom
v2.78.0

Conversation

@adibhanna

Copy link
Copy Markdown
Contributor

ZenNotes 2.78.0, the release PR: everything on v2.78.0, with What's New (6cc483ac) and the version bump (413632bd). The phone apps pick up the core changes once core 2.78.0 is published.

Features

  • Note covers (ea74766b): a wide banner image across the top of a note, picked from the keyboard (Space l c, Add Cover in the palette, :cover): gradients, six public-domain photographs, vault images, an https link, upload, paste or drop; hover for Change cover, Reposition, Remove. Stored as banner: "![[assets/...]]" + banner_y (the keys Obsidian's banner plugins read; cover: is read too), imported through importPastedImage, so local, remote, web and Cloud (encrypted included) vaults all work. New portable pref [appearance] note_cover_placement.
  • Covers on published notes and in Quick Look (fec49657): ReadOnlyNoteCover; built-in gradients are redrawn by the share viewer from note-cover-gradients.ts instead of uploading their SVG; Quick Look resolves other cover forms over zenql://cover.
  • Pele 1.12.0 (7ae3afcf): labels on author fills pick near-black or white text, <u>/<sub>/<sup>/<code>/<kbd> in labels, List<T> kept, CJK wrapping, layout fixes. No API change.
  • $$ math in Mermaid labels (fa668530): lib/mermaid-label-math.ts, KaTeX to MathML, measured and cached, loaded only with pele-runtime.
  • Phones' Calendar widget, core half (Dave on Discord, 3fc40107): getCalendarWidgetData, setTaskChecked, lib/calendar-data.ts; the shells are on feat/calendar-widget in their own repos.
  • zn 0.12.2 bundled (4bda75e8).

Fixes

  • PDFs on the phones (Dave on Discord): open in the phone's viewer where the WebView has none (16722caf), Hide/Focus Reference Pane for a PDF pinned to one note (4bacd562), the PDF card fits at phone width (b1fd339d).
  • Ticking, moving or editing a task in a note saved with Windows line endings acts on the right task (57513b7e). The other task-line copies (desktop MCP, zn, server) already count CRLF notes correctly.

Verification, release gates on 413632bd in the v2.78.0 worktree with its own node_modules: audit (high) clean; typecheck 8/8 and test:run uncached (shared-domain 2,164, app-core 3,593, desktop 1,198, quicklook 18); fresh build:prod; Developer ID signed npm run pack and the packaged launch check (page target, rendered, 1.6 s, 2.78.0); vim-editor, sidebar-vim and editor-improvements smoke suites pass. 6cc483ac (What's New data only): app-core typecheck and whats-new tests pass. Each merged branch was verified in its own session: covers over CDP (27 checks, Vim on and off, plus a Go server vault), Pele and label math over CDP in three themes and with Typst, the PDF and widget work on an Android emulator and an iOS simulator. ci-sweep since v2.77.0: no failed jobs; hazard scan: no errors.

Outside this PR: the website docs and release page (branch website/docs-2.78), the share viewer artifact for covers on published pages, and the phone releases (release/1.34.0 in zennotesios, ZenNotes/zennotesios#79; release/1.1.48 in zennotesandroid, ZenNotes/zennotesandroid#136; each with fix/pdf-embed and feat/calendar-widget).

…e keyboard

A note can now carry a cover: a wide image across its top, the way Notion
draws one, that scrolls away with the text. Space l c, Add Cover… in the
palette or :cover opens the cover picker, one search field over eight
gradients, six public-domain photographs (Hokusai, Van Gogh, Bierstadt and
Cole from The Met's CC0 collection, Earthrise and the Blue Marble from
NASA), the images already in the vault, and an https link pasted into the
field. Upload image… takes a file from disk, and an image pasted or
dropped anywhere in the picker becomes the cover. The arrows (and
Ctrl+N/P, Ctrl+J/K) move a row at a time through the tiles; Enter picks.

The cover is frontmatter, in the keys Obsidian's banner plugins read, so
a vault shared with one of them shows the same banner:

    banner: "![[assets/lisbon.jpg]]"
    banner_y: 0.35

banner_y is the focal point from 0 (top) to 1 (bottom); Banners
Reloaded's percentages and Pixel Banner's 0..100 read the same, and a
note that uses cover: shows its image too, with edits keeping its key.
The value is an embed rather than a plain [[link]] on purpose: every
host's note index (desktop extractAssetEmbeds, the Go server's port, the
phones' vault-core) counts only ![[...]] as an asset a note uses. A plain
link left the Assets view calling a cover image unused and offering to
delete it, kept the image out of a published note's uploads, and could
leave the note out of an asset rename. As an embed it gets all three with
no scanner changed anywhere; reading accepts both forms. note-cover.ts in
shared-domain edits the keys through updateFrontmatterFields, so every
other frontmatter line stays byte-identical, and removing a cover drops a
frontmatter block it left empty.

Everything a pick brings lands in assets/ through importPastedImage, the
import every host already has, so covers work in a local vault, a remote
workspace over the Go server's asset endpoints, the web client and Cloud
sync, encrypted vaults included. Gradients are written as small SVG files
and gallery photos as WebP; a second pick reuses the file, so no
ZenNotes-only value ever lands in a note. The photographs ship as inline
chunks loaded on pick, because the packaged app's file:// CSP refuses a
fetch of a bundled file. A cover edit goes through the focused editor as
a transaction when the note is on screen, so u and Mod+Z take it back
like any edit, and the note is saved right after.

In the editor the cover is a CodeMirror block widget above the first line
(cm-note-cover.ts): a slot the pane portals the same NoteCover component
into that the Read view draws, so the two cannot drift. It bleeds to the
full pane width through measured CSS variables and keeps its gap as
padding, since CodeMirror measures a block widget by its border box.
Hovering shows Change cover, Reposition and Remove, and an image dropped
on the cover replaces it. Reposition is a mode: the cover becomes a
self-keyed surface, the picture follows a drag or the arrows (j/k too
with Vim on, Shift for 1% steps, Home/End), Enter writes banner_y and
Escape puts the old position back. Reposition Cover, Paste Cover from
Clipboard and Remove Cover are in the palette, and :cover takes paste,
reposition, a percentage, ! or remove, an https URL, a vault image or a
built-in name.

Settings, Typography, Note covers chooses Scroll with note (the default),
Pinned above the editor and Read view, or Hidden, which only stops
drawing. It is a portable preference, note_cover_placement under
[appearance].

Two fixes outside the feature's own files made it work. The command
palette refocused the editor after any command that did not open an
overlay it knew, so Change Cover… typed into the note behind the picker;
COMMAND_OVERLAY_FIELDS now names the picker and the reposition mode next
to the boolean flags. And leaving the Read view at its top landed the
editor on the first rendered block, below the cover and the frontmatter,
which scrolled the cover away; for a note that shows a cover the landing
now keeps the editor at its top. The same may be right for every note
with frontmatter; that is left alone here.

Not yet: the share viewer, Quick Look, PDF and DOCX export, the pinned
reference pane, floating windows and hover previews do not draw covers,
and the phones get them with the next core release.

Verified in the built app over CDP with isolated userData and config dir
(27 checks, Vim on and off): Space l c opens the picker; Dusk writes the
embed and its SVG; the cover spans the pane at its top; u and Ctrl+R;
:cover earthrise; the Assets view counts the cover as used; renaming the
image rewrites the banner and the cover follows; keyboard and drag
reposition save banner_y and Escape writes nothing; split and Read draw
it at full height; leaving Read keeps it in sight; a pasted and a dropped
image become the cover without embedding into the text; a cover: note
shows at its 30%; Pinned and Hidden, saved to config.toml; :cover!
restores the note byte for byte; with Vim off j does nothing in
reposition mode, the arrows do, and the palette opens the picker with its
field focused. Against a Go server built from znserver (6 checks):
:cover lagoon and a dropped image land in the server's vault, the cover
loads through zen-asset://remote, and :cover! restores the note.

Tests: the reader (every value shape, both keys, all three scales,
quoting, comments), the editor (block creation, key choice, rounding,
removal and the emptied block, a round trip), a cover following an asset
rename, and the palette's refocus rule. shared-domain 2,158, app-core
3,543, desktop 1,194 and quicklook 15 tests pass; typecheck clean.
Dave (Discord, 2026-10-27) embedded a PDF that showed fine on his Linux
desktop, but on his Android phone "Open as reference" and "Preview" both
opened an empty sheet. Android's WebView has no PDF viewer: an iframe
pointed at a PDF stays about:blank however right its URL is. On an
emulator the frame's URL, fetched from inside the WebView, came back 200
with application/pdf and the right bytes, and navigator.pdfViewerEnabled
was false. Every PDF surface in core is such a frame: the editor's inline
embed, the reference pane, the reading view's embed and an asset tab.

Where the engine says it has no viewer (canShowPdfInline), core no
longer draws the frame:

- The editor's PDF card always shows, even with PDFs in edit mode set to
  full. Its badge says "open", it has no Preview, and a tap hands the
  file to another app through openAssetExternally. That wins over a pin,
  so a PDF pinned on an older build opens there too.
- The reading view's standalone PDF embed becomes the same card.
- The reference pane and an asset tab show PdfOpenElsewhere: the name,
  why, and Open in another app. A pin made before this, or from the
  asset menu, lands there instead of on a blank sheet.
- openVaultAssetExternally takes { confirm: false }. A PDF viewer runs
  nothing, so a PDF goes straight to it without the "Open attachment?"
  question other attachments get.

An engine that gives no answer keeps the frame, and desktop Chromium,
Safari and Firefox answer true, so nothing changes there. Checked over
CDP on the desktop build (card, Preview frame and reference pane as
before) and on an iPhone 17 Pro simulator, whose WebView draws the PDF in
the pane. The Android shell had no openAssetExternally; zennotesandroid
fix/pdf-embed adds one.

The absolute path in Dave's embed
(/home/dave/pCloudrive/zennotes/assets/Tourtickets.pdf) played no part:
on the phone it resolves to assets/Tourtickets.pdf through the
unique-basename reading, and the vault-relative form behaved the same.

Tests: cm-live-preview-pdf-inline (the card for the absolute and the
relative path, full mode, an old pin, and the unchanged desktop card and
frame), local-assets-pdf-inline (the reading view both ways), pdf-inline
and PdfOpenElsewhere. app-core's suite and typecheck pass.
A PDF card pins its file to the note it sits in
(pinAssetReferenceForNote), but the Reference commands checked only the
global pin. With a per-note pin showing, the palette offered neither
Hide Reference Pane nor Focus Reference Pane, and
runAppCommand('ref.toggle') refused to run. The phones close their
full-screen reference sheet through that command, so Done and Back could
not close a PDF there (Dave, Discord, 2026-10-27; the shells' half is
fix/pdf-embed in zennotesandroid and zennotesiphone).

activeReferencePath reads the pin the pane shows, the open note's own
pin, else the global one, the way App.tsx already did inline. App,
ref.toggle and ref.focus now share it. Unpin Reference still means the
global pin; the pane's own x unpins a per-note one.

Tests: pinned-reference, and commands (Hide and Focus offered for a
per-note pin, Hide hides it, neither without a pin). On the Android
emulator the palette lists Show Reference Pane for Dave's pin.
… at phone width

At 360 px with Android's text size at 1.3x, the compact PDF card's "open
as reference" badge spilled out of its button and drew under the Preview
button beside it (Dave, Discord, 2026-10-27), and the file name was
squeezed to nothing; at 1.0x the name was already gone. The badge was
flex: 0 0 auto inside a button that may shrink (min-width: 0) and does
not clip, so once the row ran out of room the button shrank under its
badge.

The button now wraps: when the name and the badge do not fit on one line
the badge moves under the name, and the button clips anything that still
would not fit. The badge may shrink to an ellipsis as a last resort. The
expanded PDF header wraps its actions below the title the same way
instead of running past the card's clipped edge. At desktop widths both
stay on one line as before.

Checked on the Android emulator at 360 px and 1.3x (the whole name, the
badge under it, nothing past the card), on an iPhone 17 Pro simulator
beside Preview, and on the desktop build, unchanged.
A cover drew in the editor and the Read view only. The public page of a
published note and the macOS Quick Look preview now draw it as well,
read-only, at the note's focal point: ReadOnlyNoteCover is the app's
frame without the buttons, the drop target or the picker imports, which
would have carried the cover gallery into both bundles.

Publishing a note with a gradient cover failed outright ("The
attachment assets/cover-gradient-dusk.svg is not a supported image"):
the service refuses SVG uploads on purpose, since a served SVG can run
script. The file does not need to travel. Its name names the gradient,
the specs moved to shared-domain (note-cover-gradients.ts), and the
share viewer redraws the same SVG from them, so the publisher now
leaves built-in gradient files out of the upload. An SVG of the
author's own is still refused, with the same message.

Quick Look uses the file its embed scan already found for a ![[...]]
cover. Any other form readNoteCover accepts (a plain [[link]], a bare
path, cover:) the page asks for by reference over a new zenql://
cover route, which resolves it with the extension's AssetResolver, so
the frontmatter is parsed in one place and the extension keeps its
limits (inside the vault, media files only). A cover from the web
stays out, as the preview never goes online, and Hidden in Note
covers hides it there too.

How to test locally: build the share viewer, serve dist with a page
carrying a #zen-share-data payload whose markdown has
banner: "![[assets/x.webp]]" and an assets entry for it; then one with
banner: "![[assets/cover-gradient-dusk.svg]]" and no assets. For Quick
Look, follow apps/quicklook/README.md's test-copy steps and press Space
on notes with each cover form.
…works, on the right task

The task-index walks in shared-domain's tasklists.ts (editTaskAtIndex,
behind every set*AtIndex mutator; taskLineNumbers, behind moveTaskLine;
and takeTaskLineAtIndex) matched TASK_LINE_RE against raw lines. A line
saved on Windows ends in "\r", which the pattern's "$" does not match,
while parseTasksFromBody turns "\r\n" into "\n" before it counts. So
the scanner listed a CRLF note's tasks and every action on them did
nothing, and in a note mixing endings the walk counted fewer tasks than
the parser and acted on the wrong one: toggling index 1 of
"a\n b\r\n c\n" ticked c.

Found while building the phones' Calendar widget, whose ticks go
through toggleTaskFromList: on the iPhone, a widget tick on such a note
was dropped.

The walks now match the line with its "\r" taken off and put the ending
back on whatever they write, as extractOpenTaskBlocks already does for
rollover. A line without "\r" matches exactly as before. The MCP
server's own task-line copy (apps/desktop/src/mcp/vault-ops.ts) is one
of the synced copies and is left alone here; it likely has the same
gap.

Four CRLF cases in apps/desktop/src/main/tasklists.test.ts: a toggle
keeps the ending, mixed endings tick the right task, take and move.
desktop 1,198 and shared-domain 2,145 tests pass; typecheck clean.
…get shows

Dave (Discord, 2026-10-27) asked for the phone Calendar sheet on his
Home Screen: the month with week numbers and marks, and the week's
tasks below with boxes to tick. A widget cannot run this app, so the
phone shells draw it natively from a snapshot; core supplies what it
shows and takes the ticks back.

lib/calendar-data.ts holds the panel's grid, ISO week, word-count dots
and note measure, moved out of CalendarPanel unchanged, so the widget
and the panel share one copy of those rules.

getCalendarWidgetData (@zennotes/app-core/calendar) builds the grid
marks and the week agenda for the previous, current and next month with
the panel's own rules: daily-note tasks take the note's date, only open
tasks are listed, and a trashed note's leftover tasks are skipped. Each
inline task carries its line number, the line as it is on disk, and the
line after core's own toggleTaskAtIndex, so a shell that writes a tick
outside the app swaps one known line for the other and never parses a
task. There is no new task-line parser copy.

setTaskChecked (@zennotes/app-core/tasks) applies a tick the widget
logged. It looks the task up in the body toggleTaskFromList will edit
(the open buffer, else the file), by id or by the same line when lines
above it moved, and reads the write back. It never ticks a cancelled or
forwarded task. It answers 'retry' instead of guessing when a clean open
buffer is behind the file on disk (editing that buffer would save it
over the newer file) or when the writer declined.

openDateNote, and a day plus a focused "Add a task for ..." field on
DateCalendar and CalendarPanel, serve the widget's links. The settings
snapshot adds weekly notes, week start and week numbers, which the
widget draws.

Desktop behavior does not change: with no initialDate or focusAdd the
panel orients, measures and draws as before. On the built desktop app
over CDP, a seeded vault shows the same dots, task marks, week label and
agenda. A rehearsal core from this tree ran in both phone shells on an
Android API 37 emulator and an iOS 27 simulator.

app-core 3,561 tests pass (new: calendar-widget-data, tasks-set-checked
and three CalendarPanel cases); typecheck clean.
…wer, Done and Back close the reference sheet, and the PDF card fits at phone width
…een, and ticks that land in Windows-saved notes

Core's half of the Calendar widget Dave asked for (Discord,
2026-10-27): getCalendarWidgetData builds the grid marks and week agenda
the phone widgets draw, and setTaskChecked applies the ticks they log,
matched in the body the toggle edits and read back. openDateNote and a
day plus a focused add field on DateCalendar serve the widget's links.
The panel's rules moved to lib/calendar-data.ts unchanged; desktop
behavior is the same.

Also the task-index walks in shared-domain's tasklists.ts now match a
line without its "\r", so ticking or editing a task in a note saved with
Windows line endings works everywhere, on the right task.

The phone shells' halves (Android e1d17d5, iPhone 264aaa5 on their
feat/calendar-widget branches) need a published core containing this
merge.
… theme

Mermaid fences have drawn with Pele since 2.71.0, pinned at 1.6.0. Pele
shipped six releases since (1.7.0 to 1.12.0) with no breaking change to the
API this app calls (`render`, `renderAsync`, `supports`, `PeleError` from
`pele/lazy`), no new diagram types, and fixes that show up in real notes:

- Labels on a fill the author chose (`classDef`, `style`, a sequence `rect`
  or `box`) pick near-black or white text, whichever contrasts with the
  fill. On 1.6.0 they painted in the theme's text colour, so a dark node was
  unreadable in a light theme and a light one in a dark theme. Author fills
  on subgraphs, composite states and the like now draw at full opacity.
- Labels format `<u>`, `<sub>`, `<sup>`, `<code>` and `<kbd>`, and text that
  only looks like a tag stays: `List<T>` used to come out as `List`.
- Chinese and Japanese labels wrap between characters instead of stretching
  the diagram; C4 relation labels stop covering the shapes they join;
  sequence self-messages, timelines, sankey, treemap and block diagrams get
  layout fixes.

The two new colour variables, `--pele-fg-on-light` and `--pele-fg-on-dark`,
stay at Pele's defaults on purpose: they must contrast with the author's
fill, and no theme token is dark (or light) in every theme. The `.pele`
rule's comment says so.

The help text claimed other HTML in a label shows as written. Even 1.6.0
dropped unknown tags and kept their text; it now lists the tags that format.

Not taken up here: 1.8.0's `math` option (typeset `$$...$$` in labels
through a resolver the host supplies) and 1.11.0's `monospace` option.
Label math still shows as written.

The lockfile moves one hoisted entry and the five workspace specs
(app-core, desktop, web, share-viewer, quicklook); nothing else changes.
The phone shells get the new version through the core artifact.

Verified on the built desktop app over CDP, in apple light, apple dark and
gruvbox dark-hard: 27 fences across 22 diagram types render in Preview and
the editor's live preview, with no console errors; the pele-runtime chunk is
still absent at boot and loads with the first diagram. Mermaid's own docs
(550 examples) render the same set on both versions: 534 draw, the 16 ZenUML
examples do not, none newly fail.
…very theme, with sub/superscript, underline and code in labels (Pele 1.12.0)
A Mermaid label like `A["$$\frac{a}{b}$$"]` showed its TeX as written.
Mermaid itself typesets label math with KaTeX, so a diagram written for
mermaid.live, GitHub or Obsidian read differently here, and the manual had
to list it as a known gap. Pele 1.8.0 added a `math` render option for
exactly this: the host typesets each formula and returns its markup and
size, and Pele lays the label out around it.

lib/mermaid-label-math.ts is that typesetter, passed through
renderOptions in mermaid-render, so Preview, the editor's live preview,
the expanded view, Quick Look, shared pages and the phones all get it.

What it chooses:
- KaTeX, always. Label math is LaTeX because the syntax is Mermaid's, so a
  note whose math renderer is Typst still gets KaTeX in its diagrams.
- MathML, not KaTeX's HTML. Pele puts the markup in a foreignObject, and
  WebKit (the iPhone app, Quick Look, Safari) positions KaTeX's HTML wrongly
  inside SVG.
- Measured, not estimated. Labels sized for something other than what
  paints in them were the clipped-label bugs of mermaid.js's HTML labels
  (#529, #911). Each formula is measured as the exact element Pele
  receives, at the font size Pele asks for, in an off-screen probe with the
  line height and wrapping of the box Pele wraps it in, then rounded up to
  whole pixels. Successful measurements are cached by size and source, so
  typing next to a diagram does not force a layout per formula per key.
- Lazy. The module is reached only through pele-runtime, so it adds
  nothing before a diagram is drawn; KaTeX itself already ships in
  vendor-markdown with the editor.

What it does not do: class members and diagram titles keep `$$` as written,
as in Mermaid; TeX KaTeX rejects shows as written, the way a broken formula
reads in a note; the Math size setting does not scale diagram math, which
follows the diagram's font size; formulas use the browser's math font, so
they can look slightly different from the note's KaTeX formulas. KaTeX's
default refuses \href and \url, so label math never becomes a link.

The in-app manual and the website docs describe it.

Verified on the built desktop app over CDP, in apple light, apple dark,
gruvbox dark-hard, and with Typst as the math renderer: 14 formulas across
flowchart nodes and edges, sequence messages and notes, state transitions,
a class relation, an ER relation, author fills and a mindmap typeset in
Preview and in the live preview, each inside its box with no overflow and
no console errors; text on a dark author fill is white and on a light one
near-black; broken TeX shows as written. The same diagrams in a WKWebView
(WebKit) place every formula inside its box.
The bundled CLI is the floor a desktop build guarantees, and 0.12.2 is
the newest release (ZenNotes/tui#20). It brings LaTeX to the terminal
preview: with [terminal] math = "latex", zn draws display math and TikZ
through the TeX installed on the user's computer (ZenNotes/tui#17), says
plainly that zn does not include TeX and prints the install commands in
zn doctor, and fits preview pictures to the pane as it grows or shrinks.
The pin is the release's terminal-release.json copied byte for byte
after it verified against packaging/release-signing/zn-release-1.pub in
ZenNotes/tui (source commit 3f1f638a, integration protocol 1, all four
archives it names matching checksums.txt). Staged through
`npm run terminal:stage`; the native darwin-arm64 binary reports
zn v0.12.2. The in-app manual's CLI card says 0.12.2 and what 0.12.0 to
0.12.2 change.
…eX math in the terminal preview with your own TeX
What's New opens on the first launch after the update with four
highlights. First, a note can carry a cover: a wide image across its
top, picked from the palette or Space l c, repositioned or removed on
hover. Second, a cover is plain banner frontmatter that Obsidian's
banner plugins read, with an ordinary image file in assets, and
published notes and Quick Look show it. Third, Mermaid labels typeset
$$ math and keep text readable on a node's own fill. Fourth, the
bundled zn 0.12.2 draws LaTeX in its terminal preview.

The phones' Calendar widget and PDF fixes stay out on purpose: the
window is desktop only, and the phones announce through their stores.
Comment thread packages/app-core/src/components/ReadOnlyNoteCover.tsx Fixed
CodeQL flagged the release PR (alert 136, js/xss-through-dom, high):
ReadOnlyNoteCover set an <img> src from a URL that starts as page text,
the share page's #zen-share-data payload. It was not an attack path:
an <img> runs no script whatever its source, readNoteCover keeps only
http(s) links as web covers, lookupShareAsset returns only http(s)
uploads, and the rest are this app's own gradient data URLs and Quick
Look's zenql:// route.

The component now says so where the URL meets the element.
readOnlyCoverSource keeps http(s), zenql:// and data:image/ and draws
no cover for anything else, so a later caller cannot point a share page
or a Quick Look preview at another kind of URL. Every source the two
surfaces hand over today passes unchanged.

Tests: ReadOnlyNoteCover (the allowed sources, including a built-in
gradient; javascript:, a text/html data URL, file: and a bare path
refused; the img drawn at its focal point; no frame for a refused
source). app-core 3,599 and quicklook 18 pass; typecheck clean.
…d not pick

Found while recording the 2.78.0 diagram clip: from Gruvbox Hard dark,
Cmd+Shift+P, Themes, then typing l switched the app to Nord Light with
no row highlighted, and the next keystroke kept it there. Each new
filter runs two effects in one commit: one re-syncs the highlight onto
the current theme, which lands on the next render, and the preview one
read `active` in the same commit, still an index into the previous
list. It previewed whatever sat at that index in the new list, and the
next re-sync found that theme current and locked onto it. Choosing
Themes from the recent commands, with an empty query, could do the same
with an index from the command list.

The preview now remembers the mode and the list it last saw and skips
the commit in which either changed (and the first render), so it only
follows an `active` that was set against the list it indexes. Arrows,
the pointer and Enter preview and keep exactly as before, and Escape
still reverts.

The same take showed the theme search could not find "apple light" or
"gruvbox dark hard": rankItems scores each field alone and the query
spans two. rankThemes (lib/themes.ts) adds the row read as one phrase,
family, mode, flavor, label, below the label's weight, so typing a
label still ranks it first.

Tests: rankThemes (family plus mode or flavor, a label first, every
theme for an empty query). In the built app over CDP with isolated
stores, from Gruvbox Hard dark: typing "light" leaves the theme alone,
ArrowDown previews Apple Light and Escape reverts, "apple light" lists
Apple Light first, and Themes from the recents keeps the theme; six
runs passed. One earlier run read the theme 500 ms after ArrowDown
before it repainted, with the machine at a load average of about 38.
Comment thread packages/app-core/src/components/ReadOnlyNoteCover.tsx Dismissed
@adibhanna
adibhanna merged commit 52ab86f into main Oct 27, 2026
11 checks passed
@adibhanna
adibhanna deleted the v2.78.0 branch October 27, 2026 20:23
@adibhanna
adibhanna restored the v2.78.0 branch October 27, 2026 20:23
@adibhanna
adibhanna deployed to boundary-artifacts October 27, 2026 20:48 — with GitHub Actions Active
@adibhanna
adibhanna deployed to boundary-artifacts October 27, 2026 20:49 — with GitHub Actions Active
@adibhanna
adibhanna deployed to boundary-artifacts October 27, 2026 21:05 — with GitHub Actions Active

This branch was successfully deployed

1 active deployment
boundary-artifacts — 52ab86fd Deployed Oct 27, 2026 by adibhanna via draft #78
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