Skip to content

ZenNotes 2.72.0 - #963

Merged
adibhanna merged 15 commits into
mainfrom
v2.72.0
Oct 19, 2026
Merged

adibhanna merged 15 commits into
mainfrom
v2.72.0

Conversation

@adibhanna

Copy link
Copy Markdown
Contributor

ZenNotes 2.72.0: desktop, released with zn 0.9.0 (already out), server 2.72.0 with Docker, the boundary packages, the website release page, and both phones on core 2.72.0.

New

  • Task folders ([Feature request] Alongside "Folders excluded from tasks" option to "Only these folders for tasks" #962, cc4cdb51): tasks.includedFolders beside excludedFolders in vault.json, one shared rule (isPathExcludedFromTasks(path, excluded, included)), both lists follow a folder rename, a Settings row and "Use as Task Folder" in the folder menu; the MCP server and the Node zn follow it. The Go server (ZenNotes/znserver 34da661) and zn 0.9.0 (ZenNotes/tui a0d335c) mirror it.
  • Cloud errors you can act on (72ab890e, edea7da0): one translator words every Cloud failure, a Details disclosure keeps the technical line with Copy details and Report an Issue, a device Cloud no longer accepts signs out, a failed loopback sign-in shows its reason.

Fixes

Also

  • Bundled zn 0.9.0, verified against zn-release-1 (3e7e2356); What's New 2.72.0 (a6b96a65); version bump (5a52b484).

Gates on this head (3792c985), in a clean checkout with its own npm ci: typecheck 8/8 uncached; shared-domain 2,048, app-core 3,308, desktop 1,183 and quicklook 15 uncached; fresh build:prod green; npm run pack and the packaged launch check pass (page target, rendered, 1.5 s, 2.72.0, Developer ID team WYY7PK57DM); vim-editor, sidebar-vim and editor-improvements pass on the first run. ci-sweep.py v2.71.0: no failed jobs. hazard-scan.py --since v2.71.0: no hazards. dash-lint clean on the notes and every commit. An Opus review of the five side-session commits found the two Cloud bugs above; both are fixed here with tests.

From the sidebar, `m` and Change color… or Change icon… opened a picker,
but the keys kept driving the sidebar behind it. j and k moved the row
cursor under the backdrop, l opened that row and pulled focus out of the
dialog into the editor, and Enter opened the row instead of picking. Only
Tab reached the choices. With Vim off, arrows and Enter did the same.

The picker did hold DOM focus. Panel keys follow the store's focused
panel instead, which stays on the sidebar while a dialog is open, and
both routers (VimNav, and the Vim-off handler in App) stood down only for
dialogs that carry a data-* marker of their own. The pickers are plain
Modal shells with none. Both routers now also stand down for any
aria-modal dialog, which every Modal panel sets, so the other unmarked
dialogs are covered too.

The pickers then answer the keys themselves: arrows move between the
choices, and h j k l do with Vim on, as single-key motions do everywhere.
Up and down keep the column the grid shows (three, or two on a narrow
window), and an edge stays put. Enter picks the focused choice, Escape
closes, and the picker opens on the color or icon already set, with a
focus ring on the choice.

Left alone: pane focus (Alt+h/j/k/l) and the global shortcuts guarded in
App keep their own marker lists. Closing that gap is a separate change.

Nine tests for the grid. Driven over CDP on a built app with Vim on and
off. Before, j j l moved the sidebar from Bravo to Delta and opened Delta
behind the open picker, and the shipped 2.71.0 did the same with arrows
and Enter with Vim off. After, the sidebar stays put, the keys walk the
grid, and Enter writes the color or icon to vault.json.

Reported by @Luistfer.
…es (#960)

The follow-up the #960 fix left open. Two keydown guards in App decided
"is a dialog open?" from a list of data-* markers (context menu, prompt,
confirm, Cloud conflict) plus the palette and Settings flags. Dialogs the
shared Modal shell draws without a marker of their own, such as the
sidebar icon and color pickers, Publish and the workflow pickers, were
invisible to them.

So under the sidebar color picker, Alt+L moved focus and the focused panel
into the editor behind the backdrop, Ctrl+1 switched the tab under it, and
Mod+N and Shift+Mod+N each made a note there and pulled the keyboard into
its title field.

Both guards now use isOverlayOrDialogOpen(), which keeps every marker the
old lists checked and adds any aria-modal panel. The store flags stay as
they were, so palettes and Settings behave as before: the keymap recorder
still takes a focusPane chord (#124), and Mod+P is still no toggle over an
open palette (#938).

Driven over CDP on a built app, picker open from a right-clicked sidebar
row: before, Alt+L, Ctrl+1, Mod+N and Shift+Mod+N all acted behind it;
after, all four leave the dialog, tab and vault alone. Without a dialog,
Alt+H and Shift+Mod+N still work, Settings still keeps Alt+H, the recorder
captures Alt+L, and the search palette's Ctrl+N and Ctrl+P still move.
In a quote or callout, `$$` and Enter auto-closed the math block with its
body line and closing `$$` indented by spaces instead of quoted. A line
without its `>` leaves the quote, so the block fell out of the callout:
Preview drew an empty math box in the callout and the formula as plain
text below it. A fence (```) opened there broke the same way.

The snippet's continuation prefix turned every marker before the opener
into spaces, which is right for a list item (#405) and wrong for a quote.
It now keeps quote markers and still turns list markers into spaces, so
`> $$`, `> > $$`, `> - $$` and `- > $$` each continue the way their lines
need. A quote marker may also be followed by more than one space, so a
list item's continuation inside a callout (`>   $$`) opens a block at
all; it never auto-closed before. The #405 fallback for Enter on an
existing fence opener keeps the `>` the same way.

Lines typed inside a math block in a callout already continued with `> `
on Enter, through the markdown command. Left alone: Enter on a content
line of a fenced code block in a quote still starts a bare line, because
the markdown command stands down inside fenced code. That is a separate
change.

Twelve tests: eight for the snippet and four through the editor's real
Enter chain, whose harness now mounts the snippets ahead of the markdown
keymap as EditorPane does (in the old order, markdown's Enter took `$$`
in a quote first and the snippet never ran). Driven over CDP on a built
app: before, `> $$` and Enter wrote two-space lines; after, with Vim on
and off, every line is quoted, typing on continues with `> `, and Preview
draws the math inside the callout.

Reported by @athu16.
Asked on Discord what they would change about Cloud, uNyanda said the
error messages: not everyone using Cloud is technical, so a message
should say what went wrong, why, and what to do about it.

They could not. IPC carries only an error's text across to the window,
so desktop users read whatever a failure happened to say: "fetch
failed" when offline, "The operation was aborted due to timeout",
"ZenNotes Cloud object upload failed (411).", the server's
"Authentication is required." for a device removed on the website, and
about thirty internal sync checks such as "Expected sync sequence 41,
received 43". The conflict review printed the Electron wrapper whole,
"Error invoking remote method 'cloud-vault-conflict:resolve': Error:
...", and so did the publish dialog.

One translator now words every Cloud failure: describeCloudError in
shared-domain/cloud-error-copy. It reads the HTTP status and code where
they still exist, which is the desktop main process before an error
crosses IPC (every cloud* handler goes through it) and the phones,
whose hosts run in-process. It knows the runtime's network and timeout
texts, the encryption error codes, and disk errors. Internal sync
checks now throw cloudProtocolError or a CloudIssueError (download
incomplete or damaged, Cloud changed during a first sync, two names
that differ only in case) with their old messages unchanged, so tests
and logs still match them. Anything else is a sentence this code wrote
for people and passes through.

The technical line is kept for support, out of the way. Settings ->
Cloud shows the sentence with a closed Details disclosure under it:
the original text, status and code, Copy details, and Report an Issue
(the existing command's name). The status bar, its hover text and
toasts show the sentence alone. The sentences were approved row by row
from a before-and-after table, and every place they point to carries
that name on screen: Connect ZenNotes Cloud, Disconnect, Sync now,
Review, This vault, Save archive, and Billing on zennotes.org.

Two behaviors changed with the words, both approved:

- A device Cloud no longer accepts (401, a revoked device) now signs
  out on the desktop too, as the phones already did. The status row
  reads "Signed out of Cloud" and Settings says why above Connect
  ZenNotes Cloud, until the person connects again.
- A failed sign-in on the loopback path (Windows, Linux, dev builds)
  showed its reason nowhere: the browser said only that sign-in
  failed, and Settings kept "Finish signing in in your browser" for up
  to five minutes. Every platform now shows the reason in a dialog and
  returns to Connect. That is the path uNyanda's keyring error took.

Offline or timed-out runs read "Can't reach Cloud" in the status row.
Storage, file-count and vault limits, a plan without Sync, Backups or
Publishing (including a free month that ended), the encryption
passphrase and recovery-key errors, and the conflict review's stale
choices are reworded to match. A help entry, "When Cloud reports a
problem", names Details and Copy details; the website mirror waits on
its own branch.

Left alone: server wording. The website sends developer text in
several answers (RESTORE_FAILED forwards a raw exception, bare aborts
send empty messages); the app words those codes itself, and the server
fixes are listed for a separate change.

Tests: the translator has 24 of its own, and the suites that asserted
the old wording now assert the new (shared-domain 2,018, app-core
3,291, desktop 1,177). Driven over CDP on a built app with both stores
isolated, against a fake Cloud on 127.0.0.1 with the browser, keychain
and dialogs stubbed: a refused sign-in, a keychain refusal, a server
500, a dropped connection, a revoked device and a plan without Sync
each show the approved words, Details holds the technical line, Copy
details copies it, and the revoked device really signs out (29/29).
#961)

The follow-up #961 left open. With the caret on a code line of a fence
inside a quote or callout (`> xy` under `> ```js`), Enter started a bare
line. A line without its `>` leaves the quote, so whatever was typed next
fell out of the callout, the code card ended there and the closing fence
opened a stray empty block below it. The markdown Enter command returns
no markup context inside fenced code, and the default newline copies only
leading whitespace, which on a quoted line is nothing.

The fence Enter command that already handles the opener line (#405) now
also takes the content and closing lines of a fence whose opener sits in
a quote. The new line gets the opener's prefix with list markers turned
into spaces and quote markers kept, the same rule as the opener and the
block snippet, plus whatever indentation the line carries past it. Where
the quote markup ends is read from the parse, so code that starts with
`>` (a `>>>` prompt) is not taken for a marker. Otherwise it behaves like
the default newline: whitespace after the caret goes, and a line left
holding only its markup is trimmed to it. The closing line keeps the
quote too, so the caret stays in the callout after the block.

Unquoted fences, several carets, and a caret inside the quote markup are
left to the default command, as is Vim normal mode, where Enter is the
<CR> motion. In a quoted fence the code language's own indentation (after
a `{`) is not applied: it counts columns from a line start that holds
the `>`.

Twelve tests through the editor's real Enter chain, mounted with the
note grammar so a ```js body parses as JavaScript, as in EditorPane:
content and closing lines, nested quotes, a fence in a quoted list item,
extra indentation, a mid-line split, a `>>>` line, a trimmed blank line,
Vim off, Vim normal mode, and two controls (a line with no `>` after the
quote, and an unquoted fence keeping its bracket indentation). Driven
over CDP on a built app with Vim on and off: before, every quoted case
typed a bare line and the card broke; after, each keeps `> `, `> > ` or
`>   ` and the block stays whole.

Not changed: Vim `o` and `O` open a bare line on any quoted line, fenced
or not. That is a separate change.
The website now words its own refusals (ZenNotes/website bfad4b9), and
the translator from 72ab890 matched a few of the old sentences to tell
limits apart. With the new text a vault-limit refusal read as a full
plan, a publishing attachment over the size limit read as a sync file
that "stays on this device", and a backup restore over a limit was told
that "some changes are waiting on this device".

Limits are now told apart by the dimension Cloud sends with them
(sync_active_vaults, publish_*), with the old and the new sentences as
the fallback for a service that sends none. Restore refusals ("Restoring
this backup ...", "A file in this backup ...") keep the service's own
words, as restore-a-note ones already did. GONE, a code the service now
sends for a bare 410, keeps its sentence too.

The invite-only answer said to choose a plan under Billing. In
invite-only mode a plan does not open Cloud, so it now says Cloud is
invite-only and the account has not been invited yet, as the service
does.

Two tests: limits by dimension and by the new sentences, and a 403
UNAUTHENTICATED (an outdated sign-in) treated as signed out.
shared-domain 2,020 pass.
ZenNotes Cloud keeps its change log for 90 days (sync.change_retention_days)
and answers 410 CURSOR_EXPIRED to a device whose cursor is older than what is
left. No app handled it. The coordinator asked /changes after the same stale
cursor on every run and every run failed, and unlinking and relinking did not
help, because the desktop keys its sync state by vault folder and server and
reused it. A laptop left in a drawer for a season could not sync again, and
the honest message from 72ab890 was "choose Report an Issue".

The coordinator now catches that answer (status 410, code CURSOR_EXPIRED) in
both pulls of a run and rebuilds from the manifest, then follows the log again
from the manifest's cursor. It lives in the shared coordinator, so desktop and
both phones get it with no host change.

A first sync knows nothing about this device's files. This rebuild still knows
what sync last agreed on for each, so it replays what the pruned log would
have said, item by item, from the difference between the tracked state and
the manifest, through the same path the change feed takes. Both repositories
already refuse to replace or delete a file whose bytes are not what sync last
agreed on, so that guard covers every write:

- Changed or renamed in Cloud, untouched here: lands.
- Edited here, untouched in Cloud: pushed as an edit on its tracked revision.
  Deleted here: pushed as a delete. Created here: uploaded.
- Edited on both sides: queued for a decision, never overwritten.
- New to this device: lands like a first sync. A queued decision moves to the
  Cloud version it now faces, without touching the file.

The order is what the manifest cannot give back: deletes run first so the
paths they free are free, then renames, then edits, then arrivals. Whether an
arrival is already on disk is judged from a fresh scan, because a delete just
before it can take away a file with the same bytes at its path. A chain of
renames through each other's paths can come out as conflicts, never as an
overwrite. If one file fails midway, what landed is saved at the old cursor,
so a retry rebuilds from there and does not redo a change undone since
(#813).

A file deleted in Cloud and edited here is kept and queued as a delete
conflict, as the feed would. The manifest lists only live files, so the
revision that deleted it is unknown, and an upsert onto a deleted item needs
that revision. Such a conflict carries a null Cloud revision, and keeping this
device's version sends it back as a new file. A file deleted on both sides
just drops the question.

The translator now says sync is catching up by comparing every file with
Cloud and that edits here are kept, instead of pointing to Report an Issue.
It shows only if a catch-up is cut short.

Tests: an expired cursor with no local edits (edit, rename, delete and new
file in Cloud, then the log resumes), with local edits (edit, delete, new
file, and a file both sides changed), a Cloud delete of a file edited here
(keep it, or accept the delete), a queued decision moved forward, a same-bytes
file re-created where a deleted one stood, and a failure midway. Also a
metadata-first host downloading only the changed file, the reference-mode
streaming path, the phones' host service keeping the link, and the desktop
repository and state store on disk, once through the real desktop HTTP
client reading Cloud's 410 body. typecheck clean; shared-domain 2,030,
app-core 3,303, desktop 1,179 pass.

Not verified: a built app against a Cloud with a pruned log, and the phone
builds.
A vault moving into ZenNotes often has checkboxes everywhere. The only way to
keep them out of Tasks was to exclude folder after folder, and to remember
every new one. The ask was the other direction: one Tasks folder, and nothing
else counts.

The vault's `tasks` settings gain `includedFolders`, the Task folders, next to
`excludedFolders`. Empty means the whole vault, as before. Once it names
folders, only the notes inside them feed the Tasks list, boards and
calendars, and the excluded list still carves subfolders out of them. The
rule lives in the shared `isPathExcludedFromTasks`, whose new third argument
is optional, so a caller that passes only the excluded list keeps today's
behavior. Desktop main, the MCP server and the Node zn read both lists, and
`includeExcluded` / `--include-excluded` scan past both.

Settings, Tasks, Checklists shows the Task folders above the excluded folders
(one row component serves both now), and the sidebar folder menu, which `m`
opens in Vim mode, gains Use as Task Folder / Stop Using as Task Folder next
to Exclude from Tasks.

Two things came along because a Task folder makes them matter more. The
exclusion toggle rebuilt `tasks` from its own list, so excluding a folder
would have dropped the Task folders; both toggles now go through one helper
that keeps the other list. And a folder rename or move now repoints both
lists, in main and in the renderer's copy, because a renamed Task folder left
behind would match nothing and empty Tasks. The exclusion list never followed
a rename either.

Left alone: the phone bridges apply the lists themselves (the iPhone passes
only the excluded list, Android neither), so mobile is a follow-up and the
in-app help leaves it out for now. An older build that rewrites vault.json
rebuilds `tasks` from the fields it knows and drops `includedFolders`.

Tests for the rule, the rename rewrite, the toggle helper, the MCP scan and
Settings search ("tasks folder" still finds where new task files go). Driven
over CDP on a built app: `m` on a folder, a filter and Enter set the Task
folder and Tasks dropped every other note; excluding a subfolder inside it
kept both lists; Stop Using brought the whole vault back; renaming the folder
moved both entries and Tasks kept its contents.

Reported by @davi-jorge-art.
The bundled CLI is the floor a desktop build guarantees, and 0.9.0 is the
newest release. It carries two zn releases into a desktop-managed install
from the first launch instead of the first daily update check: 0.8.1,
where a finished task note stays in its Custom status column on the TUI's
board and `zn mcp` gives assistants the desktop's instructions again, and
0.9.0, which follows the vault's Task folders (#962) in `zn task list`,
`zn task watch` and `zn mcp`, and makes the TUI's Settings save both Tasks
folder lists under `tasks` where every app reads them. 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 a0d335c, integration protocol 1, all four archives
matching checksums.txt). Staged through `npm run terminal:stage`; the
native darwin-arm64 binary reports zn v0.9.0. The in-app manual's CLI
card says 0.9.0 and what 0.8.1 and 0.9.0 change.
What's New opens on the first launch after the update with four
highlights. First, Task folders (#962): where to turn one on and what it
does to every other checkbox, with where both folder lists live. Second,
Cloud errors that say what happened and what to do, and where Details,
Copy details and Report an Issue sit. Third, a device back after months
away catching up by itself, keeping the edits made while it was away.
Fourth, the pickers taking the keyboard (#960) and blocks staying inside
a callout (#961).
A pre-release review found that the error translator (72ab890) read any
401 as Cloud refusing this device's sign-in. Signing out then revoked the
device on the service: logout() sends DELETE /devices/current with the
token, which was still valid. Two 401s that are not Cloud's refusal took
that path. A presigned direct upload the storage host answered 401
arrives as DIRECT_UPLOAD_FAILED with the storage's status, and a proxy or
captive-portal page answering 401 arrives with no Cloud JSON at all. A
storage hiccup during a large upload could revoke a device whose
sign-in was fine.

Only Cloud's API answer signs out now: the code UNAUTHENTICATED (401, or
403 for an outdated sign-in) or ACTIVE_DEVICE_REQUIRED, or a code-less
401 or "Invalid ability provided." 403 whose body was Cloud's JSON. Any
other code keeps its own words, so the upload reads as a large file that
could not be uploaded and will be tried again. A body that was not
Cloud's JSON (the clients' "ZenNotes Cloud request failed (401).",
"invalid JSON response" and "unexpected response") reads as an answer
from something other than ZenNotes Cloud, such as public Wi-Fi.

Found alongside: a 429 or 5xx during the sign-in exchange said "sync
will try again in a few minutes", and an unreachable service said "sync
picks up where it left off". Sign-in never retries by itself, and the
request it answered is already spent. cloudSignInExchangeError words
those three as "choose Connect ZenNotes Cloud again" and keeps the
technical line. Desktop wraps its exchange fetch with it, and the phones
get it through CloudAuthFlow. A refused code keeps the existing
"didn't go through" sentence.

Tests: a storage 401, three proxy bodies, and the real signed-out cases
(401 with and without the code, 403 UNAUTHENTICATED,
ACTIVE_DEVICE_REQUIRED), and sign-in wording for 429, 503 and an
unreachable service in the translator, the desktop CloudAuthManager
(which also stops waiting) and the shared CloudAuthFlow. Typecheck
clean; shared-domain 2,043, app-core 3,308 and desktop 1,183 pass, run
with other agents' uncommitted edits present in the checkout. Not
rebuilt or driven in the app, by request: the release gates run
elsewhere.
…921)

On macOS, with a Chinese or Japanese input method on and Vim mode on, a
markdown file opened from outside the vault took the input method's text
in normal mode: hjkl made the candidate window flash, and `ia` was typed
into the file instead of entering insert mode. Notes in the vault were
fine, because the main editor carries the input-method guard from #84 and
#464, which makes the editor non-editable outside insert mode so an input
method has nowhere to compose. The floating note and Quick capture carry
it too. The external-file window, the pinned reference pane and the
template editor built Vim without it.

All three now install the guard the same way, following the same
setting (Keep the input method out of normal mode). A new test reads
every app-core source that
turns Vim on and fails when one of them leaves the guard out, which is how
three editors slipped through.

Checked over CDP with a file opened from the command line outside the
vault, Vim on, normal mode: on the installed 2.71.0 the editor was
contenteditable and an input-method commit (Input.insertText "ia") wrote
`ia` into the file; on this build the editor is not editable in normal
mode and the same commit changes nothing.

Reported by @tonghejiao.
A pre-release review found a way for 6db3043's rebuild to remove a note.
While a device was away, Cloud renamed note Y from B.md to C.md, then note
X from A.md to B.md. The rebuild cannot see that order and landed renames
in tracked-item order, so X could go first, find B.md still held by Y, and
queue a move conflict whose local side was Y's file at B.md. Y then moved
on to C.md. With nothing left at B.md, the conflict's local side was
looked up by hash and landed on Y's file at C.md, so choosing the Cloud
version wrote B.md and removed C.md. The next sync sent Y's delete to Cloud
and uploaded A.md again as a copy of X. A rename with an edit made the same
false conflict, without the loss.

Renames, with or without new bytes, now land in passes: one waits while
the path it moves to is still the tracked path of a file that is itself
leaving, and goes once that file has moved. A queued decision never moves
its file, so it holds its path. Only a true ring (A to B, B to C, C to A)
cannot be ordered without a temporary name. It lands as before: the
repository refuses each move into a taken path, nothing on disk changes,
each note is queued for review, and choosing Cloud on each settles the
rotation. The no-overwrite promise is unchanged.

Also corrects a claim in 6db3043. It said the translator's
CURSOR_EXPIRED words show when a catch-up is cut short. They cannot: the
coordinator rebuilds on that answer, and the pull after the rebuild starts
at the manifest's cursor, which Cloud never refuses (it refuses only a
cursor below changes_pruned_through). A rebuild cut short surfaces its own
failure, and the next run rebuilds again. The claim is dropped, not made
reachable. The comment now says a current sync never shows the words; they
stay for anything else that meets the code.

Three tests through the portable repository: the B to C, A to B chain in
the order that used to fail (no conflict, nothing pushed, a second sync
clean), a longer chain with an edit in the middle, and a three-way ring
(queued, files untouched, settled on Cloud, then a clean sync). The two
chain tests fail without the ordering. typecheck clean; shared-domain
2,046, app-core 3,308, desktop 1,183 pass.
Every image pasted from the clipboard landed in assets/ as image.png,
then image 2.png, image 3.png and on. The naming already falls back to
`Pasted Image <date> <time>` when a paste comes with no name, but
Chromium, which the desktop app and Android's WebView run on, names every
clipboard bitmap image.png, and that placeholder was kept as if someone
had chosen it.

pastedImageFilename (shared-domain/pasted-image.ts, used by desktop main,
the web client and the iPhone bridge) now treats the bare stem `image` as
no name, so a paste gets the timestamped one, as Obsidian does with
"Pasted image <timestamp>". A file copied from the Finder or Explorer
arrives under its own name and keeps it, including names that only start
with image. The in-app manual says so where it describes assets/.

Not done here: a prompt for the name or a naming pattern setting, which
the issue mentions as options; the timestamp covers its ask for unique,
meaningful names.

Two tests. Driven over CDP on a built app, pasting a PNG named image.png
into a note twice: the installed 2.71.0 wrote image.png and image 2.png;
this build wrote Pasted Image 2026-10-19 142312.png and Pasted Image
2026-10-19 142314.png, with matching embeds.

Reported by @0xFFO.
@adibhanna
adibhanna merged commit 3792c98 into main Oct 19, 2026
10 checks passed
@adibhanna
adibhanna deleted the v2.72.0 branch October 19, 2026 19:51
@adibhanna
adibhanna restored the v2.72.0 branch October 19, 2026 19:51
@adibhanna
adibhanna deployed to boundary-artifacts October 19, 2026 19:57 — with GitHub Actions Active
@adibhanna
adibhanna deployed to boundary-artifacts October 19, 2026 19:58 — with GitHub Actions Active
@adibhanna
adibhanna deployed to boundary-artifacts October 19, 2026 19:59 — with GitHub Actions Active

This branch was successfully deployed

1 active deployment
boundary-artifacts — 3792c985 Deployed Oct 19, 2026 by adibhanna via draft #62
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