feat: add desktop LAN access setting
This commit is contained in:
@@ -14,7 +14,7 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
net::TcpListener,
|
net::{TcpListener, UdpSocket},
|
||||||
process::Command,
|
process::Command,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicU32, AtomicU64, Ordering},
|
atomic::{AtomicU32, AtomicU64, Ordering},
|
||||||
@@ -1630,13 +1630,14 @@ fn settings_file_path() -> PathBuf {
|
|||||||
.join("settings.json")
|
.join("settings.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_desktop_settings_json() -> Option<serde_json::Value> {
|
||||||
|
fs::read_to_string(settings_file_path())
|
||||||
|
.ok()
|
||||||
|
.and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok())
|
||||||
|
}
|
||||||
|
|
||||||
fn read_desktop_local_port_from_disk() -> Option<u16> {
|
fn read_desktop_local_port_from_disk() -> Option<u16> {
|
||||||
let path = settings_file_path();
|
read_desktop_settings_json()
|
||||||
let raw = fs::read_to_string(path).ok();
|
|
||||||
let parsed = raw
|
|
||||||
.as_deref()
|
|
||||||
.and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok());
|
|
||||||
parsed
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|v| v.get("desktopLocalPort"))
|
.and_then(|v| v.get("desktopLocalPort"))
|
||||||
.and_then(|v| v.as_u64())
|
.and_then(|v| v.as_u64())
|
||||||
@@ -2548,27 +2549,10 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result<String> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let desktop_settings = read_desktop_settings_json();
|
||||||
|
|
||||||
let opencode_binary_from_settings: Option<String> = (|| {
|
let opencode_binary_from_settings: Option<String> = (|| {
|
||||||
let data_dir = env::var("OPENCHAMBER_DATA_DIR")
|
let value = desktop_settings.as_ref()?.get("opencodeBinary")?.as_str()?.trim();
|
||||||
.ok()
|
|
||||||
.and_then(|v| {
|
|
||||||
let t = v.trim().to_string();
|
|
||||||
if t.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(PathBuf::from(t))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.or_else(|| {
|
|
||||||
resolved_home_dir_path
|
|
||||||
.as_ref()
|
|
||||||
.map(|home| home.join(".config").join("openchamber"))
|
|
||||||
});
|
|
||||||
let data_dir = data_dir?;
|
|
||||||
let settings_path = data_dir.join("settings.json");
|
|
||||||
let raw = fs::read_to_string(&settings_path).ok()?;
|
|
||||||
let json = serde_json::from_str::<serde_json::Value>(&raw).ok()?;
|
|
||||||
let value = json.get("opencodeBinary")?.as_str()?.trim();
|
|
||||||
if value.is_empty() {
|
if value.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -2592,6 +2576,13 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result<String> {
|
|||||||
Some(candidate)
|
Some(candidate)
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
let sidecar_bind_host = desktop_settings
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|value| value.get("desktopLanAccessEnabled"))
|
||||||
|
.and_then(|value| value.as_bool())
|
||||||
|
.map(|enabled| if enabled { "0.0.0.0" } else { "127.0.0.1" })
|
||||||
|
.unwrap_or("127.0.0.1");
|
||||||
|
|
||||||
let mut push_unique = |value: String| {
|
let mut push_unique = |value: String| {
|
||||||
let trimmed = value.trim();
|
let trimmed = value.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
@@ -2668,7 +2659,7 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result<String> {
|
|||||||
.sidecar(SIDECAR_NAME)
|
.sidecar(SIDECAR_NAME)
|
||||||
.map_err(|err| anyhow!("Failed to resolve sidecar '{SIDECAR_NAME}': {err}"))?
|
.map_err(|err| anyhow!("Failed to resolve sidecar '{SIDECAR_NAME}': {err}"))?
|
||||||
.args(["--port", &port.to_string()])
|
.args(["--port", &port.to_string()])
|
||||||
.env("OPENCHAMBER_HOST", "127.0.0.1")
|
.env("OPENCHAMBER_HOST", sidecar_bind_host)
|
||||||
.env("OPENCHAMBER_DIST_DIR", dist_dir.clone())
|
.env("OPENCHAMBER_DIST_DIR", dist_dir.clone())
|
||||||
.env("OPENCHAMBER_RUNTIME", "desktop")
|
.env("OPENCHAMBER_RUNTIME", "desktop")
|
||||||
.env("OPENCHAMBER_DESKTOP_NOTIFY", "true")
|
.env("OPENCHAMBER_DESKTOP_NOTIFY", "true")
|
||||||
@@ -3206,9 +3197,7 @@ fn parse_theme_override(theme_mode: Option<&str>, theme_variant: Option<&str>) -
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn read_desktop_theme_override() -> Option<tauri::Theme> {
|
fn read_desktop_theme_override() -> Option<tauri::Theme> {
|
||||||
let settings = fs::read_to_string(settings_file_path())
|
let settings = read_desktop_settings_json();
|
||||||
.ok()
|
|
||||||
.and_then(|raw| serde_json::from_str::<serde_json::Value>(&raw).ok());
|
|
||||||
|
|
||||||
let use_system_theme = settings
|
let use_system_theme = settings
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -3232,6 +3221,22 @@ fn read_desktop_theme_override() -> Option<tauri::Theme> {
|
|||||||
parse_theme_override(theme_mode, theme_variant)
|
parse_theme_override(theme_mode, theme_variant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn detect_desktop_lan_ipv4() -> Option<String> {
|
||||||
|
let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
|
||||||
|
socket.connect("8.8.8.8:80").ok()?;
|
||||||
|
let address = socket.local_addr().ok()?;
|
||||||
|
let ip = address.ip();
|
||||||
|
|
||||||
|
if ip.is_loopback() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
match ip {
|
||||||
|
std::net::IpAddr::V4(ipv4) => Some(ipv4.to_string()),
|
||||||
|
std::net::IpAddr::V6(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply platform-specific window builder configuration.
|
/// Apply platform-specific window builder configuration.
|
||||||
fn apply_platform_window_config<M: Manager<tauri::Wry>>(
|
fn apply_platform_window_config<M: Manager<tauri::Wry>>(
|
||||||
builder: WebviewWindowBuilder<'_, tauri::Wry, M>,
|
builder: WebviewWindowBuilder<'_, tauri::Wry, M>,
|
||||||
@@ -3268,6 +3273,11 @@ fn desktop_set_window_theme(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn desktop_get_lan_address() -> Option<String> {
|
||||||
|
detect_desktop_lan_ipv4()
|
||||||
|
}
|
||||||
|
|
||||||
fn is_window_state_visible(app: &tauri::AppHandle, state: &DesktopWindowState) -> bool {
|
fn is_window_state_visible(app: &tauri::AppHandle, state: &DesktopWindowState) -> bool {
|
||||||
if state.width == 0 || state.height == 0 {
|
if state.width == 0 || state.height == 0 {
|
||||||
return false;
|
return false;
|
||||||
@@ -4019,6 +4029,7 @@ fn main() {
|
|||||||
desktop_hosts_set,
|
desktop_hosts_set,
|
||||||
desktop_host_probe,
|
desktop_host_probe,
|
||||||
desktop_set_window_theme,
|
desktop_set_window_theme,
|
||||||
|
desktop_get_lan_address,
|
||||||
remote_ssh::desktop_ssh_instances_get,
|
remote_ssh::desktop_ssh_instances_get,
|
||||||
remote_ssh::desktop_ssh_instances_set,
|
remote_ssh::desktop_ssh_instances_set,
|
||||||
remote_ssh::desktop_ssh_import_hosts,
|
remote_ssh::desktop_ssh_import_hosts,
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
import { getDesktopLanAddress, isDesktopLocalOriginActive, isDesktopShell, restartDesktopApp } from '@/lib/desktop';
|
||||||
|
|
||||||
|
export const DesktopNetworkSettings: React.FC = () => {
|
||||||
|
const isLocalDesktop = isDesktopShell() && isDesktopLocalOriginActive();
|
||||||
|
const [savedValue, setSavedValue] = React.useState(false);
|
||||||
|
const [draftValue, setDraftValue] = React.useState(false);
|
||||||
|
const [isLoading, setIsLoading] = React.useState(true);
|
||||||
|
const [isSaving, setIsSaving] = React.useState(false);
|
||||||
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
const [lanAddress, setLanAddress] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isLocalDesktop) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/config/settings', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to load desktop settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json().catch(() => null)) as null | { desktopLanAccessEnabled?: unknown };
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabled = data?.desktopLanAccessEnabled === true;
|
||||||
|
setSavedValue(enabled);
|
||||||
|
setDraftValue(enabled);
|
||||||
|
setError(null);
|
||||||
|
} catch (cause) {
|
||||||
|
if (!cancelled) {
|
||||||
|
setError(cause instanceof Error ? cause.message : 'Failed to load desktop settings');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!cancelled) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [isLocalDesktop]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isLocalDesktop || !draftValue) {
|
||||||
|
setLanAddress(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
const address = await getDesktopLanAddress();
|
||||||
|
if (!cancelled) {
|
||||||
|
setLanAddress(address);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [draftValue, isLocalDesktop]);
|
||||||
|
|
||||||
|
const isDirty = draftValue !== savedValue;
|
||||||
|
const currentPort = React.useMemo(() => {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Number(window.location.port);
|
||||||
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
||||||
|
}, []);
|
||||||
|
const lanUrl = draftValue && lanAddress && currentPort ? `http://${lanAddress}:${currentPort}` : null;
|
||||||
|
|
||||||
|
const handleToggle = React.useCallback(() => {
|
||||||
|
setDraftValue((current) => !current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSaveAndRestart = React.useCallback(async () => {
|
||||||
|
if (!isDirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/config/settings', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ desktopLanAccessEnabled: draftValue }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to save desktop settings');
|
||||||
|
}
|
||||||
|
|
||||||
|
setSavedValue(draftValue);
|
||||||
|
|
||||||
|
const restarted = await restartDesktopApp();
|
||||||
|
if (!restarted) {
|
||||||
|
throw new Error('Saved, but failed to restart app');
|
||||||
|
}
|
||||||
|
} catch (cause) {
|
||||||
|
setError(cause instanceof Error ? cause.message : 'Failed to save desktop settings');
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}, [draftValue, isDirty]);
|
||||||
|
|
||||||
|
if (!isLocalDesktop) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="mb-1 px-1">
|
||||||
|
<h3 className="typography-ui-header font-medium text-foreground">Desktop Network Access</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="space-y-2 px-2 pb-2 pt-0">
|
||||||
|
<div
|
||||||
|
className="group flex cursor-pointer items-start gap-2 py-1.5"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={handleToggle}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
handleToggle();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={draftValue}
|
||||||
|
onChange={handleToggle}
|
||||||
|
ariaLabel="Allow LAN access to desktop sidecar"
|
||||||
|
disabled={isLoading || isSaving}
|
||||||
|
/>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="typography-ui-label text-foreground">Let other devices on your local network open this app</div>
|
||||||
|
<div className="typography-micro text-muted-foreground/70">
|
||||||
|
Restarts the app so phones, tablets, and other computers on your Wi-Fi can open it.
|
||||||
|
</div>
|
||||||
|
<div className="typography-micro text-[var(--status-warning)]/85">
|
||||||
|
Warning: while enabled, the app is reachable by anyone on the same local network.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<div className="px-2 typography-micro text-[var(--status-error)]">{error}</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{lanUrl ? (
|
||||||
|
<div className="px-2 typography-micro text-muted-foreground/80">
|
||||||
|
{isDirty && !savedValue ? 'After restart, open from another device: ' : 'Open from another device: '}
|
||||||
|
<span className="font-mono text-foreground">{lanUrl}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="flex justify-start py-1.5">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="xs"
|
||||||
|
onClick={handleSaveAndRestart}
|
||||||
|
disabled={isLoading || isSaving || !isDirty}
|
||||||
|
className="shrink-0 !font-normal"
|
||||||
|
>
|
||||||
|
{isSaving ? 'Saving…' : 'Save + Restart'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -10,10 +10,11 @@ import { GitHubSettings } from './GitHubSettings';
|
|||||||
import { VoiceSettings } from './VoiceSettings';
|
import { VoiceSettings } from './VoiceSettings';
|
||||||
import { TunnelSettings } from './TunnelSettings';
|
import { TunnelSettings } from './TunnelSettings';
|
||||||
import { OpenCodeCliSettings } from './OpenCodeCliSettings';
|
import { OpenCodeCliSettings } from './OpenCodeCliSettings';
|
||||||
|
import { DesktopNetworkSettings } from './DesktopNetworkSettings';
|
||||||
import { KeyboardShortcutsSettings } from './KeyboardShortcutsSettings';
|
import { KeyboardShortcutsSettings } from './KeyboardShortcutsSettings';
|
||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
import { isDesktopLocalOriginActive, isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
||||||
import type { OpenChamberSection } from './types';
|
import type { OpenChamberSection } from './types';
|
||||||
|
|
||||||
interface OpenChamberPageProps {
|
interface OpenChamberPageProps {
|
||||||
@@ -25,6 +26,7 @@ export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) =>
|
|||||||
const { isMobile } = useDeviceInfo();
|
const { isMobile } = useDeviceInfo();
|
||||||
const showAbout = isMobile && isWebRuntime();
|
const showAbout = isMobile && isWebRuntime();
|
||||||
const isVSCode = isVSCodeRuntime();
|
const isVSCode = isVSCodeRuntime();
|
||||||
|
const showDesktopNetworkSettings = isDesktopShell() && isDesktopLocalOriginActive();
|
||||||
|
|
||||||
// If no section specified, show all (mobile/legacy behavior)
|
// If no section specified, show all (mobile/legacy behavior)
|
||||||
if (!section) {
|
if (!section) {
|
||||||
@@ -44,6 +46,11 @@ export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) =>
|
|||||||
<OpenCodeCliSettings />
|
<OpenCodeCliSettings />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{showDesktopNetworkSettings && (
|
||||||
|
<div className="border-t border-border/40 pt-6">
|
||||||
|
<DesktopNetworkSettings />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="border-t border-border/40 pt-6">
|
<div className="border-t border-border/40 pt-6">
|
||||||
<SessionRetentionSettings />
|
<SessionRetentionSettings />
|
||||||
</div>
|
</div>
|
||||||
@@ -128,6 +135,7 @@ const ChatSectionContent: React.FC = () => {
|
|||||||
// Sessions section: Default model & agent, Session retention
|
// Sessions section: Default model & agent, Session retention
|
||||||
const SessionsSectionContent: React.FC = () => {
|
const SessionsSectionContent: React.FC = () => {
|
||||||
const isVSCode = isVSCodeRuntime();
|
const isVSCode = isVSCodeRuntime();
|
||||||
|
const showDesktopNetworkSettings = isDesktopShell() && isDesktopLocalOriginActive();
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<DefaultsSettings />
|
<DefaultsSettings />
|
||||||
@@ -136,6 +144,11 @@ const SessionsSectionContent: React.FC = () => {
|
|||||||
<OpenCodeCliSettings />
|
<OpenCodeCliSettings />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{showDesktopNetworkSettings && (
|
||||||
|
<div className="border-t border-border/40 pt-6">
|
||||||
|
<DesktopNetworkSettings />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="border-t border-border/40 pt-6">
|
<div className="border-t border-border/40 pt-6">
|
||||||
<SessionRetentionSettings />
|
<SessionRetentionSettings />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export type DesktopSettings = {
|
|||||||
homeDirectory?: string;
|
homeDirectory?: string;
|
||||||
// Optional absolute path to `opencode` binary.
|
// Optional absolute path to `opencode` binary.
|
||||||
opencodeBinary?: string;
|
opencodeBinary?: string;
|
||||||
|
desktopLanAccessEnabled?: boolean;
|
||||||
projects?: ProjectEntry[];
|
projects?: ProjectEntry[];
|
||||||
activeProjectId?: string;
|
activeProjectId?: string;
|
||||||
approvedDirectories?: string[];
|
approvedDirectories?: string[];
|
||||||
@@ -479,6 +480,21 @@ export const restartDesktopApp = async (): Promise<boolean> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDesktopLanAddress = async (): Promise<string | null> => {
|
||||||
|
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||||
|
const result = await tauri?.core?.invoke?.('desktop_get_lan_address');
|
||||||
|
return typeof result === 'string' && result.trim().length > 0 ? result.trim() : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to get desktop LAN address (tauri)', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const openDesktopPath = async (path: string, app?: string | null): Promise<boolean> => {
|
export const openDesktopPath = async (path: string, app?: string | null): Promise<boolean> => {
|
||||||
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -519,6 +519,9 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
|
|||||||
const trimmed = candidate.opencodeBinary.trim();
|
const trimmed = candidate.opencodeBinary.trim();
|
||||||
result.opencodeBinary = trimmed.length > 0 ? trimmed : undefined;
|
result.opencodeBinary = trimmed.length > 0 ? trimmed : undefined;
|
||||||
}
|
}
|
||||||
|
if (typeof candidate.desktopLanAccessEnabled === 'boolean') {
|
||||||
|
result.desktopLanAccessEnabled = candidate.desktopLanAccessEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
const projects = sanitizeProjects(candidate.projects);
|
const projects = sanitizeProjects(candidate.projects);
|
||||||
if (projects) {
|
if (projects) {
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ export const createSettingsHelpers = (dependencies) => {
|
|||||||
const normalized = normalizeDirectoryPath(candidate.opencodeBinary).trim();
|
const normalized = normalizeDirectoryPath(candidate.opencodeBinary).trim();
|
||||||
result.opencodeBinary = normalized;
|
result.opencodeBinary = normalized;
|
||||||
}
|
}
|
||||||
|
if (typeof candidate.desktopLanAccessEnabled === 'boolean') {
|
||||||
|
result.desktopLanAccessEnabled = candidate.desktopLanAccessEnabled;
|
||||||
|
}
|
||||||
if (Array.isArray(candidate.projects)) {
|
if (Array.isArray(candidate.projects)) {
|
||||||
const projects = sanitizeProjects(candidate.projects);
|
const projects = sanitizeProjects(candidate.projects);
|
||||||
if (projects) {
|
if (projects) {
|
||||||
|
|||||||
@@ -40,4 +40,15 @@ describe('settings helpers', () => {
|
|||||||
|
|
||||||
expect(helpers.sanitizeSettingsUpdate({ messageStreamTransport: 'websocket' })).toEqual({});
|
expect(helpers.sanitizeSettingsUpdate({ messageStreamTransport: 'websocket' })).toEqual({});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accepts desktopLanAccessEnabled as a persisted shared setting', () => {
|
||||||
|
const helpers = createTestHelpers();
|
||||||
|
|
||||||
|
expect(helpers.sanitizeSettingsUpdate({ desktopLanAccessEnabled: true })).toEqual({
|
||||||
|
desktopLanAccessEnabled: true,
|
||||||
|
});
|
||||||
|
expect(helpers.sanitizeSettingsUpdate({ desktopLanAccessEnabled: false })).toEqual({
|
||||||
|
desktopLanAccessEnabled: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user