diff --git a/apps/desktop/electron/build-mode.cjs b/apps/desktop/electron/build-mode.cjs new file mode 100644 index 00000000000..e0611563042 --- /dev/null +++ b/apps/desktop/electron/build-mode.cjs @@ -0,0 +1,20 @@ +'use strict' + +/** + * build-mode.cjs — pure helper for the desktop's thin-vs-thick build mode. + * + * The desktop ships in two shapes: + * - thick (default): bundles the first-launch bootstrap installer, can + * spawn a local Hermes backend, and supports in-app self-update. + * - thin: no bootstrap, no local backend, no self-update. Connects ONLY + * to a remote gateway. Used for sandboxed/package-managed deployments + * (Flatpak, Snap, etc.) where the agent lives elsewhere. + * + * The esbuild bundler bakes this env var into the source code, so it's read at build time, not runtime. + */ + +function isThinClient() { + return process.env.HERMES_DESKTOP_BUILD_MODE === 'thin' +} + +module.exports = { isThinClient } diff --git a/apps/desktop/electron/build-mode.test.cjs b/apps/desktop/electron/build-mode.test.cjs new file mode 100644 index 00000000000..a93f5319597 --- /dev/null +++ b/apps/desktop/electron/build-mode.test.cjs @@ -0,0 +1,41 @@ +'use strict' + +const test = require('node:test') +const assert = require('node:assert/strict') + +// We test build-mode.cjs by controlling process.env directly. The module +// reads process.env.HERMES_DESKTOP_BUILD_MODE at call time (not import time), +// so we can mutate the env and re-require to exercise both modes. + +function freshModule() { + // Bust the require cache so the module re-evaluates with the current env. + delete require.cache[require.resolve('./build-mode.cjs')] + return require('./build-mode.cjs') +} + +test('isThinClient returns false by default (thick mode)', () => { + const prev = process.env.HERMES_DESKTOP_BUILD_MODE + delete process.env.HERMES_DESKTOP_BUILD_MODE + const { isThinClient } = freshModule() + assert.equal(isThinClient(), false) + process.env.HERMES_DESKTOP_BUILD_MODE = prev +}) + +test('isThinClient returns true when HERMES_DESKTOP_BUILD_MODE=thin', () => { + const prev = process.env.HERMES_DESKTOP_BUILD_MODE + process.env.HERMES_DESKTOP_BUILD_MODE = 'thin' + const { isThinClient } = freshModule() + assert.equal(isThinClient(), true) + process.env.HERMES_DESKTOP_BUILD_MODE = prev +}) + +test('isThinClient returns false for non-thin values', () => { + const prev = process.env.HERMES_DESKTOP_BUILD_MODE + process.env.HERMES_DESKTOP_BUILD_MODE = 'thick' + const { isThinClient } = freshModule() + assert.equal(isThinClient(), false) + process.env.HERMES_DESKTOP_BUILD_MODE = 'thick-client' + const { isThinClient: isThin2 } = freshModule() + assert.equal(isThin2(), false) + process.env.HERMES_DESKTOP_BUILD_MODE = prev +}) diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index 5d859bca649..c24a0d11592 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -24,6 +24,7 @@ const https = require('node:https') const path = require('node:path') const { pathToFileURL } = require('node:url') const { execFileSync, spawn } = require('node:child_process') +const { isThinClient } = require('./build-mode.cjs') const { installEmbedReferer } = require('./embed-referer.cjs') const { detectRemoteDisplay, isWindowsBinaryPathInWsl, isWslEnvironment } = require('./bootstrap-platform.cjs') const { runBootstrap } = require('./bootstrap-runner.cjs') @@ -1839,6 +1840,17 @@ async function resolveHealedBranch(updateRoot, branch) { } async function checkUpdates() { + // Thin client: no local checkout to git-pull, no bundled updater. Updates + // come from the package manager (Flatpak, Snap, etc.), not in-app self-update. + if (isThinClient()) { + return { + supported: false, + reason: 'thin-client', + message: 'Updates are managed by your package manager. This is a thin client build.', + fetchedAt: Date.now() + } + } + const updateRoot = resolveUpdateRoot() let { branch } = readDesktopUpdateConfig() const gitDir = path.join(updateRoot, '.git') @@ -1972,6 +1984,10 @@ let updateInFlight = false // updater isn't staged (e.g. a dev/source run that never went through the // installer); callers degrade gracefully. function resolveUpdaterBinary() { + // Thin client: no staged Tauri updater — the packaged app is managed + // externally. Returning null lets the existing callers degrade gracefully + // (the manual-command path), though applyUpdates already short-circuits. + if (isThinClient()) return null const name = IS_WINDOWS ? 'hermes-setup.exe' : 'hermes-setup' const candidate = path.join(HERMES_HOME, name) return fileExists(candidate) ? candidate : null @@ -2130,6 +2146,16 @@ async function releaseBackendLock(updateRoot, tag) { // Detection (checkUpdates / commit changelog / "N behind") stays in the UI; // only this apply action changed. async function applyUpdates(opts = {}) { + // Thin client: self-update is not supported. The packaged app is managed + // externally (Flatpak, Snap, etc.). + if (isThinClient()) { + return { + ok: false, + error: 'unsupported', + message: 'Self-update is not available in thin client builds. Use your package manager to update.' + } + } + if (updateInFlight) { throw new Error('An update is already in progress.') } @@ -2831,6 +2857,23 @@ function createActiveBackend(dashboardArgs) { } function resolveHermesBackend(dashboardArgs) { + // Thin client: no local backend, no bootstrap. The only valid path is a + // remote gateway connection. Returning the bootstrap-needed sentinel here + // would kick off install.ps1, which the thin build doesn't ship. Instead + // return a dedicated sentinel so startHermes() can produce a clear error + // directing the user to configure a remote gateway. + if (isThinClient()) { + return { + kind: 'thin-client-no-local', + label: 'Thin client build — remote gateway required', + command: null, + args: dashboardArgs, + bootstrap: false, + env: {}, + shell: false + } + } + // 1. Explicit override -- HERMES_DESKTOP_HERMES_ROOT points at a developer // checkout. Honour it as-is (no bootstrap; the user is driving). const overrideRoot = process.env.HERMES_DESKTOP_HERMES_ROOT && path.resolve(process.env.HERMES_DESKTOP_HERMES_ROOT) @@ -2974,6 +3017,19 @@ function resolveHermesBackend(dashboardArgs) { } async function ensureRuntime(backend) { + // Thin client: resolveHermesBackend returned a sentinel telling us there + // is no local backend to spawn. Rather than crashing on a null command + // spawn, throw a clear error so startHermes() catches it and the boot- + // failure overlay surfaces the "configure a remote gateway" message. + if (backend.kind === 'thin-client-no-local') { + const err = new Error( + 'This is a thin client build with no bundled Hermes agent. ' + + 'Go to Settings → Gateway and configure a remote gateway URL.' + ) + err.isThinClientNoLocal = true + throw err + } + if (!backend.bootstrap) { await advanceBootProgress('runtime.external', `Using ${backend.label}`, 32) return applyWindowsNoConsoleSpawnHints(backend) @@ -4985,6 +5041,10 @@ async function testDesktopConnectionConfig(input = {}) { if (authMode !== 'oauth') { token = decryptDesktopSecret(block.token) } + } else if (isThinClient()) { + // Thin client: no local backend to test against. A "local" connection + // test is meaningless — there's no bundled agent to reach. + throw new Error('Local connection test is not available in thin client builds. Configure a remote gateway URL.') } else { const remote = (await resolveRemoteBackend(key)) || (await startHermes()) baseUrl = remote.baseUrl @@ -5401,6 +5461,17 @@ async function startHermes() { } } + // Thin client: the remote check above was the only path. If we get here, + // no remote was configured — refuse to spawn a local backend. + if (isThinClient()) { + const err = new Error( + 'No remote gateway configured. This thin client build requires a remote gateway. ' + + 'Go to Settings → Gateway to configure one.' + ) + err.isThinClientNoRemote = true + throw err + } + // Mutual exclusion with an in-app update (#50238). If this instance was // relaunched while the Tauri updater is still applying an update, spawning // a local backend now re-locks the venv shim and gets killed by the @@ -6120,6 +6191,10 @@ ipcMain.on('hermes:pet-overlay:control', (_event, payload) => { mainWindow.webContents.send('hermes:pet-overlay:control', payload) }) ipcMain.handle('hermes:bootstrap:reset', async () => { + // Thin client: no bootstrap to reset. Just reload the window. + if (isThinClient()) { + return { ok: true } + } // Renderer's "Reload and retry" path. Clear the latched failure and // reset connection state so the next startHermes() call restarts the // full backend flow (including a fresh runBootstrap pass). @@ -6140,6 +6215,10 @@ ipcMain.handle('hermes:bootstrap:reset', async () => { return { ok: true } }) ipcMain.handle('hermes:bootstrap:repair', async () => { + // Thin client: no local install to repair. + if (isThinClient()) { + return { ok: true } + } // Forceful repair: drop the bootstrap-complete marker so the next // startHermes() re-runs the full installer (refreshing a broken/partial // venv), and clear any latched failure + live connection. The renderer @@ -7157,6 +7236,23 @@ function uninstallVenvPython() { } async function getUninstallSummary() { + // Thin client: no local agent install to uninstall. Return a minimal + // summary so the settings UI can show "nothing to remove" instead of + // probing for a venv that doesn't exist. + if (isThinClient()) { + return { + hermes_home: HERMES_HOME, + agent_installed: false, + gui_installed: true, + source_built_artifacts: [], + packaged_app_paths: [], + userdata_dir: app.getPath('userData'), + userdata_exists: true, + platform: process.platform, + probe: 'thin-client' + } + } + const py = uninstallVenvPython() const agentRoot = ACTIVE_HERMES_ROOT // Fast JS-side fallback used when the agent venv is gone (lite client) or the @@ -7329,6 +7425,11 @@ async function runDesktopUninstall(mode) { ipcMain.handle('hermes:uninstall:summary', async () => getUninstallSummary()) ipcMain.handle('hermes:uninstall:run', async (_event, payload) => { + // Thin client: no local agent to uninstall. The packaged app is managed + // by the OS package manager. + if (isThinClient()) { + return { ok: false, error: 'unsupported', message: 'Uninstall is handled by your package manager in thin client builds.' } + } const mode = payload && typeof payload === 'object' ? payload.mode : payload return runDesktopUninstall(String(mode || '')) }) @@ -7421,6 +7522,7 @@ function registerDeepLinkProtocol() { // whole new app instead of routing into the running one. const _gotSingleInstanceLock = app.requestSingleInstanceLock() if (!_gotSingleInstanceLock) { + console.log("Hermes Desktop is already running, exiting.") app.quit() } else { app.on('second-instance', (_event, argv) => { diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 6d0ca01f33f..6dc1b635632 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -18,11 +18,13 @@ "profile:main": "wait-on http://127.0.0.1:5174 && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron --inspect=9229 .", "profile:main:cpu": "wait-on http://127.0.0.1:5174 && cross-env XCURSOR_SIZE=24 NODE_OPTIONS=--cpu-prof HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .", "start": "npm run build && electron .", - "build": "node scripts/assert-root-install.cjs && node scripts/write-build-stamp.cjs && node scripts/stage-native-deps.cjs && tsc -b && vite build && node scripts/bundle-electron-main.mjs && npm run postbuild", + "build": "node scripts/assert-root-install.cjs && node scripts/write-build-stamp.cjs && node scripts/stage-native-deps.cjs && tsc -b && vite build && node scripts/bundle-electron-main.mjs && npm run postbuild", + "build:thin": "cross-env HERMES_DESKTOP_BUILD_MODE=thin npm run build", "postbuild": "node scripts/assert-dist-built.cjs", "prebuilder": "node scripts/patch-electron-builder-mac-binary.cjs", "builder": "cross-env NODE_OPTIONS=--max-old-space-size=16384 node scripts/run-electron-builder.cjs", "pack": "npm run build && npm run builder -- --dir", + "pack:thin": "cross-env HERMES_DESKTOP_BUILD_MODE=thin npm run pack", "dist": "npm run build && npm run builder", "dist:mac": "npm run build && npm run builder -- --mac", "dist:mac:dmg": "npm run build && npm run builder -- --mac dmg", @@ -31,13 +33,15 @@ "dist:win:msi": "npm run build && npm run builder -- --win msi", "dist:win:nsis": "npm run build && npm run builder -- --win nsis", "dist:linux": "npm run build && npm run builder -- --linux AppImage deb rpm", + "dist:thin": "cross-env HERMES_DESKTOP_BUILD_MODE=thin npm run dist", + "dist:linux:thin": "cross-env HERMES_DESKTOP_BUILD_MODE=thin npm run dist:linux", "test:desktop": "node scripts/test-desktop.mjs", "test:desktop:all": "node scripts/test-desktop.mjs all", "test:desktop:dmg": "node scripts/test-desktop.mjs dmg", "test:desktop:nsis": "node scripts/test-desktop.mjs nsis", "test:desktop:existing": "node scripts/test-desktop.mjs existing", "test:desktop:fresh": "node scripts/test-desktop.mjs fresh", - "test:desktop:platforms": "node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-env.test.cjs electron/backend-probes.test.cjs electron/backend-ready.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/dashboard-token.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/link-title-window.test.cjs electron/workspace-cwd.test.cjs electron/fs-read-dir.test.cjs electron/git-root.test.cjs electron/git-worktree-ops.test.cjs electron/windows-child-process.test.cjs electron/update-remote.test.cjs electron/update-count.test.cjs electron/update-rebuild.test.cjs electron/update-marker.test.cjs electron/update-relaunch.test.cjs electron/windows-user-env.test.cjs electron/wsl-clipboard-image.test.cjs electron/titlebar-overlay-width.test.cjs electron/window-state.test.cjs", + "test:desktop:platforms": "node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-env.test.cjs electron/backend-probes.test.cjs electron/backend-ready.test.cjs electron/bootstrap-runner.test.cjs electron/build-mode.test.cjs electron/connection-config.test.cjs electron/dashboard-token.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/link-title-window.test.cjs electron/workspace-cwd.test.cjs electron/fs-read-dir.test.cjs electron/git-root.test.cjs electron/git-worktree-ops.test.cjs electron/windows-child-process.test.cjs electron/update-remote.test.cjs electron/update-count.test.cjs electron/update-rebuild.test.cjs electron/update-marker.test.cjs electron/update-relaunch.test.cjs electron/windows-user-env.test.cjs electron/wsl-clipboard-image.test.cjs electron/titlebar-overlay-width.test.cjs electron/window-state.test.cjs", "typecheck": "tsc -p . --noEmit", "lint": "eslint src/ electron/", "lint:fix": "eslint src/ electron/ --fix", diff --git a/apps/desktop/scripts/before-build.cjs b/apps/desktop/scripts/before-build.cjs index 673aca380d3..4f6bd0101f3 100644 --- a/apps/desktop/scripts/before-build.cjs +++ b/apps/desktop/scripts/before-build.cjs @@ -2,10 +2,32 @@ * Desktop bundles ship precompiled renderer assets. Returning false here tells * electron-builder to skip the node_modules collector/install step, which * avoids workspace dependency graph explosions and keeps packaging - * deterministic across environments. The Hermes Agent Python payload is no - * longer bundled; the Electron app fetches it at first launch via - * `install.ps1`'s stage protocol (Windows). See `electron/main.cjs`. + * deterministic across environments. + * + * In thin-client builds we also strip the install-stamp and native-deps + * extraResources entries — no bootstrap, no local PTY. */ -module.exports = async function beforeBuild() { +const path = require('node:path') + +const THIN_CLIENT = process.env.HERMES_DESKTOP_BUILD_MODE === 'thin' + +module.exports = async function beforeBuild(context) { + if (THIN_CLIENT && context.packager) { + // Strip install-stamp.json and native-deps from extraResources — neither + // exists in a thin build (write-build-stamp and stage-native-deps are + // skipped in build:thin). + const buildConfig = context.packager.config + if (Array.isArray(buildConfig.extraResources)) { + buildConfig.extraResources = buildConfig.extraResources.filter( + entry => { + const to = typeof entry === 'object' && entry ? entry.to : null + if (to === 'install-stamp.json' || to === 'native-deps') { + return false + } + return true + } + ) + } + } return false } diff --git a/apps/desktop/scripts/bundle-electron-main.mjs b/apps/desktop/scripts/bundle-electron-main.mjs index bb5b0ad061b..95ae0eacc2a 100644 --- a/apps/desktop/scripts/bundle-electron-main.mjs +++ b/apps/desktop/scripts/bundle-electron-main.mjs @@ -16,6 +16,11 @@ const root = resolve(here, '..') const entry = resolve(root, 'electron/main.cjs') const tmp = resolve(root, 'electron/main.bundled.cjs') +// Thin-client builds strip bootstrap, local backend, and self-update. +// Bake the flag into the bundle so build-mode.cjs's process.env read resolves +// to a string literal at runtime (no env var needed in the packaged app). +const thinClient = process.env.HERMES_DESKTOP_BUILD_MODE === 'thin' + await build({ entryPoints: [entry], bundle: true, @@ -24,6 +29,9 @@ await build({ target: 'node20', outfile: tmp, external: ['electron', 'node-pty'], + define: { + 'process.env.HERMES_DESKTOP_BUILD_MODE': JSON.stringify(thinClient ? 'thin' : '') + }, logLevel: 'info' }) diff --git a/apps/desktop/scripts/write-build-stamp.cjs b/apps/desktop/scripts/write-build-stamp.cjs index 72b978c5f9a..47f3f27cf37 100644 --- a/apps/desktop/scripts/write-build-stamp.cjs +++ b/apps/desktop/scripts/write-build-stamp.cjs @@ -1,4 +1,4 @@ -"use strict" +'use strict' /** * Writes apps/desktop/build/install-stamp.json with the git ref the desktop @@ -17,29 +17,30 @@ * } * * Source preference order: - * 1. CI env vars ($GITHUB_SHA / $GITHUB_REF_NAME) -- avoid edge cases with + * 1. BUILD_STAMP env var (json) + * 2. CI env vars ($GITHUB_SHA / $GITHUB_REF_NAME) -- avoid edge cases with * shallow clones, detached HEADs, etc. in CI. - * 2. Local `git rev-parse` against the parent repo (../..). + * 3. Local `git rev-parse` against the parent repo (../..). * * Dev / out-of-repo builds without git produce an explicit error rather than * silently writing an unstamped manifest -- the packaged app refuses to * bootstrap without a stamp. */ -const fs = require("fs") -const path = require("path") -const { execSync } = require("child_process") +const fs = require('fs') +const path = require('path') +const { execSync } = require('child_process') const STAMP_SCHEMA_VERSION = 1 -const DESKTOP_ROOT = path.resolve(__dirname, "..") -const REPO_ROOT = path.resolve(DESKTOP_ROOT, "..", "..") -const OUT_DIR = path.join(DESKTOP_ROOT, "build") -const OUT_FILE = path.join(OUT_DIR, "install-stamp.json") +const DESKTOP_ROOT = path.resolve(__dirname, '..') +const REPO_ROOT = path.resolve(DESKTOP_ROOT, '..', '..') +const OUT_DIR = path.join(DESKTOP_ROOT, 'build') +const OUT_FILE = path.join(OUT_DIR, 'install-stamp.json') function tryExec(cmd, opts) { try { - return execSync(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], ...opts }).trim() + return execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], ...opts }).trim() } catch { return null } @@ -53,52 +54,79 @@ function fromCI() { commit: sha, branch: branch, dirty: false, // CI builds from a checkout-of-ref by definition - source: "ci" + source: 'ci' } } function fromLocalGit() { - const sha = tryExec("git rev-parse HEAD", { cwd: REPO_ROOT }) + const sha = tryExec('git rev-parse HEAD', { cwd: REPO_ROOT }) if (!sha) return null - const branch = tryExec("git rev-parse --abbrev-ref HEAD", { cwd: REPO_ROOT }) + const branch = tryExec('git rev-parse --abbrev-ref HEAD', { cwd: REPO_ROOT }) // `git status --porcelain -uno` is empty iff tracked files match HEAD. // We exclude untracked files (-uno) intentionally: a developer who's // checked out an installer scratch dir alongside the repo shouldn't // poison every local build with a [DIRTY] stamp. We DO care about // tracked-but-modified files because those mean the .exe content // differs from the commit being pinned. - const status = tryExec("git status --porcelain -uno", { cwd: REPO_ROOT }) + const status = tryExec('git status --porcelain -uno', { cwd: REPO_ROOT }) const dirty = status !== null && status.length > 0 return { commit: sha, - branch: branch === "HEAD" ? null : branch, // detached HEAD -> null + branch: branch === 'HEAD' ? null : branch, // detached HEAD -> null dirty: dirty, - source: "local" + source: 'local' } } +function fromEnv() { + const stamp = process.env.BUILD_STAMP + if (!stamp) return null + + const json = JSON.parse(stamp) + if (json.schemaVersion !== 1) { + throw new Error('Schema version !== 1') + } + if (typeof json.branch !== 'string') { + throw new Error('Expected branch to be string') + } + if (typeof json.commit !== 'string') { + throw new Error('Expected commit to be string') + } + if (typeof json.builtAt !== 'string') { + throw new Error('Expected builtAt to be string') + } + if (typeof json.dirty !== 'boolean') { + throw new Error('Expected dirty to be boolean') + } + if (typeof json.source !== 'string') { + throw new Error('Expected source to be string') + } + + return json +} + function main() { - const stamp = fromCI() || fromLocalGit() + const stamp = fromEnv() || fromCI() || fromLocalGit() if (!stamp || !stamp.commit) { console.error( - "[write-build-stamp] ERROR: could not determine git commit.\n" + - " - $GITHUB_SHA not set\n" + - " - `git rev-parse HEAD` failed at " + + '[write-build-stamp] ERROR: could not determine git commit.\n' + + ' - $GITHUB_SHA not set\n' + + ' - `git rev-parse HEAD` failed at ' + REPO_ROOT + - "\n" + - "Packaged builds require a git ref to pin first-launch install.ps1\n" + - "against. Run from a git checkout or set $GITHUB_SHA explicitly." + '\n' + + 'Packaged builds require a git ref to pin first-launch install.ps1\n' + + 'against. Run from a git checkout or set $GITHUB_SHA explicitly.' ) process.exit(1) } if (stamp.dirty) { console.warn( - "[write-build-stamp] WARNING: working tree is dirty.\n" + - " Pinning to " + + '[write-build-stamp] WARNING: working tree is dirty.\n' + + ' Pinning to ' + stamp.commit.slice(0, 12) + - " but the packaged code may differ from that commit.\n" + - " Commit your changes before publishing this build." + ' but the packaged code may differ from that commit.\n' + + ' Commit your changes before publishing this build.' ) } @@ -112,14 +140,14 @@ function main() { } fs.mkdirSync(OUT_DIR, { recursive: true }) - fs.writeFileSync(OUT_FILE, JSON.stringify(payload, null, 2) + "\n", "utf8") + fs.writeFileSync(OUT_FILE, JSON.stringify(payload, null, 2) + '\n', 'utf8') console.log( - "[write-build-stamp] wrote " + + '[write-build-stamp] wrote ' + path.relative(REPO_ROOT, OUT_FILE) + - " -> " + + ' -> ' + stamp.commit.slice(0, 12) + - (stamp.branch ? " (" + stamp.branch + ")" : "") + - (stamp.dirty ? " [DIRTY]" : "") + (stamp.branch ? ' (' + stamp.branch + ')' : '') + + (stamp.dirty ? ' [DIRTY]' : '') ) } diff --git a/apps/desktop/src/app/settings/about-settings.tsx b/apps/desktop/src/app/settings/about-settings.tsx index fa0ca76a2c3..c443adb06d9 100644 --- a/apps/desktop/src/app/settings/about-settings.tsx +++ b/apps/desktop/src/app/settings/about-settings.tsx @@ -5,6 +5,7 @@ import { BrandMark } from '@/components/brand-mark' import { Button } from '@/components/ui/button' import { Codicon } from '@/components/ui/codicon' import { type Translations, useI18n } from '@/i18n' +import { isThinClient } from '@/lib/build-mode' import { CheckCircle2, ExternalLink, Loader2, RefreshCw } from '@/lib/icons' import { cn } from '@/lib/utils' import { @@ -106,75 +107,79 @@ export function AboutSettings() {
- + {isThinClient() ? null : ( + <> + -
-
- {statusTone === 'available' ? ( - - ) : statusTone === 'error' ? null : ( - - )} -
-

{statusLine}

-

- {a.lastChecked(relativeTime(status?.fetchedAt, a))} - {justChecked && !checking ? a.justNowSuffix : ''} -

-
-
- -
- +
+ {statusTone === 'available' ? ( + + ) : statusTone === 'error' ? null : ( + + )} +
+

{statusLine}

+

+ {a.lastChecked(relativeTime(status?.fetchedAt, a))} + {justChecked && !checking ? a.justNowSuffix : ''} +

+
+
- {behind > 0 && supported && !applying && ( - <> - - + + + )} + + - - )} +
+
- -
- - - + + + )} diff --git a/apps/desktop/src/app/settings/gateway-settings.tsx b/apps/desktop/src/app/settings/gateway-settings.tsx index aae1c75efe2..12c869fc230 100644 --- a/apps/desktop/src/app/settings/gateway-settings.tsx +++ b/apps/desktop/src/app/settings/gateway-settings.tsx @@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import type { DesktopAuthProvider, DesktopConnectionProbeResult } from '@/global' import { useI18n } from '@/i18n' +import { isThinClient } from '@/lib/build-mode' import { AlertCircle, Check, FileText, Globe, Loader2, LogIn, Monitor } from '@/lib/icons' import { cn } from '@/lib/utils' import { notify, notifyError } from '@/store/notifications' @@ -466,16 +467,18 @@ export function GatewaySettings() { ) : null}
+ {isThinClient() ? null : ( + setState(current => ({ ...current, mode: 'local' }))} + title={g.localTitle} + /> + )} setState(current => ({ ...current, mode: 'local' }))} - title={g.localTitle} - /> - )} - {!remoteReauth ? ( + {remoteReauth ? null : !isThinClient() ? ( ) : null} - + {isThinClient() ? null : ( + + )}