Skip to content

Commit aa4f58a

Browse files
Merge pull request #6 from off-grid-ai/release-core
release-core → main: media seek fix, clipboard→Pro, chat image fix, settings + version
2 parents 5e48f0b + cc0f8d5 commit aa4f58a

96 files changed

Lines changed: 8577 additions & 725 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 38 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,26 @@ concurrency:
1212
group: release
1313
cancel-in-progress: false
1414

15-
# macOS-only for now: core (free) + pro, both Developer-ID signed + notarized.
16-
# Pro source lives in the separate private repo off-grid-ai/desktop-pro (gitignored
17-
# here), cloned into pro/ at build time via the PRO_REPO_TOKEN secret. Windows is
18-
# parked (bundled llama-server fix owned elsewhere) — re-add a windows-2022 job when
19-
# it lands.
15+
# macOS-only for now. ONE signed + notarized DMG: the build bundles the private pro
16+
# source (cloned from off-grid-ai/desktop-pro) and is license-gated at runtime —
17+
# free = unlicensed (pro tabs locked), a valid Keygen key unlocks in place (no
18+
# re-download). The ASAR integrity fuse (electron-builder.yml) makes a tampered
19+
# app.asar refuse to load — our local-first anti-tamper lever (we don't phone home).
20+
# Actions are pinned to commit SHAs. Windows is parked.
2021
jobs:
21-
# Bump the version once (on Ubuntu) and push it, so every build job builds the
22-
# same version. [skip ci] keeps it from re-triggering.
22+
# Bump the version once and push it so the build job builds that exact commit.
2323
version:
2424
if: "!contains(github.event.head_commit.message, '[skip ci]')"
2525
runs-on: ubuntu-latest
2626
outputs:
2727
version: ${{ steps.bump.outputs.version }}
2828
sha: ${{ steps.bump.outputs.sha }}
2929
steps:
30-
- uses: actions/checkout@v4
30+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
3131
with:
3232
token: ${{ secrets.GITHUB_TOKEN }}
3333
fetch-depth: 0
34-
- uses: actions/setup-node@v4
34+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
3535
with:
3636
node-version: '20'
3737
- name: Bump patch version
@@ -45,102 +45,61 @@ jobs:
4545
git add package.json package-lock.json
4646
git commit -m "chore: bump version to $VERSION [skip ci]"
4747
git push
48-
# Pin the build to THIS exact commit, not floating main — otherwise a
49-
# concurrent push's bump can advance main and the build publishes a
50-
# mismatched/duplicate release (the version race that tangled v0.0.20/.21).
48+
# Pin the build to THIS exact commit, not floating main (avoids the
49+
# concurrent-bump version race).
5150
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
5251
53-
# CORE (free) build: OFFGRID_FORCE_CORE=1 makes the build ignore any pro/ that might
54-
# be present → __OFFGRID_PRO__ false (in CI pro/ isn't there anyway; belt-and-braces).
55-
# Publishes to the default update channel (latest-mac.yml).
56-
build-mac-core:
52+
# Single build: pro code bundled, license-gated. Publishes to the default update
53+
# channel (latest-mac.yml).
54+
build-mac:
5755
needs: version
5856
runs-on: macos-latest
5957
steps:
60-
- uses: actions/checkout@v4
58+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
6159
with:
62-
ref: ${{ needs.version.outputs.sha }} # the exact bumped commit, not floating main
60+
ref: ${{ needs.version.outputs.sha }} # the exact bumped commit
6361
fetch-depth: 0
6462
lfs: true # resources/bin/* (llama-server, sd-cli, whisper, ffmpeg, dylibs)
65-
# are Git LFS — without this, CI bundles 131-byte pointer stubs
66-
# and every runtime spawn fails with ENOEXEC.
63+
# are Git LFS — without this CI bundles pointer stubs and every
64+
# runtime spawn fails with ENOEXEC.
6765
- name: Pull LFS binaries
6866
run: git lfs pull
69-
- uses: actions/setup-node@v4
67+
# Private pro source into pro/ → __OFFGRID_PRO__ true. The checkout action
68+
# injects the token as an http.extraheader (never in a clone URL) and scrubs
69+
# it afterwards — safer than embedding ${TOKEN} in a git URL.
70+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
7071
with:
71-
node-version: '20'
72-
cache: 'npm'
73-
- name: Install dependencies
74-
run: npm ci
75-
- name: Build (typecheck + bundle) — CORE
76-
env:
77-
OFFGRID_FORCE_CORE: '1'
78-
run: npm run build
79-
- name: Package & publish CORE (signed + notarized)
80-
env:
81-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
82-
CSC_LINK: ${{ secrets.CSC_LINK }}
83-
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
84-
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY }}
85-
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
86-
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
87-
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
88-
run: |
89-
# notarytool needs the API key as a file; write it from the secret.
90-
printf '%s' "$APPLE_API_KEY_CONTENT" > "$RUNNER_TEMP/AuthKey.p8"
91-
export APPLE_API_KEY="$RUNNER_TEMP/AuthKey.p8"
92-
npx electron-builder --mac \
93-
-c.productName="Off Grid AI" \
94-
-c.appId="co.getoffgridai.desktop" \
95-
-c.dmg.artifactName='OffGrid-core-${version}.${ext}' \
96-
--publish always
97-
98-
# PRO build: clone the private pro source into pro/ → __OFFGRID_PRO__ true. Pro is
99-
# license-gated at runtime (a leaked DMG is inert without a valid Keygen key), so
100-
# publishing it publicly is safe. Publishes to a SEPARATE update channel (pro-mac.yml)
101-
# so core and pro auto-updates never feed each other the wrong binary.
102-
build-mac-pro:
103-
needs: version
104-
runs-on: macos-latest
105-
steps:
106-
- uses: actions/checkout@v4
107-
with:
108-
ref: ${{ needs.version.outputs.sha }}
109-
fetch-depth: 0
110-
lfs: true
111-
- name: Pull LFS binaries
112-
run: git lfs pull
113-
- name: Clone private pro source into pro/
114-
env:
115-
PRO_REPO_TOKEN: ${{ secrets.PRO_REPO_TOKEN }}
116-
run: |
117-
rm -rf pro
118-
git clone --depth 1 \
119-
"https://x-access-token:${PRO_REPO_TOKEN}@github.com/off-grid-ai/desktop-pro.git" pro
120-
rm -rf pro/.git # don't nest a repo inside the build tree
121-
- uses: actions/setup-node@v4
72+
repository: off-grid-ai/desktop-pro
73+
token: ${{ secrets.PRO_REPO_TOKEN }}
74+
path: pro
75+
fetch-depth: 1
76+
- name: Drop nested .git from pro/
77+
run: rm -rf pro/.git # don't nest a repo inside the build tree
78+
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
12279
with:
12380
node-version: '20'
12481
cache: 'npm'
12582
- name: Install dependencies
12683
run: npm ci
127-
- name: Build (typecheck + bundle) — PRO
84+
- name: Build (typecheck + bundle)
12885
run: npm run build
129-
- name: Package & publish PRO (signed + notarized, pro channel)
86+
- name: Package, sign, notarize & publish (DMG + auto-update metadata)
13087
env:
131-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
13288
CSC_LINK: ${{ secrets.CSC_LINK }}
13389
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
13490
APPLE_API_KEY_CONTENT: ${{ secrets.APPLE_API_KEY }}
13591
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
13692
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
13793
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
94+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
13895
run: |
96+
# notarytool needs the API key as a file; write it from the secret.
13997
printf '%s' "$APPLE_API_KEY_CONTENT" > "$RUNNER_TEMP/AuthKey.p8"
14098
export APPLE_API_KEY="$RUNNER_TEMP/AuthKey.p8"
99+
# ONE signed + notarized build, published to the default update channel
100+
# (the flow that shipped 0.0.22–0.0.24). electron-builder uploads the DMG
101+
# (human download) plus the zip + latest-mac.yml + blockmap that
102+
# electron-updater needs, so existing users keep auto-updating.
141103
npx electron-builder --mac \
142-
-c.productName="Off Grid AI Pro" \
143-
-c.appId="co.getoffgridai.desktop.pro" \
144-
-c.dmg.artifactName='OffGrid-pro-${version}.${ext}' \
145-
-c.publish.channel=pro \
104+
-c.dmg.artifactName='OffGrid-${version}.${ext}' \
146105
--publish always

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,7 @@ resources/bin/mflux/
4242
# Local env (telemetry/keys) — never commit
4343
.env
4444
.env.local
45+
test-results/
46+
47+
# Marketing material (dev.to drafts, emails, assets) — publishing, not app code
48+
/marketing/

