Skip to content

Desktop Windows/Linux port — P1 buildability + Tier B parity (#2707)#756

Merged
inkeep-oss-sync[bot] merged 1 commit into
mainfrom
copybara/sync
Jul 22, 2026
Merged

Desktop Windows/Linux port — P1 buildability + Tier B parity (#2707)#756
inkeep-oss-sync[bot] merged 1 commit into
mainfrom
copybara/sync

Conversation

@inkeep-oss-sync

Copy link
Copy Markdown
Contributor

Copybara-translated 1 Inkeep OSS change. Rebase-merge this PR so the prepared commit lands directly on public main.

* 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
@inkeep-oss-sync
inkeep-oss-sync Bot requested a review from nick-inkeep as a code owner July 22, 2026 02:52
@inkeep-oss-sync
inkeep-oss-sync Bot merged commit fb51ec5 into main Jul 22, 2026
@inkeep-oss-sync
inkeep-oss-sync Bot deleted the copybara/sync branch July 22, 2026 02:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant