Changelog

The project should feel alive

Readable release notes from GitHub, designed for humans.

Latest release

v2.15.1

Sep 8, 2026

Fixed Grok sessions now scroll cleanly on desktop and mobile. Grok used to open in its fullscreen mode, which has no scrollback and ignores the mouse wheel, so scrolling was erratic on desktop and impossible on phones and tablets.

opendray update
View release
v2.15.1 Current

, 2026-09-08

Fixed Grok sessions now scroll cleanly on desktop and mobile. Grok used to open in its fullscreen mode, which has no scrollback and ignores the mouse wheel, so scrolling was erratic on desktop and impossible on phones and tablets.

Fixed

  • Grok sessions now scroll cleanly on desktop and mobile. Grok used to open in its fullscreen mode, which has no scrollback and ignores the mouse wheel, so scrolling was erratic on desktop and impossible on phones and tablets. Grok now opens in its scrollback native mode, so the wheel and finger swipe scroll the conversation the same way they do for Claude. (#546)
Open on GitHub
v2.15.0 Stable

, 2026-09-05

Added Grok account switching can now carry the conversation across the switch, matching Claude.

Added

  • Grok account switching can now carry the conversation across the switch, matching Claude. Switching a running grok session to another account with "carry over conversation context" turned on seeds the new account's fresh session with a recap of your recent conversation. It is opt in and consent gated: the recap is sent to the provider under the new account, which the confirm dialog states before you switch. (#542, #543, #544)
Open on GitHub
v2.14.1 Patch

, 2026-09-04

Added Grok accounts get a full UI: a Grok accounts panel in the Providers page (list, import local, enable or disable, remove) and live account switching from the session header, matching Claude and Antigravity.

Added

  • Grok accounts get a full UI: a Grok accounts panel in the Providers page (list, import local, enable or disable, remove) and live account switching from the session header, matching Claude and Antigravity. This surfaces the grok multi-account backend that shipped in v2.14.0. (#539)
  • Opt-in git worktree isolation, so concurrent sessions can each work in their own worktree without stepping on one another. (#531)
  • Mobile can reach a gateway behind Cloudflare Access without a VPN. (#538)

Improved

  • Documentation rewritten in plain, human prose with no em dashes, the README simplified so it reads top to bottom in one pass, and the changelog brought up to date. (#537, #528, #536)
View details

Added

  • Grok accounts get a full UI: a Grok accounts panel in the Providers page (list, import local, enable or disable, remove) and live account switching from the session header, matching Claude and Antigravity. This surfaces the grok multi-account backend that shipped in v2.14.0. (#539)
  • Opt-in git worktree isolation, so concurrent sessions can each work in their own worktree without stepping on one another. (#531)
  • Mobile can reach a gateway behind Cloudflare Access without a VPN. (#538)

Changed

  • Documentation rewritten in plain, human prose with no em dashes, the README simplified so it reads top to bottom in one pass, and the changelog brought up to date. (#537, #528, #536)
Open on GitHub
v2.14.0 Stable

, 2026-08-31

Grok catches up. You can pool several xAI accounts, bind one to a session, and switch a running grok session from one account to another without losing the session , the multi account parity Claude and Antigravity already had.

Added

  • Grok multi-account: pool accounts, pick one per session, and live-switch a running session between them (PATCH /sessions/{id}/grok-account) without dropping it. Each account is an isolated GROK_HOME; MCP trust is per-account and the heavy install/cache dirs are shared by symlink. (#533, #534, #535)
  • Cortex spawn injections (memory guidance, ambient memory, skills) now reach grok too, coalesced into grok's single --rules. (#530)
View details

Grok catches up. You can pool several xAI accounts, bind one to a session, and switch a running grok session from one account to another without losing the session, the multi-account parity Claude and Antigravity already had. Each account is an isolated GROK_HOME; the heavy install and cache directories are shared across accounts by symlink, so a second account costs kilobytes, not gigabytes.

Added

  • Grok multi-account: pool accounts, pick one per session, and live-switch a running session between them (PATCH /sessions/{id}/grok-account) without dropping it. Each account is an isolated GROK_HOME; MCP trust is per-account and the heavy install/cache dirs are shared by symlink. (#533, #534, #535)
  • Cortex spawn injections (memory guidance, ambient memory, skills) now reach grok too, coalesced into grok's single --rules. (#530)
Open on GitHub
v2.13.3 Patch

, 2026-08-22

The one line installer runs to the end again, as root, on a fresh Proxmox LXC or VPS, which is exactly how it is usually run.

Added

  • Cortex knowledge lifecycle: durable operator edits, polarity, deletion-as-signal, and conflict execution. (#521)
  • Hand a knowledge page to in-session agents (#513); review proposals as a diff instead of two full documents (#515); let the sweep honour a page's approval gate (#516).
  • Tasks grouped by project in the management views. (#520)
  • Git: an "Update branch" action so a stale PR can be merged from opendray. (#518)

Fixed

  • Installer: root installs never completed: the Postgres readiness gate always failed as root (run_priv -u postgres ran a literal -u), and migrate ran with HOME=/root so its backup keyfile was unreadable. The one-liner now runs to the end. (#529)
  • Mobile: a knowledge proposal is reviewable before you decide on it. (#519)
  • Cortex: doc_read's framing is no longer written into pages. (#517)
  • Catalog: compare resolved paths when detecting non-npm bin links. (#514)
View details

The one-line installer runs to the end again, as root, on a fresh Proxmox LXC or VPS, which is exactly how it is usually run. And Cortex, the knowledge layer, grows a lifecycle: operator edits stick, a page can be handed to an in-session agent, proposals are reviewed as a diff, and the sweep respects a page's approval gate.

Added

  • Cortex knowledge lifecycle: durable operator edits, polarity, deletion-as-signal, and conflict execution. (#521)
  • Hand a knowledge page to in-session agents (#513); review proposals as a diff instead of two full documents (#515); let the sweep honour a page's approval gate (#516).
  • Tasks grouped by project in the management views. (#520)
  • Git: an "Update branch" action so a stale PR can be merged from opendray. (#518)

Fixed

  • Installer: root installs never completed: the Postgres readiness gate always failed as root (run_priv -u postgres ran a literal -u), and migrate ran with HOME=/root so its backup keyfile was unreadable. The one-liner now runs to the end. (#529)
  • Mobile: a knowledge proposal is reviewable before you decide on it. (#519)
  • Cortex: doc_read's framing is no longer written into pages. (#517)
  • Catalog: compare resolved paths when detecting non-npm bin links. (#514)
Open on GitHub
v2.13.2 Patch

, 2026-08-11

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

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.

Improved

  • 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.
View details

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.
Open on GitHub
v2.13.1 Patch

, 2026-08-07

Your phone and your browser can reach the gateway again while its Mac sits idle. A machine that falls asleep takes its network with it, so the gateway simply stops answering, a failure that looks exactly like a flaky gat

Added

  • Host power is now a setting you can see, on web and mobile. The choice between "stay reachable" and "let the machine sleep" was only reachable by hand-editing config.toml, which meant nobody found it. Server settings gain a Host power section listing all four modes with what each one costs (web renders them as annotated cards, mobile as a labelled picker) plus a note that a deliberate sleep is never blocked and that non-macOS hosts ignore the setting. Translated across English, 中文 and Español.

  • Wake-on-demand: the host may sleep, and phone/web traffic wakes it. [host] prevent_idle_sleep = "on_demand" lets the gateway's Mac sleep whenever things are quiet instead of being held awake around the clock. Incoming traffic dark-wakes the machine (macOS "Wake for network access"); the gateway then seizes a power assertion the moment a request, live stream or mid-turn session shows up, extends the wake for as long as serving actually takes, and lets go after a two-minute linger so the host can sleep again. Running sessions count as activity, so a turn started from a phone survives the phone locking. On-demand warns at startup if womp is disabled, since without it a sleeping host can't be woken remotely at all. The existing "ac" (default), "always" and "off" behaviours are unchanged.

Fixed

  • Saving invalid server settings no longer bricks the next restart. PUT /admin/settings wrote whatever it was given. Since the loader validates on every startup, a bad value (an unparseable duration, an unknown mode) saved cleanly and then stopped the gateway from coming back up, with nothing naming the offending field. The write path now validates exactly what is about to hit disk and answers 400 instead, leaving the stored config untouched.

  • The gateway keeps its host awake, so phone and web stay connected. A Mac left to itself idle-sleeps, and a sleeping host takes its network with it: the gateway stops answering, a remote Postgres becomes unreachable, and every phone or web request times out until someone physically wakes the machine. Incoming traffic does dark-wake the host, but the window is short and a user-session LaunchAgent isn't reliably scheduled inside it, so the request has already timed out, which reads as "opendray is flaky" and sends people reading gateway code instead of pmset -g log. opendray now holds a power assertion for as long as it serves. Default is wall-power only, so a laptop on battery still sleeps normally; [host] prevent_idle_sleep takes "always" or "off". A deliberate sleep (lid close, Apple menu → Sleep) is never blocked, and the assertion is tied to the gateway's pid so even a SIGKILL can't strand the host awake. macOS only; accepted and ignored elsewhere.

View details

Your phone and your browser can reach the gateway again while its Mac sits idle. A machine that falls asleep takes its network with it, so the gateway simply stops answering, a failure that looks exactly like a flaky gateway from every surface that talks to it, and sends people reading gateway code instead of pmset -g log. opendray now holds the host awake while it serves, and lets you choose how far to take that.

Added

  • Host power is now a setting you can see, on web and mobile. The choice between "stay reachable" and "let the machine sleep" was only reachable by hand-editing config.toml, which meant nobody found it. Server settings gain a Host power section listing all four modes with what each one costs (web renders them as annotated cards, mobile as a labelled picker) plus a note that a deliberate sleep is never blocked and that non-macOS hosts ignore the setting. Translated across English, 中文 and Español.

  • Wake-on-demand: the host may sleep, and phone/web traffic wakes it. [host] prevent_idle_sleep = "on_demand" lets the gateway's Mac sleep whenever things are quiet instead of being held awake around the clock. Incoming traffic dark-wakes the machine (macOS "Wake for network access"); the gateway then seizes a power assertion the moment a request, live stream or mid-turn session shows up, extends the wake for as long as serving actually takes, and lets go after a two-minute linger so the host can sleep again. Running sessions count as activity, so a turn started from a phone survives the phone locking. On-demand warns at startup if womp is disabled, since without it a sleeping host can't be woken remotely at all. The existing "ac" (default), "always" and "off" behaviours are unchanged.

Fixed

  • Saving invalid server settings no longer bricks the next restart. PUT /admin/settings wrote whatever it was given. Since the loader validates on every startup, a bad value (an unparseable duration, an unknown mode) saved cleanly and then stopped the gateway from coming back up, with nothing naming the offending field. The write path now validates exactly what is about to hit disk and answers 400 instead, leaving the stored config untouched.

  • The gateway keeps its host awake, so phone and web stay connected. A Mac left to itself idle-sleeps, and a sleeping host takes its network with it: the gateway stops answering, a remote Postgres becomes unreachable, and every phone or web request times out until someone physically wakes the machine. Incoming traffic does dark-wake the host, but the window is short and a user-session LaunchAgent isn't reliably scheduled inside it, so the request has already timed out, which reads as "opendray is flaky" and sends people reading gateway code instead of pmset -g log. opendray now holds a power assertion for as long as it serves. Default is wall-power only, so a laptop on battery still sleeps normally; [host] prevent_idle_sleep takes "always" or "off". A deliberate sleep (lid close, Apple menu → Sleep) is never blocked, and the assertion is tied to the gateway's pid so even a SIGKILL can't strand the host awake. macOS only; accepted and ignored elsewhere.

Open on GitHub
v2.13.0 Stable

, 2026-08-07

The Canvas: the agent renders a page you can actually see, mark up and iterate on, instead of describing a screen in prose and hoping it matches.

Added

  • Canvas: a visual surface for designing with the agent. The agent renders a self-contained HTML page via canvas_render and it appears in the operator's panel, where they pin a point or drag a region and send those marks back as feedback. Marking reports the real element selector and markup (and for a region, the components inside the frame) so the agent iterates on the DOM instead of guessing from coordinates. Not only UI mocks: ui, flow (flowchart), mindmap, graph (relationship diagram) and doc (a formatted spec page), with diagrams authored as inline SVG so they need no external assets and stay markable node by node.
  • A focused canvas per project. A project accumulates many canvases, so one is FOCUSED per cwd, scoped to the project rather than the session because the MCP server only ever receives the cwd. Browsing the list is free and costs nothing; only an explicit "Work on this" seeds the note that makes ordinary conversation ("make the title bigger") resolve to that canvas. Agents pull the same fact with canvas_context, and a slug-less canvas_render targets the focused canvas rather than creating a duplicate.
  • A design system per project. Colour / type / radius / spacing tokens plus free-text style rules, carried in every canvas request AND injected into each rendered document as CSS variables. The pairing is what stops successive renders from drifting apart. Ships starting palettes for people who don't think in hex, a real colour picker, and two one-click jobs: read the project's actual theme, or draw the system as a canvas.
  • Full mobile parity for all of the above: viewer, viewport switching, focus and precise marking from the phone.
  • opendray status is now a systemctl-style health dashboard. On macOS the command used to dump ~80 lines of raw launchctl print internals. A live process says nothing about whether the gateway is serving HTTP or can reach its database. The default output is now a compact checklist: process, HTTP health + uptime, database reachability, listen address, restart count (with the last non-zero exit surfaced as a hint) and the config path. The raw launchd/systemd dump moved behind --raw.

Improved

  • primary in a design system means the BRAND colour, and the gateway now says so. shadcn/ui and the Tailwind templates built on it use --primary for a near-black or near-white ink and keep the brand hue in --accent, so an agent mapping token names one-to-one produced a palette of greys with no brand colour in it. The contract is now stated wherever an agent reads it (the extract prompt, the canvas_design tool schema, the token docs and the catalog guidance) and saving a palette whose every colour resolves to a grey returns an advisory warning saying what probably went wrong.

Fixed

  • Design-system swatches show the real colour, in any notation. Reading a colour back from getComputedStyle().color only yields rgb(…) for legacy sRGB notations (a modern colour function round-trips unchanged in both WebKit and Chromium) so an all-oklch project got a grid of identical grey fallbacks on the web, and mobile, which parsed nothing but #rrggbb, got no swatches at all. The gateway now resolves hex / rgb / hsl / oklch / oklab centrally and serves the result, which also lets the hex-only OS colour picker edit an oklch theme without converting it one field at a time.
  • Pin and region marks stay on the content they were placed on. The web panel recorded a mark as a percentage of the visible frame and drew it in an overlay on top of the preview, so it was anchored to the window: scroll the canvas and the mark stayed put while the content moved out from under it, and a mark made after scrolling was stored pointing somewhere else entirely. Marks are now recorded in document percentages and drawn into the canvas document itself. This also settles a mismatch where the same x/y field meant frame percentages from the web and document percentages from mobile.
  • The mouse wheel (and touch swipe) now scrolls grok and opencode sessions. These TUIs enable mouse tracking (so xterm hands them the wheel instead of scrolling its own viewport) but then ignore wheel events, scrolling only on arrow keys. opendray's wheel→arrow fallback was gated behind "app hasn't grabbed the mouse", so it never ran for them and the wheel did nothing (verified live: grok sets ?1000/1002/1003/1006 at startup yet does nothing on button-64/65). The web terminal now recognises these wheel-ignoring providers and sends arrow keys for both wheel and one-finger touch scroll, scoped by providerId so Claude/Codex/Antigravity (which genuinely consume the wheel) are untouched.
  • Coming back to the mobile app after it slept no longer fails session loads. While the app is suspended the OS silently tears down its TCP connections, but the HTTP client's pool didn't know. The next request went out on a dead socket and stalled into a 30-second timeout and a full-screen error, even on a LAN. Connection-level failures on GET/HEAD are now replayed up to twice with backoff (a 4xx/5xx still passes through untouched), the pool's idle timeout drops to 5 seconds so stale sockets are rarely handed out at all, and returning from a suspension of 5+ seconds refetches the session list immediately instead of waiting for the next tap.

Security

  • Hardened the path-containment barrier behind /fs/download, /fs/zip and /fs/upload. The root-scoped filesystem endpoints now validate the resolved path with filepath.IsLocal and refuse any residual .. sequence in the canonical path (which also means oddly-named entries like notes..md are rejected inside these endpoints). Resolves all seven open CodeQL go/path-injection alerts plus one allocation-size-overflow finding.
  • Bumped vulnerable transitive npm dependencies in the web workspace lockfile: seroval 1.6.2 (critical GHSA-mv8w-475r-vwqw), brace-expansion 5.0.9 (three DoS advisories), postcss 8.5.26 (path-traversal advisories) and @babel/core 7.29.7. Lockfile-only; no manifest changes.
View details

The Canvas: the agent renders a page you can actually see, mark up and iterate on, instead of describing a screen in prose and hoping it matches.

Added

  • Canvas: a visual surface for designing with the agent. The agent renders a self-contained HTML page via canvas_render and it appears in the operator's panel, where they pin a point or drag a region and send those marks back as feedback. Marking reports the real element selector and markup (and for a region, the components inside the frame) so the agent iterates on the DOM instead of guessing from coordinates. Not only UI mocks: ui, flow (flowchart), mindmap, graph (relationship diagram) and doc (a formatted spec page), with diagrams authored as inline SVG so they need no external assets and stay markable node by node.
  • A focused canvas per project. A project accumulates many canvases, so one is FOCUSED per cwd, scoped to the project rather than the session because the MCP server only ever receives the cwd. Browsing the list is free and costs nothing; only an explicit "Work on this" seeds the note that makes ordinary conversation ("make the title bigger") resolve to that canvas. Agents pull the same fact with canvas_context, and a slug-less canvas_render targets the focused canvas rather than creating a duplicate.
  • A design system per project. Colour / type / radius / spacing tokens plus free-text style rules, carried in every canvas request AND injected into each rendered document as CSS variables. The pairing is what stops successive renders from drifting apart. Ships starting palettes for people who don't think in hex, a real colour picker, and two one-click jobs: read the project's actual theme, or draw the system as a canvas.
  • Full mobile parity for all of the above: viewer, viewport switching, focus and precise marking from the phone.
  • opendray status is now a systemctl-style health dashboard. On macOS the command used to dump ~80 lines of raw launchctl print internals. A live process says nothing about whether the gateway is serving HTTP or can reach its database. The default output is now a compact checklist: process, HTTP health + uptime, database reachability, listen address, restart count (with the last non-zero exit surfaced as a hint) and the config path. The raw launchd/systemd dump moved behind --raw.

Changed

  • primary in a design system means the BRAND colour, and the gateway now says so. shadcn/ui and the Tailwind templates built on it use --primary for a near-black or near-white ink and keep the brand hue in --accent, so an agent mapping token names one-to-one produced a palette of greys with no brand colour in it. The contract is now stated wherever an agent reads it (the extract prompt, the canvas_design tool schema, the token docs and the catalog guidance) and saving a palette whose every colour resolves to a grey returns an advisory warning saying what probably went wrong.

Fixed

  • Design-system swatches show the real colour, in any notation. Reading a colour back from getComputedStyle().color only yields rgb(…) for legacy sRGB notations (a modern colour function round-trips unchanged in both WebKit and Chromium) so an all-oklch project got a grid of identical grey fallbacks on the web, and mobile, which parsed nothing but #rrggbb, got no swatches at all. The gateway now resolves hex / rgb / hsl / oklch / oklab centrally and serves the result, which also lets the hex-only OS colour picker edit an oklch theme without converting it one field at a time.
  • Pin and region marks stay on the content they were placed on. The web panel recorded a mark as a percentage of the visible frame and drew it in an overlay on top of the preview, so it was anchored to the window: scroll the canvas and the mark stayed put while the content moved out from under it, and a mark made after scrolling was stored pointing somewhere else entirely. Marks are now recorded in document percentages and drawn into the canvas document itself. This also settles a mismatch where the same x/y field meant frame percentages from the web and document percentages from mobile.
  • The mouse wheel (and touch swipe) now scrolls grok and opencode sessions. These TUIs enable mouse tracking (so xterm hands them the wheel instead of scrolling its own viewport) but then ignore wheel events, scrolling only on arrow keys. opendray's wheel→arrow fallback was gated behind "app hasn't grabbed the mouse", so it never ran for them and the wheel did nothing (verified live: grok sets ?1000/1002/1003/1006 at startup yet does nothing on button-64/65). The web terminal now recognises these wheel-ignoring providers and sends arrow keys for both wheel and one-finger touch scroll, scoped by providerId so Claude/Codex/Antigravity (which genuinely consume the wheel) are untouched.
  • Coming back to the mobile app after it slept no longer fails session loads. While the app is suspended the OS silently tears down its TCP connections, but the HTTP client's pool didn't know. The next request went out on a dead socket and stalled into a 30-second timeout and a full-screen error, even on a LAN. Connection-level failures on GET/HEAD are now replayed up to twice with backoff (a 4xx/5xx still passes through untouched), the pool's idle timeout drops to 5 seconds so stale sockets are rarely handed out at all, and returning from a suspension of 5+ seconds refetches the session list immediately instead of waiting for the next tap.

Security

  • Hardened the path-containment barrier behind /fs/download, /fs/zip and /fs/upload. The root-scoped filesystem endpoints now validate the resolved path with filepath.IsLocal and refuse any residual .. sequence in the canonical path (which also means oddly-named entries like notes..md are rejected inside these endpoints). Resolves all seven open CodeQL go/path-injection alerts plus one allocation-size-overflow finding.
  • Bumped vulnerable transitive npm dependencies in the web workspace lockfile: seroval 1.6.2 (critical GHSA-mv8w-475r-vwqw), brace-expansion 5.0.9 (three DoS advisories), postcss 8.5.26 (path-traversal advisories) and @babel/core 7.29.7. Lockfile-only; no manifest changes.
Open on GitHub
v2.12.2 Patch

, 2026-07-23

Round Table members gain live, grounded memory, the mobile session screen is rebuilt around a tool dock, and the operator takes ownership of the four global knowledge pages so their curation finally sticks.

Added

  • Round Table members read the shared memory. Every seated provider now gets live, read-only access to the shared opendray-memory MCP, so members ground their claims in the real store instead of guessing. A new read-only mode (OPENDRAY_MEMORY_READONLY=1) exposes only the search / read tools and refuses every write server-side (safe even with tool permissions open) and the table prompt flips to "ground your claims via these tools". A single per-provider attach point (catalog.AttachMemoryMCP) handles the antigravity / codex quirks and is reusable for any spawn.
  • Operator owns the form of the four global KB pages. The shape of kb_infrastructure / kb_conventions / kb_lessons / kb_reusable was hardcoded in the drafter's prompts, so every consolidation sweep re-manufactured the fixed fat skeleton and operator curation never stuck. The drafter now honours each page's blueprint maintainer_mode (human → the operator owns it outright, never drafted) and, for AI-maintained pages, edits the operator's current structure in place (folding in only new, on-topic evidence) instead of regenerating from a template. A per-page prompt_hint lets the operator steer form and scope without a code change. Set both via PUT /blueprint/{slug} (cwd __global__); a web/mobile toggle is a follow-up. No migration. The four pages default to the previous behaviour.
  • Mobile: session tool dock. The session detail screen is rebuilt so the terminal owns the full height under a two-line AppBar title, and a new SessionToolDock (Files · Git · Database · Vault · More) opens each tool as a bottom sheet over the live terminal. The overflow ⋮ menu is slimmed to session actions.

Improved

  • Mobile: More / settings redesigned. The flat menu is rebuilt as grouped inset cards per section, with an account-block identity header (monogram avatar), accent-tinted icon chips, refined typography and a version footer. New sessions.dock.more / sessions.tools.* strings ship across en / zh / es at 100% parity.

Fixed

  • A human-locked global KB page could never converge from the UI. Two bugs are fixed: approving a divergence proposal silently dropped the lock (the merge was written as updated_by='agent', flipping HumanLocked off). An approval is an operator decision, so the result now stays operator-authored and locked; and rejecting a proposal didn't stick: the drafter re-generated and re-filed the identical refresh every consolidation cycle. The drafter now skips a locked page whose feedstock signature matches an already-rejected proposal, so a rejection holds instead of nagging forever.

Docs

  • The README surfaces the two marquee capabilities shipped since the last pass: Round Table (cross-vendor AI group chat + role-assigned execution plans) and the Database tool (Postgres / MySQL / MariaDB / SQLite, per-project crypto isolation, web + mobile), plus staged image attachments, TUI theme-following / wheel-scroll, and one-click provider updates.
View details

Round Table members gain live, grounded memory, the mobile session screen is rebuilt around a tool dock, and the operator takes ownership of the four global knowledge pages so their curation finally sticks.

Added

  • Round Table members read the shared memory. Every seated provider now gets live, read-only access to the shared opendray-memory MCP, so members ground their claims in the real store instead of guessing. A new read-only mode (OPENDRAY_MEMORY_READONLY=1) exposes only the search / read tools and refuses every write server-side (safe even with tool permissions open) and the table prompt flips to "ground your claims via these tools". A single per-provider attach point (catalog.AttachMemoryMCP) handles the antigravity / codex quirks and is reusable for any spawn.
  • Operator owns the form of the four global KB pages. The shape of kb_infrastructure / kb_conventions / kb_lessons / kb_reusable was hardcoded in the drafter's prompts, so every consolidation sweep re-manufactured the fixed fat skeleton and operator curation never stuck. The drafter now honours each page's blueprint maintainer_mode (human → the operator owns it outright, never drafted) and, for AI-maintained pages, edits the operator's current structure in place (folding in only new, on-topic evidence) instead of regenerating from a template. A per-page prompt_hint lets the operator steer form and scope without a code change. Set both via PUT /blueprint/{slug} (cwd __global__); a web/mobile toggle is a follow-up. No migration. The four pages default to the previous behaviour.
  • Mobile: session tool dock. The session detail screen is rebuilt so the terminal owns the full height under a two-line AppBar title, and a new SessionToolDock (Files · Git · Database · Vault · More) opens each tool as a bottom sheet over the live terminal. The overflow ⋮ menu is slimmed to session actions.

Changed

  • Mobile: More / settings redesigned. The flat menu is rebuilt as grouped inset cards per section, with an account-block identity header (monogram avatar), accent-tinted icon chips, refined typography and a version footer. New sessions.dock.more / sessions.tools.* strings ship across en / zh / es at 100% parity.

Fixed

  • A human-locked global KB page could never converge from the UI. Two bugs are fixed: approving a divergence proposal silently dropped the lock (the merge was written as updated_by='agent', flipping HumanLocked off). An approval is an operator decision, so the result now stays operator-authored and locked; and rejecting a proposal didn't stick: the drafter re-generated and re-filed the identical refresh every consolidation cycle. The drafter now skips a locked page whose feedstock signature matches an already-rejected proposal, so a rejection holds instead of nagging forever.

Docs

  • The README surfaces the two marquee capabilities shipped since the last pass: Round Table (cross-vendor AI group chat + role-assigned execution plans) and the Database tool (Postgres / MySQL / MariaDB / SQLite, per-project crypto isolation, web + mobile), plus staged image attachments, TUI theme-following / wheel-scroll, and one-click provider updates.
Open on GitHub
v2.12.1 Patch

, 2026-07-18

Grok reaches parity with the other cloud agents, the Cortex knowledge base grows a real management surface, and the mobile knowledge experience is rebuilt for phones.

Added

  • Grok is now a first-class cloud agent. It can drive the shared memory MCP (memory search, doc_read, cross-layer recall) just like Claude / Codex / Antigravity. Its spawn folder is marked trusted so grok actually starts the injected memory server instead of silently skipping it. Grok and OpenCode are also selectable in Discuss with AI and as Memory Worker agent providers, and creating a grok session now offers the Bypass permissions / YOLO toggle (--always-approve) the other agents already had.
  • A cross-page KB Librarian (experimental). Launch a dedicated agent session (pick its cloud agent, model and account) that can organize, create, edit and delete any global knowledge page across the whole base, driven conversationally, unlike the per-page Discuss chat. It gets read + write KB tools (list / upsert config / write body / delete) on its memory MCP; those tools are scoped to the Librarian session alone and never reach ordinary or third-party sessions.
  • Edit a knowledge page's settings after creation. A kb_* page's title, one-line description, nature (foundational / emergent) and inject flag were locked in at creation; they are now editable in place (web + mobile) on every page except the classic four, including seeded pages like Integrations, so you can flip a page between full-inject and on-demand retrieval.
  • Discuss with AI model lists are live and accurate. Antigravity and OpenCode models are enumerated straight from their CLIs, and Codex offers its full model family (higher plans unlock the fuller models) instead of one pinned choice, no more picking a stale model that fails at spawn.
  • Round Table members can change mid-conversation. Add or remove seated providers on an active chat (web + mobile): an added member is @mentionable on the next turn with the full thread as context; a removed one stops replying while its past messages stay.
  • Mobile: staged image uploads. Images queue in a dismissable tray before send instead of uploading immediately.

Improved

  • The mobile Knowledge (KB) page is rebuilt as a searchable list → detail flow. The old horizontal page-chip strip didn't scale once you had many kb_* docs; the KB tab is now a grouped, searchable list (Foundational / Emergent) that grows gracefully, and tapping a page opens a full-screen reader/editor with its actions in an AppBar overflow menu. New page and the Librarian move onto a FAB.

Fixed

  • Grok sessions had no MCP / memory tools. opendray wrote the memory server into the project-scoped <cwd>/.grok/config.toml, but grok refuses to start repo-local MCP servers in an untrusted folder as a supply-chain guard, so the server was configured but never started. opendray now trusts the operator's own spawn folder (--trust), matching the other CLIs.
  • The web terminal input cursor no longer drifts on iPad.
View details

Grok reaches parity with the other cloud agents, the Cortex knowledge base grows a real management surface, and the mobile knowledge experience is rebuilt for phones.

Added

  • Grok is now a first-class cloud agent. It can drive the shared memory MCP (memory search, doc_read, cross-layer recall) just like Claude / Codex / Antigravity. Its spawn folder is marked trusted so grok actually starts the injected memory server instead of silently skipping it. Grok and OpenCode are also selectable in Discuss with AI and as Memory Worker agent providers, and creating a grok session now offers the Bypass permissions / YOLO toggle (--always-approve) the other agents already had.
  • A cross-page KB Librarian (experimental). Launch a dedicated agent session (pick its cloud agent, model and account) that can organize, create, edit and delete any global knowledge page across the whole base, driven conversationally, unlike the per-page Discuss chat. It gets read + write KB tools (list / upsert config / write body / delete) on its memory MCP; those tools are scoped to the Librarian session alone and never reach ordinary or third-party sessions.
  • Edit a knowledge page's settings after creation. A kb_* page's title, one-line description, nature (foundational / emergent) and inject flag were locked in at creation; they are now editable in place (web + mobile) on every page except the classic four, including seeded pages like Integrations, so you can flip a page between full-inject and on-demand retrieval.
  • Discuss with AI model lists are live and accurate. Antigravity and OpenCode models are enumerated straight from their CLIs, and Codex offers its full model family (higher plans unlock the fuller models) instead of one pinned choice, no more picking a stale model that fails at spawn.
  • Round Table members can change mid-conversation. Add or remove seated providers on an active chat (web + mobile): an added member is @mentionable on the next turn with the full thread as context; a removed one stops replying while its past messages stay.
  • Mobile: staged image uploads. Images queue in a dismissable tray before send instead of uploading immediately.

Changed

  • The mobile Knowledge (KB) page is rebuilt as a searchable list → detail flow. The old horizontal page-chip strip didn't scale once you had many kb_* docs; the KB tab is now a grouped, searchable list (Foundational / Emergent) that grows gracefully, and tapping a page opens a full-screen reader/editor with its actions in an AppBar overflow menu. New page and the Librarian move onto a FAB.

Fixed

  • Grok sessions had no MCP / memory tools. opendray wrote the memory server into the project-scoped <cwd>/.grok/config.toml, but grok refuses to start repo-local MCP servers in an untrusted folder as a supply-chain guard, so the server was configured but never started. opendray now trusts the operator's own spawn folder (--trust), matching the other CLIs.
  • The web terminal input cursor no longer drifts on iPad.
Open on GitHub
v2.12.0 Stable

, 2026-07-16

Added Round Table: a cross vendor AI group chat (experimental). Seat several providers (Claude / Codex / Antigravity / Grok / OpenCode) plus the operator in one shared thread; @mention who should reply (or @all) and each

Added

  • Round Table: a cross-vendor AI group chat (experimental). Seat several providers (Claude / Codex / Antigravity / Grok / OpenCode) plus the operator in one shared thread; @mention who should reply (or @all) and each member answers in character after reading the whole conversation, so heterogeneous foundation-model families react to each other in seat order. Summarize the discussion on demand, or turn it into a role-assigned execution plan: each step runs as a real session in a shared project (bind the project after the fact if you started without one). Hand the whole thread off to a working session to do the actual code changes. A chat can be closed and reopened (close keeps the thread, just stops new messages). Available on both the web admin and the mobile app, where Round Table gets its own bottom-nav tab, per-agent bubble colours, and labelled action menus. Fully self-contained and rollback-able (internal/roundtable/ROLLBACK.md).

Fixed

  • The Files-tree download icon is now reachable on touch devices (iPad, phones). The per-row download button was revealed only on hover (group-hover) or keyboard focus. Tailwind v4 gates group-hover behind @media (hover: hover), so on a touch device (which can neither hover nor focus a row) the icon stayed at opacity-0 and was impossible to tap. It now pins visible under @media (hover: none), so touch users get a permanently-shown download control while pointer users keep the clean hover-reveal. (Follow-up to the v2.11.6 positioning fix, which addressed where the icon sits but not whether it ever appears without a mouse.)
  • Two MCP servers sharing a display name no longer brick Codex sessions. Every provider renderer keys its generated config on a server's display name, not its unique id. Two enabled servers with the same name therefore collided on that key: Codex emitted a duplicate [mcp_servers."…"] TOML table and died with duplicate key at startup (before printing a byte, so the session flipped straight to the read-only "[buffer unavailable]" view) while Claude's map-based renderer silently dropped one of them. renderMCP now rejects a duplicate name up front (for every provider, before any config file is written), and the Plugins create/update endpoints return 409 when a new or edited server would reuse a name already taken by a different id. Grok's manifest gap and this collision are unrelated; a stray second Notion entry sharing the name Notion API is what exposed it.
  • Grok now reports and applies CLI updates from the Providers page. The grok manifest carried an empty npmPackage, and the whole update path is npm-gated: CheckUpdate returned early (no latest version, no "update available" flag) and Update hard-errored with "not updatable via npm". Grok is published as @xai-official/grok (maintainer [email protected]), so the manifest now names it.
  • A provider CLI installed outside npm can now be updated in place. Grok's documented installer (curl -fsSL https://x.ai/cli/install.sh | bash) drops a symlink into the npm bin dir that npm does not own, and npm refuses to clobber it: EEXIST: file already exists. Simply naming the package would therefore have shipped a dashboard that advertises an update behind a button that always fails. Update now preflights the bin path: an unmanaged symlink is cleared so npm can take ownership (and the update output tells the operator exactly which link was replaced), while a regular file is never deleted: it is reported instead, mirroring the existing ErrUpdatePrefixReadonly preflight. Grok's install note now recommends npm install -g @xai-official/grok.
View details

Added

  • Round Table: a cross-vendor AI group chat (experimental). Seat several providers (Claude / Codex / Antigravity / Grok / OpenCode) plus the operator in one shared thread; @mention who should reply (or @all) and each member answers in character after reading the whole conversation, so heterogeneous foundation-model families react to each other in seat order. Summarize the discussion on demand, or turn it into a role-assigned execution plan: each step runs as a real session in a shared project (bind the project after the fact if you started without one). Hand the whole thread off to a working session to do the actual code changes. A chat can be closed and reopened (close keeps the thread, just stops new messages). Available on both the web admin and the mobile app, where Round Table gets its own bottom-nav tab, per-agent bubble colours, and labelled action menus. Fully self-contained and rollback-able (internal/roundtable/ROLLBACK.md).

Fixed

  • The Files-tree download icon is now reachable on touch devices (iPad, phones). The per-row download button was revealed only on hover (group-hover) or keyboard focus. Tailwind v4 gates group-hover behind @media (hover: hover), so on a touch device (which can neither hover nor focus a row) the icon stayed at opacity-0 and was impossible to tap. It now pins visible under @media (hover: none), so touch users get a permanently-shown download control while pointer users keep the clean hover-reveal. (Follow-up to the v2.11.6 positioning fix, which addressed where the icon sits but not whether it ever appears without a mouse.)
  • Two MCP servers sharing a display name no longer brick Codex sessions. Every provider renderer keys its generated config on a server's display name, not its unique id. Two enabled servers with the same name therefore collided on that key: Codex emitted a duplicate [mcp_servers."…"] TOML table and died with duplicate key at startup (before printing a byte, so the session flipped straight to the read-only "[buffer unavailable]" view) while Claude's map-based renderer silently dropped one of them. renderMCP now rejects a duplicate name up front (for every provider, before any config file is written), and the Plugins create/update endpoints return 409 when a new or edited server would reuse a name already taken by a different id. Grok's manifest gap and this collision are unrelated; a stray second Notion entry sharing the name Notion API is what exposed it.
  • Grok now reports and applies CLI updates from the Providers page. The grok manifest carried an empty npmPackage, and the whole update path is npm-gated: CheckUpdate returned early (no latest version, no "update available" flag) and Update hard-errored with "not updatable via npm". Grok is published as @xai-official/grok (maintainer [email protected]), so the manifest now names it.
  • A provider CLI installed outside npm can now be updated in place. Grok's documented installer (curl -fsSL https://x.ai/cli/install.sh | bash) drops a symlink into the npm bin dir that npm does not own, and npm refuses to clobber it: EEXIST: file already exists. Simply naming the package would therefore have shipped a dashboard that advertises an update behind a button that always fails. Update now preflights the bin path: an unmanaged symlink is cleared so npm can take ownership (and the update output tells the operator exactly which link was replaced), while a regular file is never deleted: it is reported instead, mirroring the existing ErrUpdatePrefixReadonly preflight. Grok's install note now recommends npm install -g @xai-official/grok.
Open on GitHub
v2.11.6 Patch

, 2026-07-13

Fixed The download icon is reachable again in a deep or long file tree. The session inspector's Files tree renders inside a scroll area whose inner wrapper sizes to its content, so long filenames and deep nesting pushed

Fixed

  • The download icon is reachable again in a deep or long file tree. The session inspector's Files tree renders inside a scroll area whose inner wrapper sizes to its content, so long filenames and deep nesting pushed rows wider than the panel: names were hard-cut with no ellipsis, and the hover-download icon (anchored to each row's right edge) sat beyond the visible edge, so hovering a file appeared to do nothing. The tree is now constrained to the panel width, so names truncate with an ellipsis and the download icon sits at the visible right edge. The Database tab is unaffected (its grid scrolls in its own containers). (#443)
View details

Fixed

  • The download icon is reachable again in a deep or long file tree. The session inspector's Files tree renders inside a scroll area whose inner wrapper sizes to its content, so long filenames and deep nesting pushed rows wider than the panel: names were hard-cut with no ellipsis, and the hover-download icon (anchored to each row's right edge) sat beyond the visible edge, so hovering a file appeared to do nothing. The tree is now constrained to the panel width, so names truncate with an ellipsis and the download icon sits at the visible right edge. The Database tab is unaffected (its grid scrolls in its own containers). (#443)
Open on GitHub
NextUpcoming

Operator polish

Operator polish, reconnect stability, memory improvements, and clearer docs.