CLAUDE.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Off Grid AI Desktop — agent guide
2+
3+
This is **Off Grid AI Desktop** — an Electron (macOS) desktop app. The product name is always **"Off Grid AI Desktop"** (never "Off Grid Desktop", "My Memories", etc.) — in window titles, OAuth client names, about screens, everywhere.
4+
5+
## Design — DESKTOP-FIRST, Off Grid brand
6+
7+
Full design doc: **`docs/DESIGN.md`**. The essentials, which OVERRIDE any mobile-first or monochrome assumptions:
8+
9+
- **Desktop-first.** Wide canvas: multi-column layouts, dense lists/tables, side panels, detail screens, hover affordances. Never design mobile-first or for narrow viewports. (The mobile app is a separate product with its own guide.)
10+
- **Typeface: Menlo** (monospace) everywhere — terminal/brutalist.
11+
- **Accent: emerald**`#34D399` (dark) / `#059669` (light). THE accent for primary actions, active states, links, success. (Tailwind `green-500/400` is an acceptable stand-in but prefer the exact tokens.)
12+
- **Base:** black / `#0A0A0A` + white; neutral grays for surfaces/borders/text tiers. Flat, sharp, dense.
13+
- Tokens: `@offgrid/design`. Brand canon: `mobile/docs/design/DESIGN_PHILOSOPHY_SYSTEM.md` (brand only — desktop *layout* follows `docs/DESIGN.md`, desktop-first).
14+
- Real brand logos (Simple Icons), no decorative tiles behind them; no gradients; no emojis in the UI.
15+
16+
## What this app is
17+
18+
A private, **local-first** layer that **sees** (screen capture → OCR → entities), **remembers** (observations/entities/memory), helps you **reflect** (mind-share / day), and **acts** (MCP connectors + approval-gated actions). Everything is processed on-device by a bundled local LLM (llama.cpp + gemma); nothing routes through a server we own.
19+
20+
Roadmap: **`ROADMAP_DESKTOP.md`** (this repo) and `../shared/ROADMAP.md`.
21+
22+
## Stack
23+
24+
Electron 39 + React 19 + Tailwind v4 + electron-vite; better-sqlite3; bundled `llama-server` (port 8439), `whisper.cpp`, `ffmpeg`, `sharp`. Local LLM is a reasoning model — pass `chat_template_kwargs:{enable_thinking:false}` + grammar-constrained `response_format` for structured output. userData dir is `~/Library/Application Support/Off Grid AI Desktop`.
25+
26+
## Conventions
27+
28+
- Verify changes with `npx tsc --noEmit` (main: `tsconfig.node.json`, web: `tsconfig.web.json`) before declaring done.
29+
- Main-process changes need an app restart; renderer changes hot-reload.
30+
- Don't over-restart — it interrupts capture.
31+
32+
## Testing — test every approved behavior change in the same pass
33+
34+
When iterating (a request, a fix, a tweak the user just confirmed), add a test that captures that specific behavior **as part of the same change** — a regression test that would fail before the change and pass after. This applies to bug fixes (test the exact broken case), new branches/conditions (cover each), and copy/contract changes other code depends on. Do not defer tests to "later" or a separate commit.
35+
36+
- **Unit tests** — vitest, `src/**/*.test.ts` (run `npm test`). Keep logic pure and Electron-free so it's testable: extract decision logic into a no-import module and test that (see `model-sizing.ts`, `search-ranking.ts` + their `__tests__/`). DB/Electron-bound code (anything importing `getDB`, `vision`, etc.) can't be unit-tested directly — pull the pure part out.
37+
- **Regression guards for prompts/contracts** — when a fix lives in a prompt or string contract, assert it by reading the source (see `extract-prompt.test.ts`, which guards the observation-confabulation fix).
38+
- **E2E** — Playwright Electron tour in `e2e/` (`npm run test:e2e`), DOM-driven, fresh temp profile, `OFFGRID_PRO=0`. Assert new surfaces render.
39+
- Before declaring a change done: `npx tsc --noEmit -p tsconfig.node.json && npx tsc --noEmit -p tsconfig.web.json && npm test` — fix failures first.
40+
41+
## Reuse before building — check the inventory FIRST
42+
43+
This is a hard rule, not a preference. **Before writing ANY new component, panel, modal, hook, or service, first search the existing inventory** (`grep -rn` `src/renderer/src/components/`, `ui/`, the relevant screen folder) for something that already does it, and **reuse it**. Do NOT create a new variant of a thing that already exists.
44+
45+
- If something close exists, **extend it with a prop** — never fork a parallel copy. Two surfaces showing the same kind of thing (a viewer, a modal, a card, a search box, a preview pane) MUST use the same component. Parallel versions cause visual + behavioural drift (e.g. don't build a new centered modal when the image **Lightbox** overlay already exists — reuse that layout).
46+
- Only build new when nothing fits, and say why.
47+
- UI follows the approved-library + brand-token rules in `docs/DESIGN.md`; icons are `@phosphor-icons/react` only (never lucide).
48+
49+
## Open core — pro feature code lives in the pro repo
50+
51+
**All code for pro features lives in the `desktop-pro` repo (`pro/`), never in core (`desktop`).** Core is public (AGPL); shipping pro source in it defeats open core. This is a hard rule, not a preference.
52+
53+
- A new pro feature: backend → `pro/main/`, UI → `pro/renderer/`, wired in via pro's `activateMain` / view-router — not core `index.ts`, not core `src/renderer`.
54+
- Core only carries the **inert shell** for a pro surface: a `proCatalog` entry + a `locked: !isPro` nav item that shows `UpgradeScreen`, or a dimmed `ProPlaceholder` in Settings. No pro business logic, handlers, or data flow in core.
55+
- Pro renderer reaches its IPC through the generic `proInvoke` / `proOn` passthrough — do NOT add per-feature namespaces to the core preload for pro features.
56+
- Shared, reusable **engines** (e.g. `@offgrid/clipboard`) stay in `shared/` and may be consumed by either tier; it's the desktop **pro integration** that must live in `pro/`.
57+
- `proEnabled()` (main) / `isPro` (renderer) gate activation; `OFFGRID_PRO=0` forces free. Gating alone is not enough — the source must also physically live in `pro/`.
58+
59+
**Settings sections follow the same rule.** A pro Settings section (proactive delivery, secretary/learned-prefs, identity, fleet console, etc.) is pro feature code — its component + logic live in `pro/renderer` and register into the core Settings screen via the section-registry seam (`pro/renderer/settings.ts` `registerProSettings` → core `registerSettingsSection`; core renders its own sections + all registered ones). Core must NOT hardcode pro section bodies in `Settings.tsx` gated by `isPro` — core only renders a dimmed `ProPlaceholder` for the locked preview when the section isn't registered (free build). Do not `if (isPro) <RealProSection/> : <ProPlaceholder/>` with the real section defined in core.
60+
61+
## Architecture & abstractions (SOLID)
62+
63+
Design to abstractions, not concrete types. When implementations are interchangeable (model backends, TTS/STT engines, image/diffusion runtimes, connectors), the rest of the app depends on one service/interface — never branch on a concrete type in UI/stores (`if (engine === 'kokoro')`, `instanceof X`). Push the decision behind the abstraction; adding an implementation should need zero changes to callers. Normalize capability gaps inside the service, not the UI.
64+
65+
## Copy & content standards
66+
67+
Any change to UI strings, docs, essays, or marketing copy follows the brand voice (`mobile/docs/brand_tone_voice.md`). Easy-to-miss rules: proof-first ("15-30 tok/s", not "fast"); privacy as mechanism ("runs in your Mac's RAM, nothing leaves the device", not "we value privacy"); no em dashes (use " - "), no curly quotes, no exclamation marks; banned words (revolutionary, seamless, empower, leverage, robust, comprehensive, crucial, delve, tapestry, testament, foster, showcase, enhance) and AI-slop phrases ("serves as", "stands as", "it's not X, it's Y") — say it plainly. No emojis in UI.

0 commit comments

Comments
 (0)