v2.13.2
The Vault stops being a flat pile of markdown. It holds a folder
structure you maintain, files each project under its own name, and
renders HTML documents as carefully as it renders markdown — from an
in-memory string in a sandboxed frame, with scripts off, because a
document that arrived by git pull is not your own writing.
The phone catches up with the browser. It can see whether your edits
reached the remote, browse by tag, follow [[wiki links]], show what
links back, and start today's note — instead of being somewhere you
could only read what you had written elsewhere. The admin UI itself
now fits a phone screen, which it never did.
Added
-
The admin UI works on a phone and a tablet. It was built for a
desktop three-pane layout, which on a phone meant a horizontal
scrollbar and two columns you could not reach. Every page now has a
narrow form: below 1024px the third pane becomes a slide-over, below
768px the list becomes the page until you pick something and the
navigation tree follows it into a drawer. Tables, spacing and the
topbar were sized to match.The drawer is one component rather than a pattern re-implemented per
page, and it is reachable from a labelled control in the header —
never from an edge handle alone, which is unreachable in practice. -
The Vault syncs from the phone. Repository status, a manual
commit / push / pull, and the auto-sync settings are all on the
phone now; previously the mobile app could edit documents but not
see whether they had reached the remote, so the answer to "did that
save get anywhere" was only available on a desktop. -
The mobile Vault has the knowledge layer the web has had. It
could create, read, edit, rename and delete a document, but nothing
that makes a vault more than a folder. It now browses by#tag,
shows a document's backlinks and its tags, follows[[wiki links]],
completes them while you type, jumps by heading, and opens today's
daily note from the same template the web writes.Two constraints worth knowing. The preview runs with JavaScript off
— a document can arrive bygit pull, so it is not the operator's
own writing by default — which means wiki-links are anchors on a
private scheme intercepted at navigation, and a heading jump lands
in the source view rather than scrolling the rendered page. And the
path rules are Unicode-aware on mobile: the web's ASCII-only
sanitiser turns笔记.mdinto--.md. -
The Vault stores and renders HTML documents, not just markdown.
Project documentation increasingly ships as HTML — exported from
Notion or Word, generated by typedoc, asciidoc or Sphinx — and a doc
library that cannot open those is a markdown library..html/
.htmare now first-class alongside.md: listed in the tree,
creatable, movable, and rendered.HTML is never served from opendray's own origin. There is no
endpoint returning a document astext/html; that would execute it
same-origin with the admin session, so a file pulled from a git
remote could take over the account. The body travels as a string
through the existing JSON read and is rendered from memory in a
sandboxed frame with an opaque origin. Scripts are off by default
— exported documentation is static markup and renders identically
without them — with a per-document opt-in for pages that genuinely
run code, remembered locally per file.Mobile gains a rendered view for the first time. It previously showed
only a document's source, which HTML would have made unreadable, so
preview covers both kinds: markdown is converted and rendered
through the same webview rather than drawn as native widgets, so the
two formats look like one product.Deliberately not symmetric:
[[wiki links]]are still scanned and
rewritten in markdown only. An HTML document can be the target of
one, but its own<a href>links are left alone — rewriting those on
a move is a different job, with relative paths, anchors and assets to
get wrong. Auto-derived paths (daily, project, personal notes) stay
markdown. And when a filename and a template disagree —guide.html
from the markdown Blank template — the filename wins, since the
operator chose the extension and the template was probably a default.In-document links work: clicking a table-of-contents entry scrolls to
that heading, and only links that actually leave the document open a
new tab. This needed more than it sounds like — see Fixed below.
Fixed
-
Vault auto-sync settings could not be saved. Whatever interval
you set, the dialog snapped back to "every 10 minutes" — the form
refetched every 8 seconds and overwrote the draft mid-edit, so Save
was never enabled and no setting had ever reached the database. The
interval is now a free-text Go duration with presets, and the
gateway rejects an unparseable one with a message instead of quietly
substituting a default. -
The mobile Vault's rename and delete were unfindable. They
existed, behind a long press on a row, with nothing on screen saying
so — reported in testing as missing outright, which for a hidden
gesture amounts to the same thing. Each row now carries a visible
actions button in place of a chevron that did nothing the row did
not already do, and the open editor offers rename and delete where
the web has always had them. -
Creating
guide.htmlon the phone producedguide.html.md. The
new-document path appended.mdunconditionally, so an HTML
document could not be created from mobile at all. -
Six hints rendered
<prefix>as literal text instead of
<prefix>. -
A table-of-contents link no longer loses the document. In the web
viewer, clicking an in-document#anchornavigated the frame away
and rendered the opendray app inside the document view. Asrcdoc
frame's document URL isabout:srcdocwhile its base URL is
inherited from the parent page, sohref="#install"resolved against
the gateway and became a real navigation. The base is now pinned to
the frame's own document, which also stops a pulled document's
relative URLs from resolving against the gateway's origin.The first attempt at "external links should open a new tab" used
<base target="_blank">, which retargets every link — so a
contents entry opened a blank tab instead of scrolling. Anchors are
now rewritten individually: fragment links are left in place, an
author's explicittargetis respected, and only links leaving the
document gettarget="_blank". Generated documentation is precisely
where this mattered — Sphinx, typedoc, asciidoc and Notion exports
all ship a fragment contents list, and markdown footnotes render as
fragment links too.Mobile deliberately does not pin a base:
loadDatamakes the
document's own URL and its base bothabout:blank, so fragments
already resolve in place, and pinning would have introduced the bug
rather than fixed it.
Changed
-
CI no longer runs the whole suite twice on every branch commit.
Afeat/**branch with an open PR matched both thepushand
pull_requesttriggers, and the concurrency group keys on the ref —
which differs between the two events — so neither run cancelled the
other.pushis now main-only;pull_requestalready covered branch
work. -
A project's documents are filed under the project's name. The
Vault is a project documentation library, and it filed every project
underprojects/— a folder naming what the whole library already
is. One level of nesting that told the reader nothing, on every path,
in every listing, and at the top of the git repository the Vault syncs
to. The operator's own notes were split off further still: agent docs
atprojects/<name>/…, the human scratchpad atpersonal/<name>.md—
the same project's material in two distant places, sorted by who
wrote it rather than by what it is about.New vaults now use
<name>/…with that project'spersonal.md
inside it. Existing vaults are not rearranged. The layout is
decided once, at first start, and written intoconfig.tomlas
[vault] layout. Recording it is the point: the alternative — work
the shape out from what is on disk each time — is the bug that put
one install's entire document library behind anotes/directory,
because a probe asking "does this folder have content?" changes its
answer the moment someone puts content there.The doc library offers the conversion, on web and on mobile, to
any vault that still nests projects. A migration that ships only as a
CLI command is one that only the people who wrote it ever run — every
existing vault would have stayed nested with its owner never learning
there was a choice. The offer is dismissible and never appears for a
vault that is already flat or has nothing to move, and the gateway
says the same thing once in the startup log for anyone who never
opens the UI.Nothing moves without a preview. Web and mobile both run the
migration as a dry run and show the real list — including what it
refuses to touch and why — before anything is renamed.
opendray notes flattendoes the same from a terminal. It defaults
to a dry run, drives the same rename the UI uses so[[wiki links]]
are repointed as it goes, repoints per-cwd project overrides, and
never overwrites: a destination that already exists is reported
and skipped, leaving both copies for you to reconcile.Where a project's notes live is now answered by the gateway —
/notes/inforeports the layout and the project mapping carries
personal_path— instead of being re-derived by web, mobile and the
CLI. Three implementations guessing is three chances to disagree, and
the CLI'snotes projectwas already guessing wrong.Reserved names step aside rather than collide: a project called
dailyfiles underdaily-docs, sincedaily/YYYY-MM-DD.mdbelongs
to the whole vault._- and.-prefixed names are reserved too.The conversion records the resulting layout itself and applies it to
the running gateway, rather than leaving both to the next restart —
otherwise a converted vault keeps derivingprojects/<name>and
personal/<name>.mdagainst the directories it just emptied. The
directories the migration empties are removed; one still holding
anything is left exactly where it is. -
Documents save when you say so, not on a timer. The editor wrote
after every pause in typing, so a long document rewrote itself to
disk over and over while it was being worked on. Saving is now a
button, or ⌘S / Ctrl+S, on web and on mobile. Leaving a document
still flushes unsaved text — that safety net costs nothing while
typing — and closing the browser tab with unsaved work asks first.Autosave was not the whole story behind the typing lag, so two things
that were: the editor streamed every keystroke to the page, which
re-rendered the whole vault tree and recomputed the outline per
character; and the tree re-rendered along with it. The stream is now
paced to what a sidebar can use, and the tree only re-renders when
the note list or the selection changes. -
Line numbers in the source view. On by default for HTML, off for
markdown, and toggleable either way. Numbers turn wrapping off, the
way every code editor does it: a wrapped line covers several rows, so
a gutter counting 1..N drifts on exactly the long lines an HTML
document is full of. -
The doc library can rename and delete a document. It could create
and edit one, and that was all — a daily note, or anything not bound
to a project, could be made but never moved or removed from the one
surface that lists everything. Rename goes through the move endpoint,
so the[[wiki links]]pointing at the old path are repointed rather
than left dangling, and a partial rewrite is reported instead of
being folded into a plain "renamed". Mobile gains rename too; it
already had delete. -
"New" and "Today" are reachable once a document is open. They
were in the header the whole time, pushed off the right edge by the
vault path: atruncateelement in a flex row still needs
min-width: 0, or it refuses to shrink below its content. Anyone
with a long vault path could only reach the two actions from the
empty state, which is exactly when they had no documents to leave. -
A folder holding the selected document can be collapsed again.
The tree re-applied "open the ancestors of the selection" on every
render, so the collapse landed and the next render undid it — and
"Collapse all" left that one branch open. Revealing the selection is
a response to the selection changing, not a rule about what must stay
open, so it now runs once per selection. -
opendray notesaccepts flags after the subcommand. Go'sflag
package stops parsing at the first non-flag argument, sonotes flatten --applyleftapplyfalse, performed a dry run, and advised
re-running with--apply— which is what had just been typed.
notes list --prefix=xsilently listed everything for the same
reason. Both orders now mean the same thing. -
The Vault is your documents. Agent skills and the MCP registry moved
out. One root held three tenants with nothing in common — the
operator's markdown, the skills opendray injects at spawn, and the MCP
registry — which is why the settings page could only describe it as
"notes, skills and git-versioned root", a sentence that parses only if
you already know the implementation. Opening the Vault showed
skills/andmcp/sitting among your folders, and because Vault
Sync commits that same directory, they went to your remote: on one
install a private docs repo had picked up opendray's own
skills/secretary/SKILL.md, and agit clean -fdthere would have
deleted the gateway's skills.New installs get
~/.opendray/vaultfor documents,~/.opendray/ skillsand~/.opendray/mcpbeside it, and a Vault repo holding
writing and nothing else. Existing installs are not moved: any
root with content still in the old place keeps being used, the
settings page prints where everything actually resolved, and says
plainly when it is still the shared layout.vault.notesand
vault.skillskeep working;vault.rootnow means the documents
directory, and[skills].rootis the new spelling. Machinery
directories that do sit inside the Vault are hidden from the doc
library and added to an opendray-managed.gitignoreblock, so
nothing new gets carried to your remote. Anything already committed
needsgit rm --cached— opendray will not rewrite your repo.Path resolution used to be reimplemented in four places (the gateway
plus each ofopendray notes|skill|mcp) with different precedence in
each, so the CLI could read a different directory than the running
gateway. There is now exactly one resolver.
Fixed
-
Verifying a git credential now checks that it can push, not just
read. A GitHub fine-grained token's Contents permission has three
levels — No access, Read-only, Read and write — and every check the
verification made passed identically for a Read-only one. So it went
green, the Vault pulled happily, and the first push came back
remote: Write access to repository not granted ... 403— the same
message a token with no Contents produces on a plain fetch, which
sends you looking at everything except the token.Verification now also probes git's receive-pack advertisement:
literally the first requestgit pushmakes, asked with the same
credential over the same protocol, and a plain GET that changes
nothing. If it is refused, push will be refused. Because it is git's
wire protocol rather than a forge API, one probe covers GitHub, Gitea
and GitLab, and it cannot disagree with what git actually does. A
read-only credential now reports "CANNOT push (read-only)" and names
the setting to change. A forge answer that is neither a clear yes nor
a clear no is reported as nothing at all — telling someone their
working token is read-only is the same mistake pointed the other way. -
"Reset to remote" no longer silently destroys unpushed work. It
rangit reset --hardplusgit clean -fdbehind a confirmation
that named no quantity — survivable when the remote is ahead of you,
and not survivable when the remote is empty. A vault whose pushes
have all been failing is exactly that, and the two faults compose:
one operator lost 354 documents when a read-only token made every
push 403, the local commits piled up unpushed, a pull hit a rebase
conflict, and "reset to remote" looked like the way out of the
conflict.The endpoint now counts what exists only locally — unpushed commits,
modified files, untracked files — and refuses with a 409 and that
breakdown unless the caller explicitly confirms. The dialog quotes
the numbers and names example files instead of asking "are you
sure?". And confirming is no longer final: opendray parks the
unpushed commits on anopendray-rescue/<timestamp>branch and
stashes the working tree (--include-untracked, sinceclean -fd
is what destroys untracked files and no ref can hold those) before
resetting, then names the rescue branch in the success toast. A tree
that is already level with its remote loses nothing and still resets
in one click — a confirmation that fires on no-ops is one people
learn to dismiss.
Added
-
Git credentials are scoped per host and owner, so one forge can
hold several identities. One row per hostname assumed one identity
per forge, which breaks the moment you touch a personal repo and an
org repo on the same host: a fine-grained GitHub token is granted per
repository, so the token that reachesgithub.com/<you>/…generally
cannot reachgithub.com/<org>/…— and there was nowhere to put the
second one. Git host entries now take an optional Owner;
resolution prefers the owner-scoped credential and falls back to the
host-wide one, so existing setups keep working untouched. Vault sync
resolves the same way, which it previously could not: it only ever
looked up by hostname — and its auth panel now names the credential it
resolved to, saying plainly when the remote's owner has none of its
own and the host-wide one is standing in. -
Git hosts is now the authority for HTTPS git auth. A session's
push went out with whatever the machine offered — Xcode ships
credential.helper = osxkeychainenabled, so a stale keychain entry
answered silently and failed with an error describing a token nobody
remembered configuring. Pushes now authenticate with the configured
credential, and inherited helpers are blanked for HTTPS remotes even
when opendray has nothing registered: failing as "no credentials"
beats quietly succeeding as an identity you never chose. SSH remotes
are untouched — the agent is a deliberate, visible configuration. -
Disabling a git host entry now actually disables it. The toggle
changed nothing: credential resolution returned disabled rows and every
caller — vault sync, PR and issue listing, remote detection — used the
token regardless, so an entry switched off kept authenticating. The
check now lives in the resolver, which also makes disabling compose
properly: turn off an owner-scoped entry and its host falls back to the
host-wide one, exactly as if the row were absent. -
Git host entries can be verified against the forge. A stored token
was a claim nobody checked, and the forges hide the mistake: a GitHub
fine-grained token keeps "which repositories" and "which permissions"
in separate sections of one form, with permissions defaulting to
none — so granting all repositories and stopping there produces a
token that authenticates perfectly and cannot read a single repo. Git
then reportsWrite access to repository not grantedon a plain
fetch, naming the wrong permission on the wrong operation. Verify
now asks the forge who the token belongs to, warns when that differs
from the entry's owner, and optionally checks a specific repo — with a
hint that says where to look.
Added
- Markdown in the Vault is syntax-highlighted while you edit it. The
file viewer has always coloured what it shows, so raw markdown in the
Vault — the one place people actually read and write it — was the last
flat grey surface. Web layers a highlighted backdrop under the
textarea; mobile colours the field directly through its editing
controller, so the caret can't drift from the glyphs. Headings, bold,
italic, code, links, quotes, lists, tags and[[wiki-links]].
Fixed
- The Vault no longer describes itself as an Obsidian feature. It
syncs through a plain git remote — Obsidian is merely one editor that
can be pointed at the same repo, not something opendray integrates
with. The user-facing wording was corrected in the previous release;
this clears the same claim from the code that outlives it.
Added
- New docs start from a template, and folders can explain themselves.
Every doc previously started as an empty file with a heading, which is
how a vault ends up with five different ideas of what a feature note
is. Creating a doc now offers Blank / Feature / Decision (ADR) /
Runbook, and a folder holding aREADME.mdgets a control that opens
it, so a directory can say what lives in it. Templates render
server-side — the title comes from the filename, the date from the
clock — so a doc started on the phone and one started on the web come
out identical rather than drifting. Dropping_templates/<id>.mdin
the vault overrides a built-in or adds a new one, so a project can
change the shape of its docs without a gateway release.
Added
- The Vault can hold a folder structure you actually maintain. Project
docs were a flat list: the "New doc" box replaced/with-, so
features/canvas.mdbecamefeatures-canvas.mdand a folder could not
be created from the UI at all — while the backend had stored nested
paths the whole time and the Notes page already rendered them as a tree.
The project-docs lane now renders that tree (rooted at the project, with
a Recent toggle for "the one I just edited"), typing a path with slashes
files a doc in a folder, and a new move/rename repoints the
[[wiki-links]]that pointed at the old path — without that, filing a
doc away silently stranded every reference to it, which is why nobody
reorganised. Web and mobile both; the rewrite skips code blocks, so a
fenced example of the syntax is never edited.
Full history: CHANGELOG.md
Announce on X
opendray v2.13.2 shipped — It was built for a.
https://github.com/Opendray/opendray/releases/tag/v2.13.2