Skip to content

v2.13.2

Choose a tag to compare

@github-actions github-actions released this 10 Aug 21:34
· 25 commits to main since this release
10bed30

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 by git 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 笔记.md into --.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 /
    .htm are 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 as text/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.html on the phone produced guide.html.md. The
    new-document path appended .md unconditionally, so an HTML
    document could not be created from mobile at all.

  • Six hints rendered &lt;prefix&gt; as literal text instead of
    <prefix>.

  • A table-of-contents link no longer loses the document. In the web
    viewer, clicking an in-document #anchor navigated the frame away
    and rendered the opendray app inside the document view. A srcdoc
    frame's document URL is about:srcdoc while its base URL is
    inherited from the parent page, so href="#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 explicit target is respected, and only links leaving the
    document get target="_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: loadData makes the
    document's own URL and its base both about: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.
    A feat/** branch with an open PR matched both the push and
    pull_request triggers, and the concurrency group keys on the ref —
    which differs between the two events — so neither run cancelled the
    other. push is now main-only; pull_request already 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
    under projects/ — 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
    at projects/<name>/…, the human scratchpad at personal/<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's personal.md
    inside it. Existing vaults are not rearranged. The layout is
    decided once, at first start, and written into config.toml as
    [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 a notes/ 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 flatten does 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/info reports 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's notes project was already guessing wrong.

    Reserved names step aside rather than collide: a project called
    daily files under daily-docs, since daily/YYYY-MM-DD.md belongs
    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 deriving projects/<name> and
    personal/<name>.md against 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: a truncate element 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 notes accepts flags after the subcommand. Go's flag
    package stops parsing at the first non-flag argument, so notes flatten --apply left apply false, performed a dry run, and advised
    re-running with --apply — which is what had just been typed.
    notes list --prefix=x silently 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/ and mcp/ 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 a git clean -fd there would have
    deleted the gateway's skills.

    New installs get ~/.opendray/vault for documents, ~/.opendray/ skills and ~/.opendray/mcp beside 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.notes and
    vault.skills keep working; vault.root now means the documents
    directory, and [skills].root is the new spelling. Machinery
    directories that do sit inside the Vault are hidden from the doc
    library and added to an opendray-managed .gitignore block, so
    nothing new gets carried to your remote. Anything already committed
    needs git rm --cached — opendray will not rewrite your repo.

    Path resolution used to be reimplemented in four places (the gateway
    plus each of opendray 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 request git push makes, 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
    ran git reset --hard plus git clean -fd behind 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 an opendray-rescue/<timestamp> branch and
    stashes the working tree (--include-untracked, since clean -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 reaches github.com/<you>/… generally
    cannot reach github.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 = osxkeychain enabled, 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 reports Write access to repository not granted on 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 a README.md gets 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>.md in
    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.md became features-canvas.md and 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

Tweet this