From ce7594be32aebe79c91663a2f7e5d09b2b2532af Mon Sep 17 00:00:00 2001 From: guilhermexp <150675893+guilhermexp@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:21:22 -0300 Subject: [PATCH 01/21] Add OpenClaw runtime information This commit introduces a new section in the "Other" settings tab to display information about the OpenClaw runtime. It now shows: - The current OpenClaw version fetched from the gateway. - Whether OpenClaw updates are managed by the app (bundled runtime) or if direct updates are supported (dev-checkout). - An "Update now" button is available when direct updates are supported. Add OpenClaw runtime information Fetch and display the current OpenClaw version from the gateway. Show whether OpenClaw updates are managed by the app or can be performed directly, and provide a button to trigger updates if supported. --- .../src/ui/settings/OtherTab.test.tsx | 172 ++++++++++++++++++ desktop/renderer/src/ui/settings/OtherTab.tsx | 118 ++++++++++++ .../settings-visual-standardization.test.ts | 5 +- desktop/src/main/ipc/config-ipc.ts | 15 ++ desktop/src/preload.ts | 1 + desktop/src/shared/desktop-bridge-contract.ts | 6 + desktop/src/shared/ipc-channels.ts | 1 + 7 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 desktop/renderer/src/ui/settings/OtherTab.test.tsx diff --git a/desktop/renderer/src/ui/settings/OtherTab.test.tsx b/desktop/renderer/src/ui/settings/OtherTab.test.tsx new file mode 100644 index 0000000000..539d6ada9d --- /dev/null +++ b/desktop/renderer/src/ui/settings/OtherTab.test.tsx @@ -0,0 +1,172 @@ +// @vitest-environment jsdom +import React from "react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { cleanup, render, screen, waitFor } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; + +import { OtherTab } from "./OtherTab"; + +const mockDispatch = vi.fn(); +const mockGetDesktopApiOrNull = vi.fn(); +const mockSetTerminalSidebar = vi.fn(); +const mockFetch = vi.fn(); +const mockGatewayRequest = vi.fn(); + +vi.mock("@store/hooks", () => ({ + useAppDispatch: () => mockDispatch, + useAppSelector: (selector: (state: unknown) => unknown) => + selector({ + auth: { mode: "self-managed" }, + }), +})); + +vi.mock("@gateway/context", () => ({ + useGatewayRpc: () => ({ + connected: true, + request: mockGatewayRequest, + }), +})); + +vi.mock("@ipc/desktopApi", () => ({ + getDesktopApiOrNull: () => mockGetDesktopApiOrNull(), +})); + +vi.mock("@analytics", () => ({ + optInRenderer: vi.fn(), + optOutRenderer: vi.fn(), + getCurrentUserId: vi.fn(() => "user-1"), +})); + +vi.mock("@shared/toast", () => ({ + errorToMessage: (err: unknown) => String(err), +})); + +vi.mock("@shared/kit", () => ({ + ConfirmDialog: () => null, +})); + +vi.mock("@shared/utils/openExternal", () => ({ + openExternal: vi.fn(), +})); + +vi.mock("@shared/hooks/useTerminalSidebarVisible", () => ({ + useTerminalSidebarVisible: () => [false, mockSetTerminalSidebar] as const, +})); + +vi.mock("./RestoreBackupModal", () => ({ + RestoreBackupModal: () => null, +})); + +vi.mock("@store/slices/auth/authSlice", () => ({ + authActions: { + clearAuthState: () => ({ type: "auth/clearAuthState" }), + setMode: (mode: string) => ({ type: "auth/setMode", payload: mode }), + }, + clearAuth: () => ({ type: "auth/clearAuth" }), + persistMode: vi.fn(), +})); + +describe("OtherTab", () => { + beforeEach(() => { + cleanup(); + vi.clearAllMocks(); + vi.stubGlobal("fetch", mockFetch); + mockFetch.mockResolvedValue({ + ok: true, + json: async () => ({ serverVersion: "2026.3.26" }), + }); + mockGatewayRequest.mockReset().mockResolvedValue({ ok: true, result: { status: "ok" } }); + mockGetDesktopApiOrNull.mockReturnValue({ + getLaunchAtLogin: vi.fn(async () => ({ enabled: false })), + analyticsGet: vi.fn(async () => ({ enabled: false })), + getOpenclawRuntimeInfo: vi.fn(async () => ({ + runtime: "bundled", + updateSupported: false, + reason: "Bundled OpenClaw is updated through OpenSpace app updates.", + })), + getGatewayInfo: vi.fn(async () => ({ + state: { + kind: "ready", + port: 31337, + logsDir: "/tmp/logs", + url: "http://127.0.0.1:31337/", + token: "test-token", + }, + })), + }); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + cleanup(); + }); + + it("shows the current OpenClaw version from the legacy dashboard bootstrap source", async () => { + render( + + + + ); + + await waitFor(() => { + expect(mockFetch).toHaveBeenCalledWith( + "http://127.0.0.1:31337/__openclaw/control-ui-config.json" + ); + }); + + expect(await screen.findByText("OpenClaw version")).toBeTruthy(); + expect(await screen.findByText("v2026.3.26")).toBeTruthy(); + }); + + it("shows bundled update ownership when OpenClaw updates are managed by the app", async () => { + render( + + + + ); + + expect(await screen.findByText("OpenClaw update")).toBeTruthy(); + expect( + await screen.findByText("Bundled OpenClaw is updated through OpenSpace app updates.") + ).toBeTruthy(); + expect(screen.getByRole("button", { name: "Managed by app" }).hasAttribute("disabled")).toBe( + true + ); + }); + + it("runs update.run when the runtime supports direct OpenClaw updates", async () => { + const onError = vi.fn(); + mockGetDesktopApiOrNull.mockReturnValue({ + getLaunchAtLogin: vi.fn(async () => ({ enabled: false })), + analyticsGet: vi.fn(async () => ({ enabled: false })), + getOpenclawRuntimeInfo: vi.fn(async () => ({ + runtime: "dev-checkout", + updateSupported: true, + reason: null, + })), + getGatewayInfo: vi.fn(async () => ({ + state: { + kind: "ready", + port: 31337, + logsDir: "/tmp/logs", + url: "http://127.0.0.1:31337/", + token: "test-token", + }, + })), + }); + + render( + + + + ); + + const button = await screen.findByRole("button", { name: "Update now" }); + button.click(); + + await waitFor(() => { + expect(mockGatewayRequest).toHaveBeenCalledWith("update.run", {}); + }); + expect(onError).toHaveBeenCalledWith(null); + }); +}); diff --git a/desktop/renderer/src/ui/settings/OtherTab.tsx b/desktop/renderer/src/ui/settings/OtherTab.tsx index 7bda20f924..bd7579f92a 100644 --- a/desktop/renderer/src/ui/settings/OtherTab.tsx +++ b/desktop/renderer/src/ui/settings/OtherTab.tsx @@ -17,6 +17,14 @@ import s from "./OtherTab.module.css"; import pkg from "../../../../package.json"; type SecurityLevel = "balanced" | "permissive"; +type ControlUiBootstrapConfig = { + serverVersion?: string; +}; +type OpenclawRuntimeInfo = { + runtime: "bundled" | "dev-checkout"; + updateSupported: boolean; + reason: string | null; +}; type ExecApprovalsFile = { version: 1; @@ -72,6 +80,11 @@ function applySecurityLevel(file: ExecApprovalsFile, level: SecurityLevel): Exec export function OtherTab({ onError }: { onError: (msg: string | null) => void }) { const [launchAtStartup, setLaunchAtStartup] = React.useState(false); const [analyticsEnabled, setAnalyticsEnabled] = React.useState(false); + const [openclawVersion, setOpenclawVersion] = React.useState(null); + const [openclawRuntimeInfo, setOpenclawRuntimeInfo] = React.useState( + null + ); + const [openclawUpdateBusy, setOpenclawUpdateBusy] = React.useState(false); const [resetBusy, setResetBusy] = React.useState(false); const [resetConfirmOpen, setResetConfirmOpen] = React.useState(false); const [terminalSidebar, setTerminalSidebar] = useTerminalSidebarVisible(); @@ -103,6 +116,67 @@ export function OtherTab({ onError }: { onError: (msg: string | null) => void }) void api.analyticsGet().then((res) => setAnalyticsEnabled(res.enabled)); }, []); + React.useEffect(() => { + const api = getDesktopApiOrNull(); + if (!api?.getOpenclawRuntimeInfo) { + return; + } + let cancelled = false; + void api + .getOpenclawRuntimeInfo() + .then((info) => { + if (!cancelled) { + setOpenclawRuntimeInfo(info); + } + }) + .catch(() => { + if (!cancelled) { + setOpenclawRuntimeInfo(null); + } + }); + return () => { + cancelled = true; + }; + }, []); + + React.useEffect(() => { + const api = getDesktopApiOrNull(); + if (!api?.getGatewayInfo) { + return; + } + let cancelled = false; + void api + .getGatewayInfo() + .then(async (info) => { + const gatewayState = info.state; + if (cancelled || gatewayState?.kind !== "ready") { + return; + } + const baseUrl = gatewayState.url.endsWith("/") ? gatewayState.url : `${gatewayState.url}/`; + const bootstrapUrl = new URL("__openclaw/control-ui-config.json", baseUrl).toString(); + const response = await fetch(bootstrapUrl); + if (!response.ok) { + throw new Error(`Failed to load OpenClaw version (${response.status})`); + } + const payload = (await response.json()) as ControlUiBootstrapConfig; + const version = + typeof payload.serverVersion === "string" && payload.serverVersion.trim() + ? payload.serverVersion.trim() + : null; + if (!cancelled) { + setOpenclawVersion(version); + } + }) + .catch(() => { + if (!cancelled) { + setOpenclawVersion(null); + } + }); + return () => { + cancelled = true; + }; + }, []); + React.useEffect(() => { if (!gw.connected) return; void gw @@ -198,6 +272,21 @@ export function OtherTab({ onError }: { onError: (msg: string | null) => void }) } }, [onError]); + const handleOpenclawUpdate = React.useCallback(async () => { + if (!openclawRuntimeInfo?.updateSupported) { + return; + } + onError(null); + setOpenclawUpdateBusy(true); + try { + await gw.request("update.run", {}); + } catch (err) { + onError(errorToMessage(err)); + } finally { + setOpenclawUpdateBusy(false); + } + }, [gw, onError, openclawRuntimeInfo?.updateSupported]); + const handleCreateBackup = React.useCallback(async () => { const api = getDesktopApiOrNull(); if (!api?.createBackup) { @@ -250,6 +339,35 @@ export function OtherTab({ onError }: { onError: (msg: string | null) => void }) Version OpenSpace v{appVersion} +
+ OpenClaw version + + {openclawVersion ? `v${openclawVersion}` : "Unavailable"} + +
+
+
+ OpenClaw update + + {openclawRuntimeInfo?.reason ?? + "Checks and installs the current OpenClaw runtime when supported."} + +
+ {openclawRuntimeInfo?.updateSupported ? ( + + ) : ( + + )} +
Auto start diff --git a/desktop/renderer/src/ui/settings/settings-visual-standardization.test.ts b/desktop/renderer/src/ui/settings/settings-visual-standardization.test.ts index 1b31158fb7..78e8fa0fa5 100644 --- a/desktop/renderer/src/ui/settings/settings-visual-standardization.test.ts +++ b/desktop/renderer/src/ui/settings/settings-visual-standardization.test.ts @@ -59,9 +59,10 @@ describe("settings visual standardization", () => { expect(richSelectCss).not.toContain("#ffb300"); }); - it("removes legacy OpenClaw wording from Other tab copy", () => { + it("keeps Other tab branding clean while allowing the runtime OpenClaw version row", () => { const tsx = readSettingsFile("./OtherTab.tsx"); - expect(tsx).not.toContain("OpenClaw"); + expect(tsx).toContain("OpenClaw version"); + expect(tsx).not.toContain("OpenClaw state"); }); }); diff --git a/desktop/src/main/ipc/config-ipc.ts b/desktop/src/main/ipc/config-ipc.ts index eb6f28b308..af7304a1c6 100644 --- a/desktop/src/main/ipc/config-ipc.ts +++ b/desktop/src/main/ipc/config-ipc.ts @@ -76,4 +76,19 @@ export function registerConfigHandlers(params: ConfigHandlerParams) { ipcMain.handle("get-app-version", () => { return { version: app.getVersion() }; }); + + ipcMain.handle("get-openclaw-runtime-info", () => { + if (app.isPackaged) { + return { + runtime: "bundled" as const, + updateSupported: false, + reason: "Bundled OpenClaw is updated through OpenSpace app updates.", + }; + } + return { + runtime: "dev-checkout" as const, + updateSupported: true, + reason: null, + }; + }); } diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 240fb81d85..69b9bd115d 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -82,6 +82,7 @@ const api: OpenclawDesktopApi = { setLaunchAtLogin: async (enabled: boolean) => ipcRenderer.invoke(IPC.launchAtLoginSet, { enabled }), getAppVersion: async () => ipcRenderer.invoke(IPC.getAppVersion), + getOpenclawRuntimeInfo: async () => ipcRenderer.invoke(IPC.getOpenclawRuntimeInfo), fetchReleaseNotes: async (version: string, owner: string, repo: string) => ipcRenderer.invoke(IPC.fetchReleaseNotes, { version, owner, repo }), checkForUpdate: async () => ipcRenderer.invoke(IPC.updaterCheck), diff --git a/desktop/src/shared/desktop-bridge-contract.ts b/desktop/src/shared/desktop-bridge-contract.ts index 7301329372..1269ef6dd7 100644 --- a/desktop/src/shared/desktop-bridge-contract.ts +++ b/desktop/src/shared/desktop-bridge-contract.ts @@ -88,6 +88,11 @@ export interface OpenclawDesktopApi { getLaunchAtLogin: () => Promise<{ enabled: boolean }>; setLaunchAtLogin: (enabled: boolean) => Promise<{ ok: true }>; getAppVersion: () => Promise<{ version: string }>; + getOpenclawRuntimeInfo: () => Promise<{ + runtime: "bundled" | "dev-checkout"; + updateSupported: boolean; + reason: string | null; + }>; fetchReleaseNotes: ( version: string, owner: string, @@ -219,6 +224,7 @@ export const DESKTOP_BRIDGE_KEYS: ReadonlyArray = [ "getLaunchAtLogin", "setLaunchAtLogin", "getAppVersion", + "getOpenclawRuntimeInfo", "fetchReleaseNotes", "checkForUpdate", "downloadUpdate", diff --git a/desktop/src/shared/ipc-channels.ts b/desktop/src/shared/ipc-channels.ts index 48caace5c9..b9bf035dcd 100644 --- a/desktop/src/shared/ipc-channels.ts +++ b/desktop/src/shared/ipc-channels.ts @@ -55,6 +55,7 @@ export const IPC = { launchAtLoginGet: "launch-at-login-get", launchAtLoginSet: "launch-at-login-set", getAppVersion: "get-app-version", + getOpenclawRuntimeInfo: "get-openclaw-runtime-info", // Updater fetchReleaseNotes: "fetch-release-notes", From 4da6a3d739d73cbf12101f1c7e2632a48114dd67 Mon Sep 17 00:00:00 2001 From: guilhermexp <150675893+guilhermexp@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:30:29 -0300 Subject: [PATCH 02/21] chore(desktop): align release pipeline and embedded runtime --- .github/workflows/build-desktop.yml | 109 ++++++-- desktop/README.md | 62 ++++- desktop/docs/gateway-message-metadata.md | 30 +-- desktop/docs/release-secrets-checklist.md | 240 ++++++++++++++++++ .../docs/telegram-manual-setup-electron.md | 2 +- desktop/package.json | 10 +- .../chat/components/ChatComposer.module.css | 3 +- .../ui/chat/components/MessageMeta.test.tsx | 2 +- .../src/ui/settings/OtherTab.test.tsx | 1 - .../account-models/AccountModelsTab.tsx | 7 +- .../settings/account-models/InlineApiKey.tsx | 6 +- .../ui/settings/connectors/ConnectorsTab.tsx | 5 +- .../src/ui/settings/skills/SkillsGrid.tsx | 5 +- .../src/ui/shared/models/modelPresentation.ts | 7 +- .../sidebar/session-sidebar-activity.test.tsx | 1 - .../src/ui/sidebar/useSessionActivity.test.ts | 4 +- desktop/renderer/src/ui/styles/base.css | 3 +- desktop/scripts/build-dmg-from-app.sh | 6 +- desktop/scripts/build-memo-runtime.mjs | 2 +- .../configure-github-release-secrets.sh | 102 ++++++++ ...der.afterAllArtifactBuild-notarize-dmg.cjs | 10 +- .../electron-builder.afterSign-notarize.cjs | 6 +- desktop/scripts/prepare-gh-runtime.mjs | 2 +- desktop/scripts/prepare-gog-credentials.mjs | 23 +- desktop/scripts/prepare-gog-runtime.mjs | 2 +- desktop/scripts/prepare-jq-runtime.mjs | 2 +- desktop/scripts/prepare-memo-runtime.mjs | 2 +- .../scripts/prepare-obsidian-cli-runtime.mjs | 2 +- desktop/scripts/prepare-openclaw-bundle.mjs | 8 +- desktop/scripts/prepare-remindctl-runtime.mjs | 2 +- .../scripts/prepare-whisper-cli-runtime.mjs | 2 +- desktop/scripts/release.sh | 21 +- desktop/src/main/gateway/extra-models.test.ts | 8 +- desktop/src/main/gog/ipc.ts | 8 +- desktop/src/main/ipc/gh-ipc.ts | 2 +- desktop/src/main/ipc/memo-ipc.ts | 5 +- desktop/src/main/ipc/obsidian-ipc.ts | 2 +- desktop/src/main/ipc/remindctl-ipc.ts | 2 +- desktop/src/main/openclaw/paths.ts | 4 +- desktop/src/main/window/mainWindow.ts | 2 +- openclaw | 2 +- 41 files changed, 591 insertions(+), 133 deletions(-) create mode 100644 desktop/docs/release-secrets-checklist.md create mode 100644 desktop/scripts/configure-github-release-secrets.sh diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index dea5e22c6a..9fdc33b734 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -1,4 +1,4 @@ -name: Build Desktop App +name: Desktop CI on: push: @@ -9,7 +9,8 @@ on: workflow_dispatch: jobs: - build-mac: + verify: + if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/v') runs-on: macos-latest steps: - uses: actions/checkout@v4 @@ -25,50 +26,116 @@ jobs: version: 10 - name: Install OpenClaw deps - run: cd openclaw && pnpm install + run: cd openclaw && pnpm install --frozen-lockfile - name: Build OpenClaw run: cd openclaw && pnpm build && pnpm ui:build - name: Install Desktop deps - run: cd desktop && npm install + run: cd desktop && npm ci + + - name: Run Desktop checks + run: cd desktop && npm run check:ci - name: Prepare OpenClaw bundle - run: cd desktop && npm run prepare:openclaw + run: cd desktop && npm run prepare:openclaw:ci - name: Prepare runtimes - run: cd desktop && npm run prepare:node + run: cd desktop && npm run prepare:runtimes - name: Build Desktop run: cd desktop && npm run build:all - - name: Package + - name: Package smoke build env: CSC_IDENTITY_AUTO_DISCOVERY: false - run: cd desktop && npx electron-builder --publish never + run: cd desktop && npx electron-builder --publish never --mac zip - - name: Upload artifacts + - name: Upload smoke artifacts uses: actions/upload-artifact@v4 with: - name: openspace-mac-${{ github.sha }} - path: desktop/release/*.{dmg,zip} + name: openspace-smoke-mac-${{ github.sha }} + path: | + desktop/release/*.zip + desktop/release/*.dmg + desktop/release/*.blockmap + desktop/release/*.yml if-no-files-found: warn release: - needs: build-mac if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + electron_args: --mac zip + artifact_name: openspace-release-mac + - os: windows-latest + electron_args: --win nsis + artifact_name: openspace-release-win + runs-on: ${{ matrix.os }} permissions: contents: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: actions/setup-node@v4 with: - name: openspace-mac-${{ github.sha }} - path: artifacts + node-version: "22" - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + - uses: pnpm/action-setup@v4 with: - draft: true - files: artifacts/* - generate_release_notes: true + version: 10 + + - name: Install OpenClaw deps + run: cd openclaw && pnpm install --frozen-lockfile + + - name: Build OpenClaw + run: cd openclaw && pnpm build && pnpm ui:build + + - name: Install Desktop deps + run: cd desktop && npm ci + + - name: Run Desktop checks + run: cd desktop && npm run check:ci + + - name: Prepare OpenClaw bundle + run: cd desktop && npm run prepare:openclaw:ci + + - name: Prepare runtimes + run: cd desktop && npm run prepare:runtimes + + - name: Build Desktop + run: cd desktop && npm run build:all + + - name: Package and publish release assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CSC_IDENTITY_AUTO_DISCOVERY: false + CSC_LINK: ${{ secrets.CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} + CSC_NAME: ${{ secrets.CSC_NAME }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + NOTARYTOOL_PROFILE: ${{ secrets.NOTARYTOOL_PROFILE }} + NOTARYTOOL_KEY: ${{ secrets.NOTARYTOOL_KEY }} + NOTARYTOOL_KEY_ID: ${{ secrets.NOTARYTOOL_KEY_ID }} + NOTARYTOOL_ISSUER: ${{ secrets.NOTARYTOOL_ISSUER }} + NOTARIZE: ${{ vars.OPENSPACE_NOTARIZE }} + run: cd desktop && npx electron-builder ${{ matrix.electron_args }} --publish always + + - name: Upload published artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact_name }}-${{ github.sha }} + path: | + desktop/release/*.zip + desktop/release/*.dmg + desktop/release/*.exe + desktop/release/*.blockmap + desktop/release/*.yml + if-no-files-found: warn diff --git a/desktop/README.md b/desktop/README.md index 543a19fbf3..0bc90b18e3 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -1,16 +1,17 @@ -# Atomic Bot Desktop +# OpenSpace Desktop -Cross-platform Electron desktop app for [Atomic Bot](https://atomicbot.ai) — an AI assistant that makes things for you. +Cross-platform Electron desktop app for OpenSpace. **Platforms:** macOS (arm64 / x64) · Windows (x64) ## Quick Start ```bash -# Install dependencies (from repo root) -pnpm install +# From repo root, install monorepo + desktop deps +cd openclaw && pnpm install +cd ../desktop && npm install -# Prepare bundled runtimes +# Prepare bundled runtimes used by the packaged app npm run prepare:all # Build & launch in dev mode @@ -22,7 +23,7 @@ npm run dev The app follows a standard Electron multi-process model with a clear separation of concerns: ``` -apps/electron-desktop/ +desktop/ ├── src/ # Main process + preload │ ├── main.ts # Electron entry point │ ├── preload.ts # Context bridge (window.openclawDesktop) @@ -128,13 +129,52 @@ Channel names live in `ipc-channels.ts`; the full API surface is declared in `de | `npm run format` | Check formatting (Prettier) | | `npm run format:fix` | Fix formatting (Prettier) | +## Release & Auto-update + +The packaged app uses `electron-updater` with the GitHub provider configured in `package.json`. Release builds are published to the GitHub Releases page of `guilhermexp/openspace`. + +Manual download assets: + +- macOS: `.dmg` +- Windows: `.exe` + +Auto-update assets: + +- macOS: `.zip`, `.blockmap`, `latest-mac.yml` +- Windows: `.exe`, `.blockmap`, `latest.yml` + +If you mirror installers on an external site, keep GitHub Releases as the canonical update feed unless you also migrate the app to a generic update provider. + +Guia de secrets e variables: + +- [release-secrets-checklist.md](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/docs/release-secrets-checklist.md) + +Tag-driven release flow: + +1. Run `npm run release patch|minor|major` inside `desktop/` +2. Push the branch and tag +3. GitHub Actions builds macOS + Windows artifacts and publishes them to the draft release +4. Publish the draft release after both platform jobs complete + +Optional signing/notarization secrets for release CI: + +- `CSC_LINK` +- `CSC_KEY_PASSWORD` +- `CSC_NAME` +- `APPLE_ID` +- `APPLE_APP_SPECIFIC_PASSWORD` +- `APPLE_TEAM_ID` +- `NOTARYTOOL_PROFILE` or `NOTARYTOOL_KEY` + `NOTARYTOOL_KEY_ID` + `NOTARYTOOL_ISSUER` +- repo variable `OPENSPACE_NOTARIZE=1` to enable notarization steps + ## Environment Variables -| Variable | Context | Description | -| ----------------------------- | -------- | ------------------------------------------------------- | -| `VITE_BACKEND_URL` | Renderer | Override API backend URL (set in `renderer/.env.local`) | -| `OPENCLAW_DESKTOP_NODE_BIN` | Main | Custom Node binary path for development | -| `CSC_IDENTITY_AUTO_DISCOVERY` | Build | Set to `false` to skip code signing | +| Variable | Context | Description | +| ---------------------------------------------------------------- | -------- | ------------------------------------------------------- | +| `VITE_BACKEND_URL` | Renderer | Override API backend URL (set in `renderer/.env.local`) | +| `OPENCLAW_DESKTOP_NODE_BIN` | Main | Custom Node binary path for development | +| `CSC_IDENTITY_AUTO_DISCOVERY` | Build | Set to `false` to skip code signing | +| `OPENCLAW_GOG_OAUTH_CLIENT_SECRET_PATH` / `..._B64` / `..._JSON` | Build | Stage the gog OAuth client secret for packaged builds | ## Adding New Features diff --git a/desktop/docs/gateway-message-metadata.md b/desktop/docs/gateway-message-metadata.md index 10c7ad32c5..e877bba31e 100644 --- a/desktop/docs/gateway-message-metadata.md +++ b/desktop/docs/gateway-message-metadata.md @@ -139,7 +139,7 @@ CSS em: `ui/src/styles/chat/grouped.css` (classes `.msg-meta__*`) ### 4.1 Parser de historico -Arquivo: `apps/electron-desktop/renderer/src/store/slices/chat/chat-utils.ts` +Arquivo: `desktop/renderer/src/store/slices/chat/chat-utils.ts` O `parseHistoryMessages()` extrai `usage` e `model` de cada mensagem raw: @@ -163,7 +163,7 @@ const messageModel = ### 4.2 Tipos -Arquivo: `apps/electron-desktop/renderer/src/store/slices/chat/chat-types.ts` +Arquivo: `desktop/renderer/src/store/slices/chat/chat-types.ts` ```typescript type UiMessageUsage = { @@ -182,12 +182,12 @@ type UiMessage = { ### 4.3 Componente de renderizacao -Arquivo: `apps/electron-desktop/renderer/src/ui/chat/components/MessageMeta.tsx` +Arquivo: `desktop/renderer/src/ui/chat/components/MessageMeta.tsx` Renderiza inline abaixo de cada mensagem do assistente: ``` -Atomic Bot 13:40 ↑4k ↓371 R26k W212 claude-opus-4-6 +OpenSpace 13:40 ↑4k ↓371 R26k W212 claude-opus-4-6 ``` Props recebidas diretamente da mensagem: @@ -200,7 +200,7 @@ Fallback: se `model` nao vier na mensagem, le do config (`agents.defaults.model. ### 4.4 Integracao no ChatMessageList -Arquivo: `apps/electron-desktop/renderer/src/ui/chat/components/ChatMessageList.tsx` +Arquivo: `desktop/renderer/src/ui/chat/components/ChatMessageList.tsx` ```tsx { @@ -226,19 +226,19 @@ Arquivo: `apps/electron-desktop/renderer/src/ui/chat/components/ChatMessageList. ↓ [Redux Store] → UiMessage com usage/model ↓ -[MessageMeta] → renderiza inline: "Atomic Bot 13:40 ↑4k ↓371 R26k claude-opus-4-6" +[MessageMeta] → renderiza inline: "OpenSpace 13:40 ↑4k ↓371 R26k claude-opus-4-6" ``` --- ## 6. Referencia de arquivos -| Arquivo | Responsabilidade | -| ------------------------------------------------------------------------------ | ---------------------------------------- | -| `ui/src/ui/chat/grouped-render.ts` | Control UI — extrai e renderiza metadata | -| `ui/src/styles/chat/grouped.css` | Control UI — estilos `.msg-meta__*` | -| `apps/electron-desktop/renderer/src/store/slices/chat/chat-types.ts` | Tipos `UiMessageUsage`, `UiMessage` | -| `apps/electron-desktop/renderer/src/store/slices/chat/chat-utils.ts` | Parser `parseHistoryMessages()` | -| `apps/electron-desktop/renderer/src/ui/chat/components/MessageMeta.tsx` | Componente de metadata inline | -| `apps/electron-desktop/renderer/src/ui/chat/components/MessageMeta.module.css` | Estilos do MessageMeta | -| `apps/electron-desktop/renderer/src/ui/chat/components/ChatMessageList.tsx` | Integracao do MessageMeta nas mensagens | +| Arquivo | Responsabilidade | +| ---------------------------------------------------------------- | ---------------------------------------- | +| `ui/src/ui/chat/grouped-render.ts` | Control UI — extrai e renderiza metadata | +| `ui/src/styles/chat/grouped.css` | Control UI — estilos `.msg-meta__*` | +| `desktop/renderer/src/store/slices/chat/chat-types.ts` | Tipos `UiMessageUsage`, `UiMessage` | +| `desktop/renderer/src/store/slices/chat/chat-utils.ts` | Parser `parseHistoryMessages()` | +| `desktop/renderer/src/ui/chat/components/MessageMeta.tsx` | Componente de metadata inline | +| `desktop/renderer/src/ui/chat/components/MessageMeta.module.css` | Estilos do MessageMeta | +| `desktop/renderer/src/ui/chat/components/ChatMessageList.tsx` | Integracao do MessageMeta nas mensagens | diff --git a/desktop/docs/release-secrets-checklist.md b/desktop/docs/release-secrets-checklist.md new file mode 100644 index 0000000000..71c02e5d01 --- /dev/null +++ b/desktop/docs/release-secrets-checklist.md @@ -0,0 +1,240 @@ +# OpenSpace Release Secrets Checklist + +Este guia cobre os valores esperados pela pipeline de release do Electron em: + +- [.github/workflows/build-desktop.yml](/Users/guilhermevarela/Documents/Projetos/openspace/.github/workflows/build-desktop.yml) +- [desktop/package.json](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/package.json) +- [desktop/scripts/electron-builder.afterSign-notarize.cjs](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/scripts/electron-builder.afterSign-notarize.cjs) +- [desktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjs](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/scripts/electron-builder.afterAllArtifactBuild-notarize-dmg.cjs) + +## Onde configurar no GitHub + +No repositório `guilhermexp/openspace`: + +1. `Settings` +2. `Secrets and variables` +3. `Actions` + +Crie os itens abaixo. + +## Mínimo para auto-update funcionar + +Sem assinatura/notarização, o fluxo de auto-update já funciona se a release por tag conseguir publicar os assets no GitHub Release. + +`Secrets` + +- nenhum extra além do `GITHUB_TOKEN` padrão do Actions + +`Variables` + +- nenhuma obrigatória + +Resultado: + +- macOS publica `.zip`, `.blockmap`, `latest-mac.yml` +- Windows publica `.exe`, `.blockmap`, `latest.yml` +- download manual continua vindo de `.dmg` e `.exe` + +## Recomendado para release distribuível + +### 1. Code signing + +`Secrets` + +- `CSC_LINK` +- `CSC_KEY_PASSWORD` +- `CSC_NAME` + +### 2. Notarização macOS + +`Variable` + +- `OPENSPACE_NOTARIZE=1` + +`Secrets` + +- use `NOTARYTOOL_PROFILE` + +ou: + +- `NOTARYTOOL_KEY` +- `NOTARYTOOL_KEY_ID` +- `NOTARYTOOL_ISSUER` + +## O que cada secret faz + +`CSC_LINK` + +- certificado de assinatura que o `electron-builder` importa no runner + +`CSC_KEY_PASSWORD` + +- senha do arquivo do certificado + +`CSC_NAME` + +- nome exato da identidade +- exemplo comum: + `Developer ID Application: Guilherme Varela (TEAM_ID)` + +`NOTARYTOOL_PROFILE` + +- profile salvo no keychain para `xcrun notarytool` +- costuma ser mais chato de automatizar em CI hospedada + +`NOTARYTOOL_KEY` + +- conteúdo ou path da chave `.p8` de App Store Connect API key + +`NOTARYTOOL_KEY_ID` + +- Key ID da API key + +`NOTARYTOOL_ISSUER` + +- Issuer ID da App Store Connect API key + +`OPENSPACE_NOTARIZE` + +- se for `1`, os hooks de notarização rodam +- se estiver vazio ou ausente, assinatura pode ocorrer, mas notarização é pulada + +## Como gerar o `CSC_LINK` + +O caminho mais comum é exportar o certificado como `.p12` no Keychain do macOS e converter para base64. + +### Exportar o certificado + +No `Keychain Access`: + +1. localize o certificado `Developer ID Application` +2. exporte como `.p12` +3. defina uma senha forte + +### Converter para base64 + +```bash +base64 -i OpenSpace-DeveloperID.p12 | pbcopy +``` + +Cole o valor copiado em `CSC_LINK`. + +Se preferir o formato explícito: + +```text +data:application/x-pkcs12;base64, +``` + +Na prática, o `electron-builder` costuma aceitar o base64 direto. + +### Descobrir o `CSC_NAME` + +```bash +security find-identity -p codesigning -v +``` + +Procure a linha da identidade `Developer ID Application` e copie o nome completo entre aspas. + +## Como gerar os dados de notarização + +### Opção recomendada para CI: App Store Connect API key + +No Apple Developer / App Store Connect: + +1. crie uma API key para notarização +2. baixe o arquivo `.p8` +3. guarde: + - `KEY_ID` + - `ISSUER_ID` + - arquivo `.p8` + +No GitHub: + +- `NOTARYTOOL_KEY` + pode ser o conteúdo da `.p8` +- `NOTARYTOOL_KEY_ID` + valor do `KEY_ID` +- `NOTARYTOOL_ISSUER` + valor do `ISSUER_ID` + +Se quiser guardar o conteúdo da chave em base64 e reconstruir no workflow no futuro, isso também funciona, mas o workflow atual espera a variável já pronta para o `notarytool`. + +## Secrets opcionais que não bloqueiam release + +O pipeline também tolera ausência das credenciais do `gog`. + +Você só precisa disso se quiser empacotar o segredo OAuth do `gog`: + +- `OPENCLAW_GOG_OAUTH_CLIENT_SECRET_PATH` +- ou `OPENCLAW_GOG_OAUTH_CLIENT_SECRET_B64` +- ou `OPENCLAW_GOG_OAUTH_CLIENT_SECRET_JSON` + +Sem isso: + +- a build não quebra +- apenas o segredo do `gog` não é pré-embutido + +## Checklist de configuração + +Para colocar em produção com update automático: + +1. criar tag `vX.Y.Z` +2. garantir que o workflow publique no GitHub Release +3. verificar que a release draft contém: + - `.dmg` + - `.zip` + - `latest-mac.yml` + - `.exe` + - `latest.yml` + - `.blockmap` + +Para ficar assinado/notarizado: + +1. configurar `CSC_LINK` +2. configurar `CSC_KEY_PASSWORD` +3. configurar `CSC_NAME` +4. configurar `OPENSPACE_NOTARIZE=1` +5. configurar `NOTARYTOOL_KEY` ou `NOTARYTOOL_PROFILE` + +## Bootstrap automático via script + +Se você já tiver os arquivos locais, pode subir quase tudo com um comando: + +```bash +REPO=guilhermexp/openspace \ +CSC_P12_PATH=~/certs/OpenSpace-DeveloperID.p12 \ +CSC_KEY_PASSWORD='SUA_SENHA_DO_P12' \ +CSC_NAME='Developer ID Application: Seu Nome (TEAM_ID)' \ +NOTARYTOOL_KEY_PATH=~/certs/AuthKey_ABC123XYZ.p8 \ +NOTARYTOOL_KEY_ID='ABC123XYZ' \ +NOTARYTOOL_ISSUER='00000000-0000-0000-0000-000000000000' \ +OPENSPACE_NOTARIZE=1 \ +bash desktop/scripts/configure-github-release-secrets.sh +``` + +Script: + +- [configure-github-release-secrets.sh](/Users/guilhermevarela/Documents/Projetos/openspace/desktop/scripts/configure-github-release-secrets.sh) + +## Smoke test final + +Depois da primeira release: + +1. instale uma versão antiga localmente +2. publique uma versão nova com tag maior +3. abra o app empacotado +4. confirme que: + - o banner de update aparece + - o download acontece + - o restart instala a nova versão + +## Observação importante + +Hoje o app usa `GitHub provider` para updates. + +Isso significa: + +- você pode hospedar `DMG` e `EXE` no seu site para download manual +- mas o update automático continua lendo os assets do GitHub Release + +Se quiser mover o feed de update para seu site, o próximo passo é trocar de `GitHub provider` para `generic provider`. diff --git a/desktop/docs/telegram-manual-setup-electron.md b/desktop/docs/telegram-manual-setup-electron.md index 2a76ac3530..dfdc066fab 100644 --- a/desktop/docs/telegram-manual-setup-electron.md +++ b/desktop/docs/telegram-manual-setup-electron.md @@ -270,7 +270,7 @@ Formato relevante do snapshot em [types.ts](/Users/guilhermevarela/Documents/Pro type ConfigSnapshot = { hash?: string; config?: unknown; -} +}; ``` ### `config.patch` diff --git a/desktop/package.json b/desktop/package.json index 86074a3023..86f463f682 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -36,7 +36,7 @@ "fetch:gh": "node scripts/fetch-gh-runtime.mjs", "fetch:whisper-cli": "node scripts/fetch-whisper-cli-runtime.mjs", "build:memo": "node scripts/build-memo-runtime.mjs", - "prepare:gog:credentials": "node --env-file=.env scripts/prepare-gog-credentials.mjs", + "prepare:gog:credentials": "node --env-file-if-exists=.env scripts/prepare-gog-credentials.mjs", "prepare:gog": "node scripts/prepare-gog-runtime.mjs", "prepare:jq": "node scripts/prepare-jq-runtime.mjs", "prepare:memo": "node scripts/prepare-memo-runtime.mjs", @@ -54,11 +54,11 @@ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p renderer/tsconfig.typecheck.json && tsc -p tsconfig.tools.json", "check:ci": "npm run lint && npm run prettier:check && npm run typecheck", "precommit": "npm run format:fix && npm run lint && npm run typecheck", - "dist:full": "npm run prepare:openclaw && npm run prepare:node && cross-env CSC_IDENTITY_AUTO_DISCOVERY=false npm run dist -- --publish never && open release/*.dmg", - "dist:local": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false npm run dist -- --publish never", - "dist:local:win": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false ELECTRON_BUILDER_COMPRESSION_LEVEL=1 npm run dist -- --publish never --win", + "dist:full": "npm run prepare:all && cross-env CSC_IDENTITY_AUTO_DISCOVERY=false npm run dist -- --publish never && open release/*.dmg", + "dist:local": "npm run prepare:all && cross-env CSC_IDENTITY_AUTO_DISCOVERY=false npm run dist -- --publish never", + "dist:local:win": "npm run prepare:all && cross-env CSC_IDENTITY_AUTO_DISCOVERY=false ELECTRON_BUILDER_COMPRESSION_LEVEL=1 npm run dist -- --publish never --win", "dist": "npm run build:all && electron-builder", - "dist:env": "npm run build:all && node --env-file=.env ./node_modules/electron-builder/out/cli/cli.js", + "dist:env": "npm run prepare:all && npm run build:all && node --env-file-if-exists=.env ./node_modules/electron-builder/out/cli/cli.js", "dist:env:local": "CSC_IDENTITY_AUTO_DISCOVERY=false npm run dist:env -- --publish never", "release": "bash scripts/release.sh", "test": "vitest run --config vitest.config.ts", diff --git a/desktop/renderer/src/ui/chat/components/ChatComposer.module.css b/desktop/renderer/src/ui/chat/components/ChatComposer.module.css index 281ad2c907..6cde761f21 100644 --- a/desktop/renderer/src/ui/chat/components/ChatComposer.module.css +++ b/desktop/renderer/src/ui/chat/components/ChatComposer.module.css @@ -216,8 +216,7 @@ border-radius: 18px; border: 1px solid var(--border); background: var(--surface-primary); - box-shadow: - 0 18px 36px rgba(0, 0, 0, 0.18); + box-shadow: 0 18px 36px rgba(0, 0, 0, 0.18); } .UiChatComposerButtonBlock { diff --git a/desktop/renderer/src/ui/chat/components/MessageMeta.test.tsx b/desktop/renderer/src/ui/chat/components/MessageMeta.test.tsx index 0d09884e39..a94ae04352 100644 --- a/desktop/renderer/src/ui/chat/components/MessageMeta.test.tsx +++ b/desktop/renderer/src/ui/chat/components/MessageMeta.test.tsx @@ -35,7 +35,7 @@ describe("MessageMeta", () => { cacheRead: 15_000, cacheWrite: 0, }} - />, + /> ); expect(screen.getByText("8% ctx")).not.toBeNull(); diff --git a/desktop/renderer/src/ui/settings/OtherTab.test.tsx b/desktop/renderer/src/ui/settings/OtherTab.test.tsx index 539d6ada9d..ed68668e9f 100644 --- a/desktop/renderer/src/ui/settings/OtherTab.test.tsx +++ b/desktop/renderer/src/ui/settings/OtherTab.test.tsx @@ -1,5 +1,4 @@ // @vitest-environment jsdom -import React from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { cleanup, render, screen, waitFor } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; diff --git a/desktop/renderer/src/ui/settings/account-models/AccountModelsTab.tsx b/desktop/renderer/src/ui/settings/account-models/AccountModelsTab.tsx index bd24597618..def79a9104 100644 --- a/desktop/renderer/src/ui/settings/account-models/AccountModelsTab.tsx +++ b/desktop/renderer/src/ui/settings/account-models/AccountModelsTab.tsx @@ -137,12 +137,7 @@ export function AccountModelsTab(props: { autoSelectedRef.current = true; setProviderFilter(activeProviderKey); } - }, [ - activeProviderKey, - isPaidMode, - providerFilter, - setProviderFilter, - ]); + }, [activeProviderKey, isPaidMode, providerFilter, setProviderFilter]); const providerOptions: RichOption[] = React.useMemo( () => diff --git a/desktop/renderer/src/ui/settings/account-models/InlineApiKey.tsx b/desktop/renderer/src/ui/settings/account-models/InlineApiKey.tsx index 607bb555dc..14415d6cc8 100644 --- a/desktop/renderer/src/ui/settings/account-models/InlineApiKey.tsx +++ b/desktop/renderer/src/ui/settings/account-models/InlineApiKey.tsx @@ -320,11 +320,7 @@ export function InlineApiKey(props: { void handlePaste()}> Paste - + {validating ? "Validating…" : busy ? "Saving…" : "Save"}
diff --git a/desktop/renderer/src/ui/settings/connectors/ConnectorsTab.tsx b/desktop/renderer/src/ui/settings/connectors/ConnectorsTab.tsx index 7a4959fd46..39fb0273e8 100644 --- a/desktop/renderer/src/ui/settings/connectors/ConnectorsTab.tsx +++ b/desktop/renderer/src/ui/settings/connectors/ConnectorsTab.tsx @@ -188,7 +188,6 @@ export function ConnectorsTab(props: {
{CONNECTORS.map((connector) => { const status = statuses[connector.id]; - const isInteractive = status !== "coming-soon"; const statusLabel = getConnectorStatusLabel(status); const actionLabel = getConnectorActionLabel(status); const actionDisabled = status === "coming-soon"; @@ -219,9 +218,7 @@ export function ConnectorsTab(props: {
{connector.name}
- {statusLabel ? ( -
{statusLabel}
- ) : null} + {statusLabel ?
{statusLabel}
: null}