Desktop Windows/Linux port — P1 buildability + Tier B parity (#2707)#756
Merged
Conversation
* spec: desktop windows/linux port — scope-frozen SPEC + evidence
* feat(desktop): electron-builder win/linux targets, per-platform natives, cross-platform fuse hooks
P1 buildability slice of specs/2026-07-16-desktop-windows-linux-port (D1,
D2, D9, D10 packaging halves):
- electron-builder.yml: NSIS win x64+arm64 (one-click, per-user,
per-arch installers, version-less artifact names); linux AppImage+deb
x64+arm64; per-platform extraResources for the ok wrapper + keyring
binaries (darwin rules moved under mac:); node-pty excluded off-mac
(D7 terminal dock dark)
- build/installer.nsh: HKCU user-PATH append/remove for the bundled CLI
+ openknowledge:// protocol registry keys (electron-builder protocols
is mac-Info.plist-only)
- build/deb-postinst.sh / deb-postrm.sh: default template copies +
/usr/bin/{ok,open-knowledge} symlinks
- resources/cli/bin/ok.cmd + ok.ps1 (Windows wrappers) and ok-linux.sh
(flat-layout Linux wrapper), same self-diagnosing exit-69 contract
- scripts/afterPack.mjs + afterSign.mjs: fuses flip + verify on ALL
platforms (D9); helper-bundle clone, node-pty chmod, notarize stay
darwin-only; shared scripts/resolve-electron-binary.mjs
- scripts/prepare-platform-natives.mjs: force-install win/linux keyring
prebuilds for cross-arch builds; build:win / build:linux scripts
- tests: per-platform extraResources merge in cli-native-deps test,
wrapper-shipping assertions, ok-linux.sh contract tests
* ci(desktop): manual artifact-only Windows/Linux build workflow (D6)
workflow_dispatch only, publishes nothing, triggers nothing: ubuntu
prepare job builds the workspace (CLI build chain is bash-shaped) and
hands cli/dist + the staged native-config addon to per-OS packaging
jobs (electron-vite + electron-builder). Azure Artifact Signing is
optional via AZURE_* secrets/vars; unsigned Windows output is QA-only.
* feat(desktop): win/linux window chrome (D8), pty capability gate (D7), deep-link self-registration (D2/Q8)
- window-chrome.ts: titleBarStyle hidden + theme-matched titleBarOverlay
+ solid background for non-darwin windows; nativeTheme 'updated'
re-applies (setTitleBarOverlay win32-only, background everywhere);
autoHideMenuBar keeps the native menu row hidden while its
accelerators stay live (D11 contract)
- renderer: electron-platform-* class from the FOUC script; win/linux
collapse the traffic-light left reserve and reserve the right edge
for the Window Controls Overlay via titlebar-area env() vars
- ptyAvailable on OkDesktopConfig (desktop+app mirrors, preload +
get-info): terminal dock affordances, Settings Terminal section and
the Terminal menu handlers all hide off-mac instead of surfacing a
node-pty spawn failure; dock stays available for future non-pty
content
- url-scheme.ts: packaged win/linux setAsDefaultProtocolClient
self-heal per boot; appimage-integration.ts writes the
x-scheme-handler .desktop entry for AppImage launches (APPIMAGE env),
XDG_DATA_HOME-aware, best-effort xdg refresh
* feat: Tier B parity off-darwin — MCP/skill reclaim, PATH install, ok desktop auto-launch (D4/D10)
- install-shape.ts: shared packaged-install classifier (mac bundle /
NSIS / linux dir / appimage / unsupported) replacing the darwin-only
.app regex across every machine-integration gate; AppImage declines
all persistent-path integrations (its mount path dies each boot)
- bundle-paths.ts: per-platform ok wrapper resolution (ok.cmd on win,
flat resources/ layout on linux) — also fixes the packaged local-op
CLI spawn path off-mac
- path-install.ts: linux runs the full rc/symlink flow (adds .bashrc
for bash logins, keeps mac behavior byte-identical); win32 returns
skipped/installer-managed (NSIS owns user PATH per D10)
- mcp-wiring / skill-reclaim / project-mcp-reclaim: gates lifted to the
classifier; inner machinery was already platform-parameterized via
cli editors.ts (win32 chain shape, %APPDATA%/XDG config resolvers)
- integrations settings (user + project scope) available on win/linux
via the same shared predicate
- cli desktop-dispatch: per-platform install probes (%LOCALAPPDATA%
NSIS dir, /opt deb path, wrapper introspection on both) + direct-exe
launch off-mac; linux headless gate also requires a display server;
reason 'unsupported-platform' added ('darwin-only' retired from
emission, kept in the union)
- resolve-detached-spawn-args: windowsHide on the detached server spawn
* feat(desktop): custom Windows/Linux renderer menubar (D11)
macOS keeps the native menu bar; win/linux draw a VS Code-style menubar
in the chrome row (shadcn Menubar, Lingui'd labels) since D8's hidden
titlebar leaves no native one. Contract per D11:
- ok:menu:dispatch — ONE discriminated RequestChannel (sharing-dispatch
precedent; ratchet 83→84): 'query' returns the aggregated snapshot
(active target + view-menu state + recents + capability flags) that
drives the native menu's enable/check rendering; 'menu-action' relays
through the same sendMenuActionToFocused path native items use;
'role' maps onto Electron menu-role behaviors; 'command' reuses the
native deps' click handlers (navigator, folder dialog, settings,
updater, MCP re-arm — extracted to reconfigureMcpWiringFlow)
- accelerators stay registered on the (hidden) main-process Menu, so
shortcuts work without the DOM menubar focused; MenubarShortcut
strings are display-only
- AppMenubar mounts in EditorHeader (all window kinds incl.
single-file) and the Navigator chrome row; Terminal menu deliberately
absent off-mac (D7 pty gate)
- menu namespace added to the three OkDesktopBridge copies + preload;
types 4-way mirrored per the EditorViewMenuStateSnapshot lockstep
* docs: changeset + AGENTS.md desktop-platform line for the win/linux port
* chore: strip spec decision markers from source comments (comment discipline)
* test(app): widen the FOUC electron-mode source guard for the platform-class stamp
* test(app): model pty capability in terminal-surface DOM fixtures
The dock and Settings gates now require config.ptyAvailable; the mocks
model the capable macOS host, plus a new pty-less-host case.
* fix: lazy-load AppMenubar (size gate) + normalize trailing-semicolon PATH in NSIS include
- AppMenubar (and its radix Menubar primitive) now loads via lazy()
behind the tiny app-menubar-gate predicate — web + macOS never
download the chunk; reclaims 9.5 kB of the eager bundle. Main-app
size limit 476->477 kB for the 12-byte residual (rationale in the
size note).
- installer.nsh strips a pre-existing trailing ';' from HKCU Path
before appending so the write never produces an empty PATH segment
(review nit).
* test(desktop): pin platforms in host-dependent tests; cover packaged win/linux protocol self-heal
CI's test tier runs on Linux — three tests keyed off host defaults my
per-platform changes made host-sensitive:
- bundle-paths.test.ts pins 'darwin' (the default arg is
process.platform for the production call sites)
- computePathInstallDescriptor / isPathShimInstalled /
removePathShimFromRcFiles gain an explicit platform opt (defaults to
the host); tests pin 'darwin' so the linux .bashrc target doesn't
leak into mac-shape assertions
- url-scheme-handler.test.ts pins the packaged-no-registration
assertion to darwin and adds packaged win32/linux self-heal coverage
(setAsDefaultProtocolClient per boot, no before-quit removal)
* test(cli): pin darwin in launchDesktop spawn-shape tests; cover win/linux direct-exe launch
Same Linux-CI-host class as the desktop test pins: launchDesktop
defaults platform to process.platform, so the open -b assertions only
hold on darwin. Adds per-platform direct-exe spawn-shape coverage
(detached + stdio ignore + unref, args empty).
* review: tarball integrity pin, menu-type drift catcher, off-mac done-path coverage, message/log fixes
Addresses the claude[bot] review majors + minors:
- prepare-platform-natives.mjs verifies each fetched keyring tarball's
sha512 against pnpm-lock.yaml before extraction (fail-closed on a
missing lockfile entry) and cleans the temp tarball on failure
- bridge-contract-types.test.ts gains 4-way Eq assertions for
MenuDispatchRequest/MenuRendererSnapshot (the ipc-channels copy is
the one call-site typechecks miss)
- linux/win32 done-path tests: project MCP repair cycle on NSIS + deb
exec shapes; user skill reclaim through the deb shape (launchDesktop
spawn shapes landed in the previous commit)
- notFoundMessage stat-error/available cases no longer hardcode the
macOS install path; open-settings executeJavaScript catches window
teardown; AppImage registration logs route through the desktop
logger; integrations-settings available JSDoc updated to the
install-shape gate; prepare-universal.mjs error says pnpm install
* fix(desktop): guard StrFunc declarations per NSIS compile pass
electron-builder compiles installer.nsh twice (BUILD_UNINSTALLER pass,
then installer pass); an unreferenced StrFunc declaration in either pass
trips makensis warning 6010, which electron-builder promotes to an error.
Caught on the first real makensis run (arm64 QA build) — the manual
build workflow has not been dispatched yet, so CI never compiled it.
* fix(desktop): three Windows-install defects found by live VM QA
- NSIS payload: useZip + differentialPackage:false. The default solid-7z
payload fails extraction under Windows-on-ARM's x86 emulation (Nsis7z
drops the block holding the exe and every dll — half-install with no
error), and useZip alone is ignored while the build is
differential-aware, which stages 7z bytes under a .zip name and dies
with 'Error opening ZIP file'. A true zip payload installs correctly
(VM-verified end-to-end).
- Exclude native-config's darwin cargo target/ tree from win/linux
packages: 65 MB dead weight (28% of the installer) whose .fingerprint
paths exceed MAX_PATH and survive uninstall.
- CLI desktop dispatch: probe the real one-click install dir
(sanitized package name, not productName) and treat isTTY=undefined as
the interactive-console signature on win32 — Electron-as-Node stdio are
pipes on Windows, so bare `ok` always read as headless and never
launched the desktop.
* fix(desktop,cli): Linux-install defects found by live VM QA
- deb maintainer scripts: the comment WARNING about FpmTarget's macro
templating itself contained a dollar-brace token, which the templater
rejects — first-ever FpmTarget run caught it (deb was never built).
- deb metadata: electron-builder requires homepage + author.email for the
Debian control file; the private desktop package.json had neither, so
the CI linux build would have died the same way.
- MCP chain v2: the unix resolver chain never probed the Linux deb
install — a deb-only user's entry fell through to npx @latest (version
drift) or, with no Node, exit 127 'install OK Desktop' while OK Desktop
was installed. v2 inserts the /opt/OpenKnowledge probe between the mac
bundle probes and the npx fallback; the sentinel bump makes existing v1
entries classify stale, and the desktop repair sweep upgrades them in
place (VM-verified: all user-global + project entries migrated on first
boot, and the written entry execs the deb wrapper).
- Wired-at-all markers in skill-reclaim + repair-skills now match the
version-independent '# ok-mcp-' family prefix (the shape
worktree-setup-inherit already used, and whose JSDoc names this exact
trap) — pinning them to one version would have broken wired-detection
for every existing v1 project after the bump.
* fix(cli): share publish inherits the ambient env for git spawns
simple-git's .env(obj) REPLACES the child environment, and makeGit passed
a bare { GIT_TERMINAL_PROMPT: '0' } — stripping USERPROFILE/SystemRoot/
PATH on Windows (no global config, no HTTPS transport) AND the
GIT_AUTHOR_* identity fallback the command sets for machines with no git
identity. The initial commit died with 'Please tell me who you are' AFTER
the GitHub repo was created, leaving a half-published project the wizard
can't resume (name-check blocks on the now-taken name). POSIX hosts
survived by accident: execvp's default PATH finds /usr/bin/git and the
passwd-db HOME fallback finds config. VM-verified on a real Windows
install: repo created, origin set, zero commits.
Fix: share makeGit with clone.ts's plumbing — buildCloneEnv (spread
process.env + prompt/locale overrides) and buildCloneGitOptions (the
unsafe flags simple-git needs to accept an env carrying the user's
PAGER/EDITOR/SSH vars). New regression test proves env-only identity
reaches the commit child with every config-file source isolated.
* fix(desktop): restore System32 on the Windows server PATH so Open-with-Claude/Cursor work
On Windows, `process.env`'s search-path key is `Path`, not `PATH`. The detached-
server spawn read `env.PATH` case-sensitively (undefined on Windows), so
`buildEnrichedPath` collapsed the child's PATH to the git-enrichment dirs only —
dropping `C:\Windows\System32`. The server's `reg` (handoff install-detection),
`rundll32` (protocol-URL dispatch), and `cmd.exe`/`where` (Cursor spawn) are bare
command names libuv resolves via the child's PATH, so they failed ENOENT. Result:
`POST /api/handoff` returned 422 and "Open with Claude/Cursor" silently no-op'd on
the desktop app — even though the renderer's Electron `getApplicationInfoForProtocol`
detection correctly showed them installed (the web app was unaffected because its
server inherits the shell's real `Path` directly).
Read the inherited PATH case-insensitively and emit exactly one canonical `PATH`
key (strip case-variants from the spread so libuv can't break a `Path`-vs-`PATH`
tie by keeping the un-enriched or git-only value).
Verified on the ARM64 Win11 VM: `POST /api/handoff` went 422 -> 200 and Claude +
Cursor launched with the correct workspace/prompt. Added a `Path`-cased regression
test — the existing coverage only exercised uppercase `PATH`.
* fix(desktop): address PR review — catch open-settings executeJavaScript, pnpm comment
- openSettings menu handler: `.catch()` the `executeJavaScript` promise (TOCTOU
window teardown) so a mid-dispatch rejection degrades to a no-op instead of an
unhandledRejection, matching the sibling `open-settings` IPC handler.
- electron-builder.yml: stale "Bun" → "pnpm" in the keyring cross-arch comment
(repo migrated to pnpm; sibling comment already says pnpm).
* fix(desktop): gate the composer Terminal handoff group on ptyAvailable (win/linux)
The "Ask AI" bottom composer offered a Terminal group (Claude/Codex CLI) on
win/linux desktop even though node-pty is excluded there (D7), so picking it
was a silent no-op — the docked terminal in EditorPane never mounts, so the
launch routed into nothing (no terminal, no process, no error).
App.tsx built the TerminalLaunchProvider value (read via useTerminalLaunch by
BottomComposer, DocPanel, CreatePromptComposer, OpenInAgentContextSubmenu)
gated only on desktopBridge presence, not config.ptyAvailable. EditorPane,
the Settings -> Terminal section, and the AppMenubar Terminal items already
gate on it; this provider was the one that leaked. Now win/linux desktop
offers only the working Desktop (URL-scheme) handoffs; macOS is unchanged.
Matches the port changeset's own claim that the built-in terminal is hidden
off-mac instead of failing, so no new changeset.
App.dom.test.tsx's createBridge() mock gains the `config` the real preload
always exposes (App now reads config.ptyAvailable).
Verified on the Linux arm64 VM (deb v0.34.0): the Terminal group disappears
from the composer, only the Desktop group remains, and the Cursor handoff
still launches Cursor end-to-end.
* fix(desktop): drop require() in ok-wrapper test (main's new oxlint rule)
The merge brought main's #2762 oxlint rule forbidding require() in ESM test
modules. This branch's ok-wrapper NODE_OPTIONS test used
`require('node:fs')`, which the auto-merge left redundant beside the static
`readFileSync` import main added to the same file. Use the static import.
* fix(cli): point acp-harness-probe fixture at CHAIN_V2 after merge
This branch's Linux-install VM-QA fix (22667b1c73) bumped the canonical
managed-MCP chain CHAIN_V1 (`# ok-mcp-v1`) → CHAIN_V2 (`# ok-mcp-v2`) and
removed the V1 export. Main independently added the opencode harness-probe
test importing CHAIN_V1; the auto-merge kept the V2 source + the V1-importing
test, so `openCodePublished()`'s chain body was `undefined` and both
opencode matcher assertions failed. Retarget the fixture at CHAIN_V2 (the
chain OK now publishes). No other CHAIN_V1 references remain.
* fix(test): migrate PR-touched test files off the bun:test shim (main's #2757 guard)
Merging main brought its incremental guard forbidding `bun:test` imports in
files a change touches (the shim is grandfathered only for the untouched
corpus). This branch's 23 win/linux-port + MCP test files still imported the
shim, so `open-knowledge / lint` failed on them. They passed `pnpm check`
locally because the shim aliases bun:test→vitest at runtime; the guard is a
CI-only diff-scoped step.
- 21 files: pure `from 'bun:test'` → `from 'vitest'` (describe/test/it/expect/
hooks map 1:1).
- url-scheme-handler.test.ts: `mock` was only function spies → `vi.fn`
(`.mock.calls` is vitest-native).
- App.dom.test.tsx: `mock.module` → `vi.doMock` (30+); the SUT was already a
post-mock dynamic `import('./App')`, so the non-hoisted doMock applies. The
shim itself implements mock.module via vi.doMock, so behavior is identical.
biome organizeImports re-sorted the moved specifiers. All 23 files' tests pass
(cli 428, desktop 134+78, app 11+6); guard + full lint green.
* fix(desktop): restore win/linux protocol self-heal test after merge
The second origin/main merge resolved url-scheme-handler.test.ts as a
bun:test double-migration and took main's version wholesale — but that
version also carried main's OLD assertion ("does NOT call
setAsDefaultProtocolClient in packaged builds"), silently reverting this
branch's win/linux split. On the Linux CI host packaged builds DO self-heal
the scheme binding (url-scheme.ts:900 `else if (platform !== 'darwin')`), so
the un-split assertion failed (called once with 'openknowledge').
Re-applied the split onto main's version (keeping main's new foreign-host
trust-gate tests): darwin packaged → not called (platform-pinned); win32/linux
packaged → self-heal per boot, no before-quit. Desktop suite 2703 pass; guard
clean.
* test(server): de-flake git-preflight-boot temp-dir teardown (ENOTEMPTY)
The `gitEnabled:false` boot test intermittently failed the macOS preflight CI
job with `ENOTEMPTY: directory not empty, rmdir '.../state-*'`. `booted.destroy()`
is awaited before the afterEach, but the server's async sinks (persistence
debounce, telemetry export, pino flush) can land a final write into the state
dir a beat after destroy resolves; on a loaded macOS runner that write races
the recursive removal, so the plain `rm` bails with ENOTEMPTY.
Retry the removal with linear backoff (maxRetries: 10, retryDelay: 100 → ~5.5s)
so it outlasts any trailing write. Behavior-preserving — same cleanup, resilient
to the race. Follow-up to #2775 (which addressed the sibling destroyHocuspocus
teardown-timeout symptom). Migrated the file's import off the bun:test shim to
vitest in lock-step (editing it brings it under the diff-scoped #2757 guard).
Passes 3/3 locally; the whole file is bun:test-free.
---------
GitOrigin-RevId: 2d446f5ec3daa98a14613d9f813cd236b1885611
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Copybara-translated 1 Inkeep OSS change. Rebase-merge this PR so the prepared commit lands directly on public main.