Minor Changes
-
Git sync gets a rebuilt control panel and mode-independent manual actions.
The sync popover now carries a three-way mode selector — Manual, Auto (Pull only), Auto (Pull and Push) — plus manual Pull, Push, and Pull and Push buttons that behave identically in every mode: the mode only chooses what runs on a schedule. Pull never commits your in-progress work in any mode; Pull and Push runs the classic sync (commit, push, merge). The panel's status section lists which files a pull would bring in, which files a push would include, and which changed files sit outside OpenKnowledge's commit scope and would be skipped — refreshed by a read-only fetch when the panel opens. Settings and the enable dialogs use the same mode names.
How often the automatic modes run is now configurable per machine: Settings → Sync → Advanced adds Check for updates every and Push my edits every (30 seconds to 1 hour), stored as
autoSync.pullIntervalSecondsandautoSync.pushIntervalSecondsin the project-local config. When a pull is blocked because your uncommitted local changes overlap an incoming update, the popover now lists the blocked files under Changed here and on the remote with a Commit and sync button that commits exactly those files and resumes, plus a Resolve in terminal handoff (desktop app) for stashing or discarding by hand — OpenKnowledge never discards uncommitted work for you.Two fixes to what a pull does with files it cannot merge cleanly. When an incoming change collides with a local edit to a non-document artifact, Pull now keeps your version rather than failing the whole pull — the Pull verb's disposition is keep-mine, and previously a single dirty artifact overlapping an incoming change would abort everything else the pull had to deliver. Separately, sync's own shareable files under
.ok/(the ones it legitimately writes) are no longer refused by the symlink guard's state-directory check, which had been rejecting them by resolved path.Three smaller changes ride along. The panel's freshness line now reports each direction separately (
↓ 2m ago · ↑ 5m ago) instead of one "Updated" timestamp that only ever reflected whichever direction ran last. An Advanced settings link in the popover opens Settings → Sync with the Advanced section already expanded. And when a new worktree inherits its sync setting from the root project, the notice names the mode exactly as the Settings control labels it, rather than using retired wording. -
One file on disk can no longer be backed by two separate collaboration documents. Addressing a document by a name that kept its
.mdor.mdxextension used to open a second, independent room over the same file. Both rooms then wrote to that file with no merge and no conflict shown, so whichever saved last silently overwrote the other, and an affected editor tab restored the broken state on every launch. Extension-qualified names now collapse onto the document they actually name, at the one point a room is keyed, so every surface converges: wiki links, graph nodes, share links, skill references, the preview URL, and the HTTP document routes. When a.mdand an.mdxfile genuinely exist side by side under one name, the document API still reaches each of them: the extension-qualified name serves the file the bare name does not. The editor lists such a pair as a single entry, so only one half opens there, and that is unchanged. Tabs carrying the old shape are repaired when they are read, so an affected install fixes itself on upgrade.ok opennow prints the project it resolved. Before, a path that landed in a different project than you expected looked identical to one that landed where you meant, and the only way to tell was to runok psafterwards. When the resolved project sits inside another OpenKnowledge project, the output names both.--projectis honored wherever you put it. Previously it worked only when it came before the target and the target had no file extension. In every other shape it was silently dropped and the project was derived from the file instead. When an override cannot be honored the command now fails and says why, rather than quietly opening somewhere else.ok stopdeclines to stop a server that something is still connected to, naming how many clients are attached, and--forceoverrides it. This counts every kind of client, including agents, because stopping a server an agent is writing through loses work the same way. A server that cannot be reached is stopped without the check.ok deinitandok uninstalldo not gain a second prompt: they add a line to the plan they already show, noting that restarting will not bring those windows back.- A window whose server has stopped now offers to restart it from the file sidebar, which is the surface that reports the failure. The action already existed but only appeared in a transient toast and in the document error state, so it was easy to miss and the window read as permanently broken.
ok stoprecords what it stopped, and servers record why they exited. Neither left a durable trace before, so an external stop, an idle shutdown, and a crash were indistinguishable afterwards and none of them reached a diagnostic bundle. Log records also name the project actually resolved instead of a placeholder, andok stopreports its target rather than the directory you happened to be in.- A project nested inside another project is no longer indexed by its parent. The two used to share content and reconcile only through disk writes.
runStop, exported for programmatic use, is now async and refuses by default when clients are connected. Callers must await it, and passforce: truefor the previous unconditional behavior.
Patch Changes
-
ACP permission prompts with three or more options now stack vertically as full-width buttons — primary Allow up top, escalating grants and refusals below — instead of hiding the extras behind an overflow chevron. Prompts with one or two options keep the classic Deny-left / Allow-right row. Fixes the case where a 2-allow / 0-reject shape would silently drop its escalating grant, and always renders a refusal control (even when the agent offers none).
-
Fixed a false "your update didn't install" notice that could appear while the update was in fact still installing. A beta-to-beta bump within the same version number, committed by a quit that no live process observed, lost the only timestamp the app had for when the install began, so the next launch judged a healthy install as failed. The app now keeps that timestamp across its own boot-time bookkeeping and holds the verdict until the install has really had its chance.
-
Terminal → New Terminal Window now inherits the project even when the window it was invoked from is still loading. The menu bar is live from the moment a window opens, but the app only recorded which project a window belonged to once that window's editor had finished loading. Invoking the command inside that gap resolved no project at all, so the new terminal opened silently in your home directory with no connection to the project you were looking at. The same gap also affected single-file windows and windows attached to an already-running server. A window is now associated with its project from the moment it is created, so the command resolves the same project throughout.
-
Keep the slash and wiki-link suggestion picker inside the editor pane
The picker is two fixed columns — a list beside a preview — about 490px wide, so a pane narrowed by a docked terminal or agents rail could not hold it and it painted over the dock, the same escape the formatting bar and comment composer were just fixed for. The picker now caps its width to the pane, and drops the preview column when the pane cannot hold both rather than squeezing each into something unreadable.
-
Typing in a large document no longer costs the server twice the markdown parsing it needs. Every keystroke in the visual editor makes the server re-derive the document's source text, and that work included parsing the document's current bytes from scratch even though the previous keystroke had just produced those exact bytes and parsed them already. On a 231 KB document that redundant parse was roughly half the per-keystroke cost. The server now reuses the parse it already has when the bytes are unchanged, and falls back to parsing whenever they are not, so documents whose source is not what the serializer would emit behave exactly as before. Typing on large documents should feel more responsive; there is no change to what gets written to disk.
-
An update you downloaded is no longer thrown away when Windows ends your session, and the app no longer tells you an install failed when it never started one.
On Windows there were only two moments an update could actually install: you clicked "Relaunch now", or you quit the app and it installed on the way out. Shutting the machine down is neither. Windows terminates the app without giving it the chance to run its quit steps, so the update was silently discarded — and because the app had already recorded that an install was underway, the next launch reported a failure for something that had never been attempted.
For anyone who leaves the app open and shuts the laptop down rather than quitting, that repeated every time, so they never got an update at all. After three launches the failure notice gave up and went quiet too, leaving no sign that an update was sitting there ready to install.
The app now tells the two situations apart. An install that was genuinely handed over and did not take is still reported as a failure, unchanged. An update that no quit ever committed is recognized as still waiting, and is offered again the ordinary way — the same "ready to install" prompt you would have seen after the download — for as long as it is waiting. Nothing installs without you choosing it.
-
The bundled agent skills now match the MCP tool surface.
linksis called with the parameter names the tool accepts (document,sourceDocuments),auditis named as the end-state check and described by what it actually reports (every lint violation plus every broken internal link, not just dead links), withlinks({ kind: "dead" })as the graph reader and a superset ofauditon the source side, external images route through the ingest procedure instead of an ad-hoc fetch, and the OKF reserved-file frontmatter exemption is documented. The project skill refreshes on its own. A pack skill already seeded into a project is yours to edit, so it keeps its old text and seeding again will not refresh it; update that skill from source, or delete it and reseed, to pick up the corrected wording. -
Agent tabs are easier to tell apart. Three changes to the tab strip:
- Unread indicator. A settled
readytab whose transcript advanced while you were on a different tab now pulses its status dot until you click into it — no more "did that other agent finish yet?" polling. - Per-thread identity. Every thread gets a stable, hashed tint ring around its agent icon, so a row of Claude tabs isn't just "same icon, truncated title" — each thread has a persistent visual anchor.
- Better auto-titles. The tab title now strips a leading agent-name address (
Claude, what's 2+2?→What's 2+2?, butCodex is failing to startpasses through unchanged), plus punctuation the prompt itself opens with (— refactor the parser→Refactor the parser), so the distinguishing words land at the start of the label.
- Unread indicator. A settled
-
Opening two projects at once no longer wedges one of them behind a red "the project's server is on a different branch than this window" banner. Each window remembers the git branch it last saw so that a freshly restored window can prove to the server which branch its cached content belongs to. That memory was stored under a single key shared by every open project, because it was scoped to the app rather than to the project. Whichever window wrote last decided what the next window read, so a window could start up claiming a completely different project's branch. The server correctly rejected the mismatched claim, the document then failed to load for 30 seconds, and the banner blamed the server even though the server was on the right branch the whole time. This was most visible right after an update, when every window restarts and reads that memory at once. Each project now keeps its own copy, so one project can no longer answer for another. The per-document lineage records, which had the same app-wide scope, are now per project as well.
-
Restarting a project's server no longer leaves the window it was started from without a project. While the old server was terminated and a replacement window came up, the window still on screen belonged to no project as far as the app was concerned, so anything needing one came up empty for those few seconds: New Terminal Window opened without the project, popping a note out was refused, opening another terminal tab in that window was refused, and the window's session state stopped being saved. Those actions now resolve the project for the whole restart.
-
Scrolling back through terminal history no longer dead-ends after the panel changes size. Moving the terminal between its bottom dock and the right column while scrolled up left the view unable to go any further up, by wheel or by keyboard, and it stayed that way, because the resize left the scrollbar and the rows pointing at different lines. The lines above were never lost, but the only way back to them was scrolling all the way down and starting again. The panel now puts the two back in agreement whenever the grid resizes under a scrolled-back view.
Resizing the terminal still moves your place in the history: it keeps the most recent visible line on screen, so a tall panel becoming a short one leaves you further down the buffer than you were. That part is unchanged.
One thing is new, and it is the trade this makes: if the terminal resizes while a scroll of yours is still gliding to a stop, that glide now stops where it is instead of finishing. Reaching the history again is worth more than the last few lines of momentum, but it is a change you can notice.
-
A component the editor does not recognize swaps itself for a raw-source view as soon as you open the document, and two things were reading that swap as though you had typed it.
The block wrapping such a component now keeps the source you wrote. When a component nested inside another one swapped first, the outer block was marked edited, lost its verbatim source, and was rebuilt from scratch with one newline at its opening and closing tags where you had written a blank line. The server wrote that second spelling over yours, which produces the same duplication and truncation described in the entry about typing in source mode, reached a different way. This covers the case where the wrapping block is itself unrecognized. It does not cover the other shape: when the wrapper IS one the editor recognizes, such as a callout, an accordion or a set of tabs, and only the component inside it is unrecognized, the server still rebuilds that wrapper and the duplication above can still happen to it. That case is tracked separately.
Opening any such document in a preview tab no longer makes the tab permanent. A single click on a file you only wanted to read was treated as a commitment, so the tab stopped being reusable and stayed in the strip.
-
Creating a folder from the sidebar, or renaming one while you have it open, no longer closes its tab and drops you back on the empty workspace screen. A folder tab was kept only while its folder appeared in the server's folder listing, and an empty folder does not reach that listing until the server has caught up. A background refresh landing inside that window closed the folder you had just navigated into, and because a freshly created folder is usually the only thing open, the workspace was left with nothing and fell back to the onboarding view. The folder you are currently viewing is now held open through that window, matching the protection documents already had, whether you reached it from the sidebar or from a wiki link. Folder tabs you have navigated away from are still tidied up as before, and deleting a folder yourself still closes its tab straight away.
One behavior worth knowing, because it is new: a folder deleted outside the app, by an agent or another client, keeps its tab open for as long as you are viewing that folder.
-
A document holding a component the editor does not recognize inside another component was reported as losing content when nothing had been lost.
When the editor meets a component it does not recognize, it shows that component as its raw source text. Before persisting a document the server compares the version it is about to write against a fresh reading of those same bytes, so that a write which drops your writing gets caught. That comparison could not tell raw source text apart from the structure the very same bytes read back as, so it counted the tags and the markdown inside a raw view as writing that had gone missing. Documents in that shape were flagged, and left rows in their own history announcing a recovered loss that never happened.
The comparison now reads a raw-source block as source rather than as writing, so it no longer expects that block's markup to survive being parsed. Documents in that shape are no longer flagged for it.
What this gives up: this check gets less sensitive, not just more accurate. Writing that vanishes from inside a raw-source block is no longer visible to it. Neither, in a document holding a raw-source block, is some writing that vanishes from outside one: the check compares two whole documents as a single run of letters with no way to tell which letters came from which block, so once a raw-source block is set aside its letters can stand in for writing lost nearby. What that costs is this check's warning and the restore point it saves before applying such a write. What bounds it is where the check started: before this change it flagged every save of a document holding an unrecognized component, so what it reported for those documents was already unusable.
Unchanged: when a wrapper the editor does recognize, such as a callout, an accordion or a set of tabs, holds a component it does not recognize, blank lines you wrote around that wrapper are still rewritten. That is tracked separately.
-
An unsynced edit can no longer cross between two open projects. When a window reconnects to a server that has restarted, it parks any edit that had not yet reached the server in browser storage and replays it once the connection is healthy again. That parking spot was named after the branch and the document path, but not after the project, and every project window in the desktop app is served from a single origin. So two projects that are worktrees of the same repository, open on the same branch, addressed one parking spot for a document at the same path. Whichever window replayed first consumed the record, which meant a project could receive text that was typed in a different project, while the window that lost the race stood down and dropped its own edit. Because the payload is document content rather than a claim about state, this crossed edits silently instead of raising an error. The parking spot is now named per project as well, so a window only ever contends with other windows of the same project.
-
Removed the transitional
ui.lockcleanup that shipped alongside theui.lockretirement.ok stopno longer SIGTERMs a lingering pre-retirement UI holder,ok cleanno longer prunes a leftoverui.lock, andok stop all/ the uninstall flows no longer discover a lock directory that holds only aui.lock. Builds before v0.60.0 wrote aui.locknext toserver.lockon every UI-serving start, so if one of those servers did not shut down cleanly the file may still be sitting in<project>/.ok/local/. It is inert — nothing reads it — butok cleanwill no longer remove it: delete it withrm .ok/local/ui.lock. A server from one of those builds that is still running also still holds itsserver.lock, sook psandok stopcontinue to see and stop it normally. Whatokcan no longer reach is a live UI process that is not the server itself: a standaloneok uisidecar, an--only ui --server-urlproxy, or the UI sibling thatok startauto-spawned before v0.53.0. In each of those,cat .ok/local/ui.lockreports thepidit recorded; stop it directly (kill <pid>, ortaskkill /F /PID <pid>on Windows) before deleting the file. -
Record what the server actually bound, and stop the spawn-error log destroying its own evidence.
A
server.lockadvertises a port, but until now nothing recorded that a server had bound one — so a diagnostic bundle could not tell a live listener from a stale advertisement, or say which process owned a port when two servers had run for the same directory. The server now logs its pid, port, bound addresses and base URL at listen time.last-spawn-error.logwas opened in truncate mode on every spawn, so a retry seconds after a failed spawn destroyed the output explaining the failure it was retrying. Both documented writers of that file — the desktop spawn and the MCP shim — now append behind a per-attempt header and start over only at a size cap, sharing one policy so neither erases what the other accumulated. The spawn-failure report bounds its stderr tail to the current attempt, so a child that dies silently no longer inherits the previous attempt's stack trace as its cause.
Downloads
| Platform | Architecture | Download |
|---|---|---|
| macOS | Apple Silicon | OpenKnowledge-arm64.dmg |
| Windows | x64 | OpenKnowledge-Setup-x64.exe |
| Windows | arm64 | OpenKnowledge-Setup-arm64.exe |
| Debian / Ubuntu | x64 | OpenKnowledge-amd64.deb |
| Debian / Ubuntu | arm64 | OpenKnowledge-arm64.deb |
| Fedora / RHEL | x64 | OpenKnowledge-x86_64.rpm |
| Fedora / RHEL | arm64 | OpenKnowledge-aarch64.rpm |