desktop: tri-platform window chrome, packaging and draft-release CI
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# Desktop builds and releases
|
||||
|
||||
## Platforms
|
||||
|
||||
| Platform | Build runner | Packages | In-app update |
|
||||
|---|---|---|---|
|
||||
| macOS, Apple Silicon | `macos-15` (arm64) | signed/notarized DMG + ZIP | ZIP via Squirrel.Mac |
|
||||
| Windows, x64 | `windows-2025` | unsigned per-user NSIS installer | NSIS |
|
||||
| Linux, x64 | `ubuntu-24.04` | AppImage + `.deb` | AppImage replacement; `.deb` through dpkg/apt with elevation if needed |
|
||||
|
||||
Windows signing is deliberately disabled for this first release. SmartScreen may warn about an unknown publisher on initial installation. AppImage is the primary Linux download because it is self-contained and updates without administrator permission. The installed `electron-updater` 6.8.9 also supports `.deb` updates; an update may request an administrator password. Both use the same download/restart banner.
|
||||
|
||||
The custom app bar stays on all three platforms. macOS keeps its left-side traffic lights; Windows/Linux use Electron's native right-side Window Controls Overlay, with CSS space reserved from the overlay's actual geometry. Windows/Linux hide the native menu until Alt is pressed. macOS keeps its system menu.
|
||||
|
||||
The existing `build/icon.icns` is the source for all platforms: electron-builder converts it to ICO/Linux PNGs. Linux's desktop identity is `learnharness.desktop`. Public package-maintainer metadata uses the owner's GitHub no-reply address; support goes through [GitHub Issues](https://github.com/amosblomqvist/learnharness/issues).
|
||||
|
||||
## Build pipeline
|
||||
|
||||
Each OS installs its own dependencies and builds its own Next standalone tree:
|
||||
|
||||
1. `npm ci`
|
||||
2. Native PDF/SVG renderer tests for that OS, running in Electron's bundled Node (no display needed).
|
||||
3. `npm run build` (`output: "standalone"`).
|
||||
4. `node scripts/build-server.mjs` — copy static assets, refuse leaked data/keys, remove only standalone-root `dist/` and `node_modules/electron/` (old installers and the development runtime), check prompt assets. Runtime dependencies' own `dist/` directories and native renderer binaries stay intact.
|
||||
5. `node scripts/smoke-server.mjs --electron` — boot the standalone server in Electron's bundled Node, using empty temporary data/auth, then check the home route and GET-only APIs.
|
||||
6. `npx electron-builder <platform flags> --publish <policy>`.
|
||||
|
||||
The shell/updater live in `app.asar`; the standalone server and native renderer binaries ship unpacked as `extraResources`. **Do not cross-build the server payload.** Next traces the build machine's native binaries, so a macOS-built standalone tree is not a Windows/Linux payload.
|
||||
|
||||
`npm run desktop:dist` remains the local macOS signing path, using the `LH_NOTARY` keychain profile. For a local Windows/Linux build, run steps 1–5 above, then:
|
||||
|
||||
```sh
|
||||
# On Windows:
|
||||
npx electron-builder --win --x64 --publish never
|
||||
|
||||
# On Linux:
|
||||
npx electron-builder --linux --x64 --publish never
|
||||
```
|
||||
|
||||
## CI vs release
|
||||
|
||||
`.github/workflows/ci.yml` runs on main pushes and pull requests:
|
||||
|
||||
- Typecheck, full deterministic test suite, and lint on Ubuntu.
|
||||
- Native renderer tests plus standalone build/boot-smoke on macOS, Windows, and Linux.
|
||||
|
||||
`.github/workflows/release.yml` has two deliberately separate modes:
|
||||
|
||||
- **Manual / Run workflow:** unsigned rehearsal installers saved as workflow artifacts for 7 days. No signing secrets needed, no release created, and nothing published. macOS notarization is skipped. Use the Windows/Linux artifacts for the first human smoke pass. These are not production macOS installers.
|
||||
- **Push `v<package.json version>`:** gates first, then one job creates or reuses a **single draft GitHub Release** before the three OS jobs package/upload their artifacts and update metadata in parallel. This avoids competing draft creation. A published or ambiguous existing release fails the preparation step instead of being modified. Separate runs for the same ref are serialized, never cancelled mid-upload. The tag must match the package version. macOS must sign and notarize successfully; missing secrets fail loudly. The workflow never publishes the draft.
|
||||
|
||||
The three jobs upload distinct updater feeds: `latest-mac.yml`, `latest.yml`, `latest-linux.yml`, with the matching ZIP/NSIS/AppImage/deb assets and blockmaps. Do not publish a partially populated draft. The app uses the baked GitHub feed without a token: **the repository must be public before end users can download updates.** Making the repository public and publishing the release remain the owner's actions.
|
||||
|
||||
## macOS CI secrets
|
||||
|
||||
Add these under repository **Settings → Secrets and variables → Actions**:
|
||||
|
||||
| Secret | Value |
|
||||
|---|---|
|
||||
| `MAC_CSC_LINK` | Base64-encoded Developer ID Application `.p12` backup, including its private key |
|
||||
| `MAC_CSC_KEY_PASSWORD` | Password protecting that `.p12` |
|
||||
| `APPLE_ID` | Apple ID used for notarization |
|
||||
| `APPLE_APP_SPECIFIC_PASSWORD` | App-specific password for that Apple ID |
|
||||
| `APPLE_TEAM_ID` | `JS6XN22HY6` |
|
||||
|
||||
The workflow maps the certificate secrets to electron-builder's `CSC_LINK` / `CSC_KEY_PASSWORD`. GitHub's built-in `GITHUB_TOKEN` supplies `GH_TOKEN` with job-scoped `contents: write`; no separate GitHub token secret is required. Signing credentials are present only for the packaging step, not the Next build. Never commit certificates, passwords, or `.env.local`.
|
||||
|
||||
## First release runbook
|
||||
|
||||
1. Push the implementation; run the manual Desktop release workflow.
|
||||
2. Human smoke on Windows/Linux: install/open, window buttons and dragging, sidebar at narrow widths, zoom, setup/keys, session/resume, native PDF/SVG verification and demo helper. macOS remains signed/notarized through the production path.
|
||||
3. Add macOS signing/notarization secrets.
|
||||
4. Bump `package.json` and the lockfile to the agreed release version; run local gates and push a matching `v...` tag.
|
||||
5. Wait for **all three** packaging jobs. Verify all installer assets and updater feeds are in the draft.
|
||||
6. Owner makes the repository public, publishes the complete draft, then checks the real GitHub update path using a prior installed version.
|
||||
|
||||
A full human pass is required before the first three-platform release. Subsequent content-only updates rely on the deterministic suite plus the per-OS build/boot-smoke matrix. Changes to `electron/`, native modules, packaging/updater configuration, or Electron itself call for another platform smoke pass.
|
||||
+7
-5
@@ -24,7 +24,7 @@ npm run lint # eslint — includes the import law (architecture §2)
|
||||
npm run typecheck # tsc --noEmit
|
||||
|
||||
npm run desktop:dev # next dev + Electron shell pointed at it
|
||||
npm run desktop:dist # build + package the desktop app (electron-builder)
|
||||
npm run desktop:dist # local signed/notarized macOS arm64 build (LH_NOTARY keychain)
|
||||
|
||||
# one-shot data scripts (run from the master tree only — data never crosses trees)
|
||||
npx tsx --env-file=.env.local scripts/import-notes.ts <dir> # bulk-import notes → graph
|
||||
@@ -33,6 +33,8 @@ npx tsx --env-file=.env.local scripts/replay-layout.ts # re-derive layout
|
||||
npx tsx scripts/sync-pricing.ts # regenerate core/prices.ts tables
|
||||
```
|
||||
|
||||
For Windows/Linux packages and the three-OS CI/release flow, see [`desktop-release.md`](desktop-release.md). Native renderer binaries are traced per platform, so release builds run on the target OS, not as cross-builds from macOS.
|
||||
|
||||
## Environment (`.env.local`)
|
||||
|
||||
Secrets and machine-specific paths only. Model choice and thinking budgets are the user's, not the environment's — BYOK model slots live in **settings** (`models` section: strong/cheap/vision, provider + model id + optional thinking level; per-role and per-chat overrides ride on top). The extraction batch threshold is a settings knob too (`graph.extractionChunkTokens`, default 4096, floor 500).
|
||||
@@ -42,9 +44,9 @@ Secrets and machine-specific paths only. Model choice and thinking budgets are t
|
||||
| `OPENROUTER_API_KEY` | required — all LLM calls go through OpenRouter |
|
||||
| `GOOGLE_SEARCH_API_KEY` + `GOOGLE_CSE_ID` | researcher's `web_search` (Google Custom Search) |
|
||||
| `DEEPGRAM_API_KEY` | optional — dictation (STT token minting; audio streams browser→Deepgram, never through the server) |
|
||||
| `CHROME_PATH` | optional explicit Chrome path for headless verification (puppeteer-core uses system Chrome; nothing is downloaded) |
|
||||
| `CHROME_PATH` | optional system Chrome fallback for demo verification; the app's own browser helper is primary (nothing is downloaded) |
|
||||
|
||||
The packaged desktop app takes keys from `userData/auth.json` (BYOK, mode 0600, injected into the server child as env only).
|
||||
The packaged desktop app takes keys from `userData/auth.json` (BYOK, mode 0600, read/written directly by the server via `LH_AUTH_FILE`). PDFs render natively with pdfjs/Skia; SVGs use resvg.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -117,7 +119,7 @@ src/
|
||||
subagents/ researcher · demo-builder · visual-maker
|
||||
· pdf-reader,
|
||||
extraction pipeline (extractor/auditor + merger handoff),
|
||||
runner (async protocol), headless Chrome helper,
|
||||
runner (async protocol), native renderers + browser-service helper,
|
||||
abort registry
|
||||
server/chat-handler.ts THE turn composition root (liveness · steers · persist · stream)
|
||||
server/turn-service.ts turn seams: persist · extraction trigger
|
||||
@@ -154,7 +156,7 @@ data/ chats/ · graph/ (nodes/chains/sessions/coverage
|
||||
|
||||
**The formal plan tree (author-once).** The teacher authors the session's dependency tree with one schema-validated tool — `plan_init`, whose mermaid-subset input is parsed deterministically (line-numbered errors) and validated as a DAG. The tree persists in the chat record; progress is never mutated — `current`/`visited`/upcoming are **derived** from tagged quiz calls in the transcript, and the panel renders that derivation.
|
||||
|
||||
**The async sub-agent protocol (ack-and-steer).** `research` / `demo` / `visual` are server-executed tools: the call resolves immediately mid-turn with a fire-and-forget ack and the teacher keeps teaching; a background runner works in parallel (the client polls `/api/activity` and `/api/jobs`). On completion the runner builds a **steer message** — a user-role `UIMessage` whose text part is the model-facing result and whose `data-subagent-artifact` part carries the full result for the client — and the **wake pipeline** delivers it: a per-chat serialized queue waits for the turn to end and no question to be pending, then fires a server-side teacher turn that weaves the result into the lesson. Delivery is exactly-once (deterministic steer ids, delivered-flag checks at every claim point); stalled jobs are recovered on read; aborted jobs deliver an abort steer. The client adopts the wake turn through the same `turnInFlight` machinery as returning mid-stream (busy shimmer, composer gate) and renders steers as artifacts — never a bubble. Sub-agents **verify, never one-shot**: the demo-builder headlessly executes its HTML in system Chrome before delivery; the visual-maker renders each SVG and looks at it (vision-capable model — the `vision` slot in settings).
|
||||
**The async sub-agent protocol (ack-and-steer).** `research` / `demo` / `visual` are server-executed tools: the call resolves immediately mid-turn with a fire-and-forget ack and the teacher keeps teaching; a background runner works in parallel (the client polls `/api/activity` and `/api/jobs`). On completion the runner builds a **steer message** — a user-role `UIMessage` whose text part is the model-facing result and whose `data-subagent-artifact` part carries the full result for the client — and the **wake pipeline** delivers it: a per-chat serialized queue waits for the turn to end and no question to be pending, then fires a server-side teacher turn that weaves the result into the lesson. Delivery is exactly-once (deterministic steer ids, delivered-flag checks at every claim point); stalled jobs are recovered on read; aborted jobs deliver an abort steer. The client adopts the wake turn through the same `turnInFlight` machinery as returning mid-stream (busy shimmer, composer gate) and renders steers as artifacts — never a bubble. Sub-agents **verify, never one-shot**: the demo-builder executes its HTML in the app's own Chromium browser-service (system Chrome fallback) before delivery; the visual-maker rasterizes each SVG natively and looks at it (vision-capable model — the `vision` slot in settings).
|
||||
|
||||
**The knowledge graph (two channels, one writer).** The write channel: the extraction pipeline — extractor (LLM: sees the source — the session's teacher prose or a notes chunk — plus its own source's digest (**THE GRAPH SO FAR**: propositions + chains already extracted from this source, ids verbatim — digest-aware redesign, 2026-09-30 evening: the extractor IS the assembler, referencing existing ids instead of restating, leaning on established conclusions as premises); fully trusted; its deliverable is core ideas + reasoning chains: premises that jointly entail a conclusion, submitted as chains and stored as chains — `chains.jsonl` is the truth, edges a derived view; owner redesign + chains-primary, 2026-09-30) → auditor (LLM, an agentic loop that never sees the source: explores the existing graph through `find` / `list_sessions` / `list_propositions` tools and stamps each proposition with an identity verdict — link to an existing node (equivalence bias: paraphrases merge) or create) → merger (`mergeFindings`, deterministic: applies the verdicts, link-appends phrasings/provenance, resolves and dedupes chains — identical arguments merge under one `arg_` id; then the chain-simplification pass strips provably redundant derivations — prefer-fewest-premises + ancestry-cover — checkpoints coverage) → layout settle. A source's batches run strictly in order (each extractor continues the digest of every earlier merge). No quiz outcomes or understanding assessments live in the graph (2026-09-27 cleanup); practice (`/practice`) consumes the graph as feedback-only problem sets and writes nothing back. Triggered when a chat's **uncovered tokens** (message-view fingerprints, `coverage.jsonl`) cross `graph.extractionChunkTokens` (a settings knob, default 8192, floor 500 — read fresh per turn); failure checkpoints nothing, the next trigger re-runs. The same pipeline serves the **notes front door** (`POST /api/graph/import`: any `*.md`/`*.txt` becomes a source; re-imports are idempotent by path hash). The read channel: `GET /api/graph` derives the snapshot from the ledgers — a pure read (`deriveView` rebuilds the lossy image: each chain an implication node `⟹` with premise→implication→conclusion arrows, so two arguments for one conclusion stay two diamonds); the home view is a hand-rolled Canvas 2D renderer (one rAF loop, dirty-flag idle, d3-zoom camera, zoom-gated labels, cluster hulls) over a d3-force layout, with server-persisted render settings and drag-committed positions. Clusters are **birth-session groupings**: a node joins the cluster of the session that birthed it (a chat, or an imported notes file as a file-kind session), derived at read — no embedding pass, no naming pass. The teacher reaches the graph on its own terms through the gated prober tools (`list_sessions` / `list_propositions`, the `agent.graphTools` setting) — there is no automatic context injection.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user