diff --git a/packages/electron/main.mjs b/packages/electron/main.mjs index 963168b3..48b4675c 100644 --- a/packages/electron/main.mjs +++ b/packages/electron/main.mjs @@ -749,6 +749,7 @@ const spawnLocalServer = async () => { // warning and persists the flag via /api/config/settings. const lanAccessEnabled = settings.desktopLanAccessEnabled === true; const bindHost = lanAccessEnabled ? '0.0.0.0' : '127.0.0.1'; + const desktopUiPassword = typeof settings.desktopUiPassword === 'string' ? settings.desktopUiPassword.trim() : ''; // Probe before starting the server — main() in the server module sets up a // lot of global state before binding, and calling it twice after a listen @@ -773,6 +774,11 @@ const spawnLocalServer = async () => { process.env.OPENCHAMBER_DIST_DIR = resolveWebDistDir(); process.env.OPENCHAMBER_RUNTIME = 'desktop'; process.env.OPENCHAMBER_DESKTOP_NOTIFY = 'true'; + if (desktopUiPassword) { + process.env.OPENCHAMBER_UI_PASSWORD = desktopUiPassword; + } else { + delete process.env.OPENCHAMBER_UI_PASSWORD; + } process.env.OPENCHAMBER_SKIP_API_COMPRESSION = process.env.OPENCHAMBER_SKIP_API_COMPRESSION || 'true'; process.env.NO_PROXY = process.env.NO_PROXY || 'localhost,127.0.0.1'; process.env.no_proxy = process.env.no_proxy || 'localhost,127.0.0.1'; @@ -782,6 +788,7 @@ const spawnLocalServer = async () => { const handle = await startWebUiServer({ port: chosenPort, host: bindHost, + uiPassword: desktopUiPassword || null, attachSignals: false, exitOnShutdown: false, onDesktopNotification: (payload) => maybeShowNativeNotification(payload), diff --git a/packages/ui/src/components/sections/openchamber/DesktopNetworkSettings.tsx b/packages/ui/src/components/sections/openchamber/DesktopNetworkSettings.tsx index 8bf99dc1..64c0cddf 100644 --- a/packages/ui/src/components/sections/openchamber/DesktopNetworkSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/DesktopNetworkSettings.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; +import { Input } from '@/components/ui/input'; import { getDesktopLanAddress, isDesktopLocalOriginActive, isDesktopShell, restartDesktopApp } from '@/lib/desktop'; import { useI18n } from '@/lib/i18n'; @@ -10,6 +11,8 @@ export const DesktopNetworkSettings: React.FC = () => { const isLocalDesktop = isDesktopShell() && isDesktopLocalOriginActive(); const [savedValue, setSavedValue] = React.useState(false); const [draftValue, setDraftValue] = React.useState(false); + const [savedPassword, setSavedPassword] = React.useState(''); + const [draftPassword, setDraftPassword] = React.useState(''); const [isLoading, setIsLoading] = React.useState(true); const [isSaving, setIsSaving] = React.useState(false); const [error, setError] = React.useState(null); @@ -32,14 +35,20 @@ export const DesktopNetworkSettings: React.FC = () => { throw new Error(t('settings.openchamber.desktopNetwork.error.loadFailed')); } - const data = (await response.json().catch(() => null)) as null | { desktopLanAccessEnabled?: unknown }; + const data = (await response.json().catch(() => null)) as null | { + desktopLanAccessEnabled?: unknown; + desktopUiPassword?: unknown; + }; if (cancelled) { return; } const enabled = data?.desktopLanAccessEnabled === true; + const password = typeof data?.desktopUiPassword === 'string' ? data.desktopUiPassword : ''; setSavedValue(enabled); setDraftValue(enabled); + setSavedPassword(password); + setDraftPassword(password); setError(null); } catch (cause) { if (!cancelled) { @@ -77,7 +86,7 @@ export const DesktopNetworkSettings: React.FC = () => { }; }, [draftValue, isLocalDesktop]); - const isDirty = draftValue !== savedValue; + const isDirty = draftValue !== savedValue || draftPassword !== savedPassword; const currentPort = React.useMemo(() => { if (typeof window === 'undefined') { return null; @@ -107,7 +116,10 @@ export const DesktopNetworkSettings: React.FC = () => { 'Content-Type': 'application/json', Accept: 'application/json', }, - body: JSON.stringify({ desktopLanAccessEnabled: draftValue }), + body: JSON.stringify({ + desktopLanAccessEnabled: draftValue, + desktopUiPassword: draftPassword, + }), }); if (!response.ok) { @@ -115,6 +127,7 @@ export const DesktopNetworkSettings: React.FC = () => { } setSavedValue(draftValue); + setSavedPassword(draftPassword); const restarted = await restartDesktopApp(); if (!restarted) { @@ -124,7 +137,7 @@ export const DesktopNetworkSettings: React.FC = () => { setError(cause instanceof Error ? cause.message : t('settings.openchamber.desktopNetwork.error.saveFailed')); setIsSaving(false); } - }, [draftValue, isDirty, t]); + }, [draftPassword, draftValue, isDirty, t]); if (!isLocalDesktop) { return null; @@ -137,6 +150,24 @@ export const DesktopNetworkSettings: React.FC = () => {
+
+ + setDraftPassword(event.target.value)} + placeholder={t('settings.openchamber.desktopPassword.field.passwordPlaceholder')} + disabled={isLoading || isSaving} + /> +
+ {t('settings.openchamber.desktopPassword.field.passwordDescription')} +
+
+
{ if (typeof candidate.desktopLanAccessEnabled === 'boolean') { result.desktopLanAccessEnabled = candidate.desktopLanAccessEnabled; } + if (typeof candidate.desktopUiPassword === 'string') { + result.desktopUiPassword = candidate.desktopUiPassword.trim(); + } if (Array.isArray(candidate.projects)) { const projects = sanitizeProjects(candidate.projects); if (projects) { diff --git a/packages/web/server/lib/opencode/settings-helpers.test.js b/packages/web/server/lib/opencode/settings-helpers.test.js index 64e859ec..17aa0ee9 100644 --- a/packages/web/server/lib/opencode/settings-helpers.test.js +++ b/packages/web/server/lib/opencode/settings-helpers.test.js @@ -52,6 +52,17 @@ describe('settings helpers', () => { }); }); + it('accepts desktopUiPassword as a persisted shared setting', () => { + const helpers = createTestHelpers(); + + expect(helpers.sanitizeSettingsUpdate({ desktopUiPassword: ' secret ' })).toEqual({ + desktopUiPassword: 'secret', + }); + expect(helpers.sanitizeSettingsUpdate({ desktopUiPassword: '' })).toEqual({ + desktopUiPassword: '', + }); + }); + it('accepts mobileKeyboardMode as a persisted shared setting', () => { const helpers = createTestHelpers();