Skip to content

feat(desktop): package OpenKB as a Tauri desktop app (draft)#192

Draft
KylinMountain wants to merge 13 commits into
mainfrom
claude/desktop-tauri
Draft

feat(desktop): package OpenKB as a Tauri desktop app (draft)#192
KylinMountain wants to merge 13 commits into
mainfrom
claude/desktop-tauri

Conversation

@KylinMountain

Copy link
Copy Markdown
Collaborator

Draft. The full build chain is verified on Linux; the Windows/macOS matrix
jobs need a real CI runner to prove out. Opening for direction + review.

Summary

Packages OpenKB as a desktop app for non-technical users — no pip, no terminal.
A thin Tauri (Rust) shell wraps two things the repo already produces:

  1. The API sidecaropenkb-api frozen into a self-contained, slimmed
    PyInstaller binary that serves both the JSON/SSE API and the built web UI.
  2. The Studio web UI (feat(workbench): Knowledge Workbench — new React/TS frontend + supporting backend #189) — served by the sidecar; the shell points its
    WebView at it.
Tauri shell (Rust + system WebView)
  ├─ spawns ──▶ openkb-api-sidecar (frozen)   ┌ /api/v1/*  (JSON/SSE)
  │                                           └ /          (web UI)
  └─ WebView ─▶ http://127.0.0.1:<port>/

The user double-clicks the app; the shell starts the sidecar on localhost, waits
until it answers, then navigates the WebView to it. No browser, no port, no
"server" ever surfaces.

What's in it

desktop/packaging/ — freeze openkb.api into a slim sidecar:

  • pyi_rthook_magika.py: runtime hook stubbing magika so markitdown converts
    Office/HTML by file extension, letting the build drop magika + onnxruntime.
  • prune_litellm_proxy.py: post-build removal of the unused LiteLLM proxy server.
  • build_sidecar.sh: the PyInstaller recipe; --collect-all openkb bundles the
    built web UI (openkb/web) + prompt/template data so the sidecar serves /.

desktop/src-tauri/ — the Tauri v2 shell: creates the window with an
init-script that sets window.__OPENKB_DESKTOP__ (the Studio frontend gates its
in-app TitleBar on it), spawns the sidecar (non-fatal on failure), polls its
health, navigates to it, and kills it on exit. The PyInstaller onedir is bundled
as a resource dir (not externalBin, which expects a single file).

frontend/ — a first-launch onboarding wizard on the Studio frontend
(welcome → connect model → create first KB), shown when no API key is configured.
Built with the frontend's own conventions (react-i18next en/zh, shadcn Dialog,
config/kb API clients); no new backend.

.github/workflows/desktop.yml — build matrix over
Linux / Windows / macOS(arm64 + x86_64), and a release job that attaches every
platform's installers to a GitHub Release on a desktop-v* tag.

Verified (locally, x86_64 Linux)

  • Frozen slim sidecar boots uvicorn and serves GET /api/v1/kbs and the UI
    at / (200). ~134 MB compressed; no magika/onnxruntime, proxy pruned.
  • Tauri shell compiles and runs under xvfb (WebKitGTK): it navigates to the
    sidecar and renders the desktop TitleBar (confirming __OPENKB_DESKTOP__
    reaches the page).
  • Onboarding renders end-to-end (headless Chromium) and persists config via the
    Studio config API.
  • cargo tauri build produces a .deb and .rpm (~175 MB each) containing
    the app + bundled sidecar.

Not done yet / needs attention

  • Windows/macOS matrix — the workflow is written but only runnable on real
    runners; not yet exercised.
  • Signing/notarization — artifacts are unsigned (macOS Gatekeeper / Windows
    SmartScreen will warn). Needs certificates before public distribution.
  • AppImage downloads appimagetool at build time (needs unrestricted
    network; fine on GitHub runners).
  • Two first-party actions (upload/download-artifact) still need SHA-pinning to
    match the repo's policy (marked TODO in the workflow).
  • pyinstaller is installed ad-hoc in CI (build tool, not a runtime dep).

Scope

Additive only — everything lives under desktop/ plus the frontend onboarding.
No changes to existing Python modules or the packaged wheel.


Generated by Claude Code

Foundation for packaging OpenKB as a desktop app for non-technical users. The
architecture is a thin Tauri (Rust) shell that spawns the openkb-api server
(from #150) frozen into a slim, self-contained PyInstaller sidecar, then points
the system WebView at it over localhost.

desktop/packaging/ — freezes openkb.api:main into a slim onedir binary:
  - pyi_rthook_magika.py: runtime hook stubbing magika so markitdown converts
    Office/HTML by file extension, letting the build exclude magika +
    onnxruntime entirely.
  - prune_litellm_proxy.py: post-build removal of the unused LiteLLM Proxy
    Server (static/data assets no client code path imports).
  - build_sidecar.sh: the PyInstaller recipe wiring both together.
  - sidecar_entry.py: frozen entry mirroring the openkb-api console script.

desktop/src-tauri/ — reference Tauri v2 shell: spawns the sidecar, shows a
  splash while polling its health, navigates the WebView to it, kills it on
  exit. PyInstaller onedir output is bundled as a resource directory (not
  externalBin, which expects a single file).

Verified (x86_64 Linux): the frozen slim sidecar boots uvicorn and serves
GET /api/v1/kbs -> 200; ≈134 MB compressed / 338 MB on disk. The src-tauri
shell is a skeleton, not compiled here (the dev container lacks webkit2gtk);
build it where the platform WebView libraries are present. See desktop/README.md.

Claude-Session: https://claude.ai/code/session_01KZyUSGAzVL9yxpsWWPv6Y2
…icons

Verified by installing libwebkit2gtk-4.1-dev and running `cargo build`, which
compiled the full Tauri dependency tree and surfaced two skeleton errors:

- `frontendDist` pointed at "../splash" (resolves outside src-tauri) but the
  splash page lives at src-tauri/splash — corrected to "splash".
- `generate_context!` requires a default `icons/icon.png`; added a generated
  app icon set (png sizes + ico).

`cargo build` now finishes cleanly. Running the window still needs a display
(headless container has none); the web UI itself was verified rendering
end-to-end against the sidecar via headless Chromium.

Claude-Session: https://claude.ai/code/session_01KZyUSGAzVL9yxpsWWPv6Y2
…under xvfb

If the sidecar can't start (missing binary, port already in use), log and keep
the window on the splash instead of aborting the whole app — better failure UX,
and it lets `cargo run` dev attach to an externally-run `openkb-api`.

Verified end-to-end: built the web UI, ran openkb-api on :8765, launched the
Tauri binary under xvfb (WebKitGTK). The shell polled the server, navigated off
the splash, and rendered the real Workbench UI — confirming the full
shell → poll → navigate → render flow works.

Claude-Session: https://claude.ai/code/session_01KZyUSGAzVL9yxpsWWPv6Y2
Re-implements the setup wizard natively in the new (#189) React/TS frontend,
using its conventions — react-i18next (common namespace, en+zh parity),
shadcn Dialog/Button, and the config/kb API clients.

Shown when the global config reports no LLM key (has_api_key=false):
1. Welcome.
2. Connect your model — model + API key + optional custom base URL, saved via
   patchGlobalConfig (RFC 7386 merge-patch; the key is only sent when typed and
   is never returned by the API).
3. Create your first knowledge base — createKb, then navigates to it.

"Skip for now" dismisses it. Wired in App.tsx via a getGlobalConfig() check on
mount. Build passes (i18n:check key-parity + no-hardcoded-CJK guard, tsc -b,
vite build); verified rendering steps 1–2 via headless Chromium against a
freshly-built frontend served by openkb-api.

Claude-Session: https://claude.ai/code/session_01KZyUSGAzVL9yxpsWWPv6Y2
The #189 frontend gates desktop-only UI (the in-app TitleBar, offset chrome
cluster) on a `window.__OPENKB_DESKTOP__` flag. Set it from the shell so those
features light up when the UI runs inside Tauri.

The window is now created in setup() via WebviewWindowBuilder with an
initialization_script, instead of declaratively in tauri.conf.json — an init
script runs before page scripts on every navigation, so the flag is present on
the splash and (critically) after we navigate the WebView to the sidecar's
served UI. tauri.conf.json's windows[] is emptied accordingly.

Verified under xvfb (WebKitGTK): with the sidecar serving the new frontend,
the Tauri window renders the desktop TitleBar ("OpenKB Studio — Home", traffic
lights, Local badge) — confirming the flag reaches the page. cargo build clean.

Claude-Session: https://claude.ai/code/session_01KZyUSGAzVL9yxpsWWPv6Y2
…ecar

The sidecar froze only openkb's Python modules, so `openkb.api`'s static mount
of `openkb/web` found nothing and served `/` as 404 — an installed app would
open to a blank/error page. Add `openkb` to `--collect-all` so PyInstaller
bundles its data files (the built web UI at openkb/web, plus prompt/template
assets) and any lazy/dynamically-imported submodules.

The web UI must be built (npm run build → openkb/web) before this script runs.

Verified: the frozen sidecar now serves `/` → 200 with `<title>OpenKB
Studio</title>` and its `/assets/*.js` → 200 (was 404). Size 336M → 343M.

Claude-Session: https://claude.ai/code/session_01KZyUSGAzVL9yxpsWWPv6Y2
New workflow that builds the desktop app: install WebView deps → build the web
UI → freeze the openkb-api sidecar (PyInstaller) → `cargo tauri build` → upload
the installers. Triggered on workflow_dispatch and `desktop-v*` tags.

Linux-only for now; the matrix is structured so Windows and macOS (arm64 +
intel) entries can be added once the Linux path is proven — each needs its own
sidecar freeze since neither PyInstaller nor Tauri cross-compiles.

Reuses the existing workflows' pinned action SHAs (checkout/setup-python/
setup-node); upload-artifact is left on a tag with a TODO to SHA-pin.

Claude-Session: https://claude.ai/code/session_01KZyUSGAzVL9yxpsWWPv6Y2
The pipeline (web build → sidecar freeze bundling openkb/web → cargo tauri
build) is verified locally end to end: it produces a .deb and .rpm (~175 MB
each) containing the app + frozen sidecar, and the shell renders the desktop
TitleBar under xvfb. Notes the AppImage target needs network for appimagetool.

Claude-Session: https://claude.ai/code/session_01KZyUSGAzVL9yxpsWWPv6Y2
Extends the desktop build to all platforms (each freezes its own sidecar and
links its own WebView — neither cross-compiles) and adds a release job that
attaches every platform's installers to a GitHub Release on a `desktop-v*` tag.

The sidecar freeze runs under `shell: bash` (git-bash on Windows) with
PYTHON=python. macOS arm64/x86_64 are native builds on macos-14/macos-13.
Artifacts are unsigned for now (macOS notarization / Windows signing TBD); a
couple of first-party actions still need SHA-pinning (marked TODO). The Linux
path is verified locally (.deb/.rpm); Windows/macOS need a real runner.

Claude-Session: https://claude.ai/code/session_01KZyUSGAzVL9yxpsWWPv6Y2
Adds a "Wiki language" field to the onboarding setup step, reusing the existing
UnLanguageDatalist (the six official UN languages) as free-text quick-picks —
so a non-English user sets their knowledge-base language up front. Saved via
patchGlobalConfig config.language. en/zh label added to the common namespace.
Expands the interface beyond zh/en to the remaining UN official languages
(minus Arabic for now — RTL is a separate effort):

- SUPPORTED_LANGUAGES gains fr/es/ru; the LanguageToggle becomes a dropdown
  menu (radio group of every language by its autonym) instead of a 2-way
  toggle, and initial-language detection matches the browser locale's base
  subtag against the supported set.
- Full fr/es/ru translations for all eight namespaces (common, home, kbList,
  kb, kbSettings, settings, chat, artifacts), preserving every {{placeholder}}
  and <0/> tag; Russian includes the one/few/many/other plural forms. Each
  locale has complete key coverage vs en (no zh fallback at runtime).

Verified: build passes (i18n:check en/zh parity + no-hardcoded-CJK, tsc, vite);
the menu lists 中文/English/Français/Español/Русский and switching to Français
translates the whole UI (headless Chromium).

Note: fr/es/ru strings are machine translations and should get a native-speaker
review pass before release.
Adds Japanese to SUPPORTED_LANGUAGES and full translations for all 8
namespaces (common, home, kbList, kb, kbSettings, settings, chat,
artifacts). Japanese has no plural distinction, so _one mirrors _other.
Registers 日本語 in the language dropdown (en/zh common.json autonyms).
Verified: full key parity vs en, build passes, UI renders in Japanese.

Claude-Session: https://claude.ai/code/session_01KZyUSGAzVL9yxpsWWPv6Y2
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