refactor(desktop): make Tauri thin shell running web sidecar (#273)
## What / Why This PR finishes the desktop refactor: the Tauri app is now a thin shell that launches the web server as a sidecar and loads the UI from `http://127.0.0.1:<port>`. All real backend logic lives in `packages/web/server/index.js`; desktop Rust keeps only native integrations (menu/dialog/notifications/updater/deep-link + window chrome). This unblocks: - consistent behavior across web/desktop/vscode (single backend) - simpler desktop maintenance (no duplicated Rust backend) - host switching between Local + remote instances in desktop - reliable cold-start behavior on slow machines (VSCode + desktop) ## Key changes - Desktop sidecar runtime - build pipeline to bundle web dist + `openchamber-server` sidecar (`packages/desktop/scripts/build-sidecar.mjs`) - robust local port selection (prefer saved/default, fallback to random; persisted in `~/.config/openchamber/settings.json`) - improved PATH handling so the sidecar can locate `opencode` CLI (incl `~/.opencode/bin`, overrides, common bins) - disable native right-click context menu in production builds (dev keeps it) - Desktop instance switcher (Tauri-only) - header button + modal to add/edit/delete remote hosts, set default, probe status/ping, switch back to Local escape hatch - auth gate includes host switcher so you can recover when a remote host is broken/auth-required - host list stored desktop-locally (not tied to the currently selected remote server) - Notifications - decision logic moved server-side; desktop notifications emitted via sidecar stdout and shown natively by Tauri - prevent double-notifications on desktop Local origin (UI ignores SSE notification when native path is active) - restore macOS notification sound - Updates - Tauri updater used only when viewing Local instance in desktop shell (avoid “remote web update” triggering desktop restart) - Settings persistence & UX polish - persist model favorites/recents via `/api/config/settings` (works for web + desktop; not origin-dependent) - persist per-project sidebar collapse state in `projects[].sidebarCollapsed` via `/api/config/settings` (with debounce on toggles) - macOS header sizing/traffic-lights offsets fixed (marketing macOS major injected from desktop; MultiRun header aligned) - VSCode cold-start: keep retrying provider/agent loads after connection to avoid empty UI on slow machines - misc lint/type fixes + bun.lock sync - Desktop bootstrap / resiliency - show onboarding screen when OpenCode CLI is missing (desktop Local origin), with retry hook to restart OpenCode after install ## Testing notes - Desktop (macOS): switch Local <-> remote, set default host, verify auth gate recovery, native notifications (with sound), updater gated to Local - Web: favorites/recents + per-project collapsed state persist across reload/restart - VSCode: slow startup no longer results in missing providers/agents/models
This commit is contained in:
committed by
GitHub
parent
b733f26aed
commit
83ffb1af34
@@ -116,27 +116,13 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
|
||||
const { setSidebarOpen } = useUIStore();
|
||||
const { isMobile } = useDeviceInfo();
|
||||
|
||||
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return typeof window.opencodeDesktop !== 'undefined';
|
||||
});
|
||||
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
loadAgents();
|
||||
}, [loadAgents]);
|
||||
|
||||
const bgClass = isDesktopRuntime
|
||||
? 'bg-transparent'
|
||||
: isVSCode
|
||||
? 'bg-background'
|
||||
: 'bg-sidebar';
|
||||
const bgClass = isVSCode ? 'bg-background' : 'bg-sidebar';
|
||||
|
||||
const handleCreateNew = () => {
|
||||
// Generate unique name
|
||||
|
||||
@@ -46,27 +46,13 @@ export const CommandsSidebar: React.FC<CommandsSidebarProps> = ({ onItemSelect }
|
||||
const { setSidebarOpen } = useUIStore();
|
||||
const { isMobile } = useDeviceInfo();
|
||||
|
||||
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return typeof window.opencodeDesktop !== 'undefined';
|
||||
});
|
||||
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
loadCommands();
|
||||
}, [loadCommands]);
|
||||
|
||||
const bgClass = isDesktopRuntime
|
||||
? 'bg-transparent'
|
||||
: isVSCode
|
||||
? 'bg-background'
|
||||
: 'bg-sidebar';
|
||||
const bgClass = isVSCode ? 'bg-background' : 'bg-sidebar';
|
||||
|
||||
const handleCreateNew = () => {
|
||||
// Generate unique name
|
||||
|
||||
@@ -67,18 +67,8 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
|
||||
const { setSidebarOpen } = useUIStore();
|
||||
const { isMobile } = useDeviceInfo();
|
||||
|
||||
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return typeof window.opencodeDesktop !== 'undefined';
|
||||
});
|
||||
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||
}, []);
|
||||
|
||||
const unimportedCredentials = getUnimportedCredentials();
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -98,11 +88,7 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
|
||||
}
|
||||
};
|
||||
|
||||
const bgClass = isDesktopRuntime
|
||||
? 'bg-transparent'
|
||||
: isVSCode
|
||||
? 'bg-background'
|
||||
: 'bg-sidebar';
|
||||
const bgClass = isVSCode ? 'bg-background' : 'bg-sidebar';
|
||||
|
||||
const handleCreateProfile = () => {
|
||||
setSelectedProfile('new');
|
||||
|
||||
@@ -6,7 +6,7 @@ import { AgentSelector } from '@/components/sections/commands/AgentSelector';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import { getDesktopSettings, isDesktopRuntime, isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
import { getModifierLabel } from '@/lib/utils';
|
||||
@@ -67,11 +67,8 @@ export const DefaultsSettings: React.FC = () => {
|
||||
try {
|
||||
let data: { defaultModel?: string; defaultVariant?: string; defaultAgent?: string } | null = null;
|
||||
|
||||
// 1. Desktop runtime (Tauri)
|
||||
if (isDesktopRuntime()) {
|
||||
data = await getDesktopSettings();
|
||||
} else {
|
||||
// 2. Runtime settings API (VSCode)
|
||||
// 1. Runtime settings API (VSCode)
|
||||
if (!data) {
|
||||
const runtimeSettings = getRegisteredRuntimeAPIs()?.settings;
|
||||
if (runtimeSettings) {
|
||||
try {
|
||||
@@ -85,19 +82,19 @@ export const DefaultsSettings: React.FC = () => {
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fall through to fetch
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fetch API (Web)
|
||||
if (!data) {
|
||||
const response = await fetch('/api/config/settings', {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (response.ok) {
|
||||
data = await response.json();
|
||||
}
|
||||
// 2. Fetch API (Web/server)
|
||||
if (!data) {
|
||||
const response = await fetch('/api/config/settings', {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (response.ok) {
|
||||
data = await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,12 +150,12 @@ export const DefaultsSettings: React.FC = () => {
|
||||
defaultVariant: '',
|
||||
});
|
||||
|
||||
if (!isDesktopRuntime()) {
|
||||
const response = await fetch('/api/config/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ defaultModel: newValue }),
|
||||
});
|
||||
{
|
||||
const response = await fetch('/api/config/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ defaultModel: newValue }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.warn('Failed to save default model to server:', response.status, response.statusText);
|
||||
}
|
||||
|
||||
@@ -41,13 +41,12 @@ export const GitHubSettings: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const desktop = (window as typeof window & { opencodeDesktop?: { openExternal?: (url: string) => Promise<unknown> } }).opencodeDesktop;
|
||||
if (desktop?.openExternal) {
|
||||
type TauriShell = { shell?: { open?: (url: string) => Promise<unknown> } };
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriShell }).__TAURI__;
|
||||
if (tauri?.shell?.open) {
|
||||
try {
|
||||
const result = await desktop.openExternal(url);
|
||||
if (result && typeof result === 'object' && 'success' in result && (result as { success?: boolean }).success === true) {
|
||||
return;
|
||||
}
|
||||
await tauri.shell.open(url);
|
||||
return;
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { RiInformationLine } from '@remixicon/react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import { isDesktopRuntime, getDesktopSettings } from '@/lib/desktop';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
import { setFilesViewShowGitignored, useFilesViewShowGitignored } from '@/lib/filesViewShowGitignored';
|
||||
@@ -22,11 +21,8 @@ export const GitSettings: React.FC = () => {
|
||||
try {
|
||||
let data: { gitmojiEnabled?: boolean } | null = null;
|
||||
|
||||
// 1. Desktop runtime (Tauri)
|
||||
if (isDesktopRuntime()) {
|
||||
data = await getDesktopSettings();
|
||||
} else {
|
||||
// 2. Runtime settings API (VSCode)
|
||||
// 1. Runtime settings API (VSCode)
|
||||
if (!data) {
|
||||
const runtimeSettings = getRegisteredRuntimeAPIs()?.settings;
|
||||
if (runtimeSettings) {
|
||||
try {
|
||||
@@ -40,19 +36,19 @@ export const GitSettings: React.FC = () => {
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fall through to fetch
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fetch API (Web)
|
||||
if (!data) {
|
||||
const response = await fetch('/api/config/settings', {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (response.ok) {
|
||||
data = await response.json();
|
||||
}
|
||||
// 2. Fetch API (Web/server)
|
||||
if (!data) {
|
||||
const response = await fetch('/api/config/settings', {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (response.ok) {
|
||||
data = await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import { getDesktopSettings, isDesktopRuntime } from '@/lib/desktop';
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
import { DEFAULT_MEMORY_LIMITS, DEFAULT_ACTIVE_SESSION_WINDOW } from '@/stores/types/sessionTypes';
|
||||
|
||||
@@ -34,11 +33,8 @@ export const MemoryLimitsSettings: React.FC = () => {
|
||||
try {
|
||||
let data: { memoryLimitHistorical?: number; memoryLimitViewport?: number; memoryLimitActiveSession?: number } | null = null;
|
||||
|
||||
// 1. Desktop runtime (Tauri)
|
||||
if (isDesktopRuntime()) {
|
||||
data = await getDesktopSettings();
|
||||
} else {
|
||||
// 2. Runtime settings API (VSCode)
|
||||
// 1. Runtime settings API (VSCode)
|
||||
if (!data) {
|
||||
const runtimeSettings = getRegisteredRuntimeAPIs()?.settings;
|
||||
if (runtimeSettings) {
|
||||
try {
|
||||
@@ -52,19 +48,19 @@ export const MemoryLimitsSettings: React.FC = () => {
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fall through to fetch
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fetch API (Web)
|
||||
if (!data) {
|
||||
const response = await fetch('/api/config/settings', {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (response.ok) {
|
||||
data = await response.json();
|
||||
}
|
||||
// 2. Fetch API (Web/server)
|
||||
if (!data) {
|
||||
const response = await fetch('/api/config/settings', {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (response.ok) {
|
||||
data = await response.json();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,17 +87,6 @@ export const MemoryLimitsSettings: React.FC = () => {
|
||||
const persistSetting = React.useCallback(async (key: string, value: number) => {
|
||||
try {
|
||||
await updateDesktopSettings({ [key]: value });
|
||||
|
||||
if (!isDesktopRuntime()) {
|
||||
const response = await fetch('/api/config/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ [key]: value }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.warn(`Failed to save ${key} to server:`, response.status, response.statusText);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to save ${key}:`, error);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { isWebRuntime } from '@/lib/desktop';
|
||||
import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { toast } from '@/components/ui';
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
@@ -8,7 +8,9 @@ import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
import { GridLoader } from '@/components/ui/grid-loader';
|
||||
|
||||
export const NotificationSettings: React.FC = () => {
|
||||
const isWeb = isWebRuntime();
|
||||
const isDesktop = React.useMemo(() => isDesktopShell(), []);
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
const isBrowser = !isDesktop && !isVSCode;
|
||||
const nativeNotificationsEnabled = useUIStore(state => state.nativeNotificationsEnabled);
|
||||
const setNativeNotificationsEnabled = useUIStore(state => state.setNativeNotificationsEnabled);
|
||||
const notificationMode = useUIStore(state => state.notificationMode);
|
||||
@@ -22,7 +24,7 @@ export const NotificationSettings: React.FC = () => {
|
||||
const [pushBusy, setPushBusy] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isWeb) {
|
||||
if (!isBrowser) {
|
||||
setPushSupported(false);
|
||||
setPushSubscribed(false);
|
||||
return;
|
||||
@@ -58,10 +60,16 @@ export const NotificationSettings: React.FC = () => {
|
||||
};
|
||||
|
||||
void refresh();
|
||||
}, [isWeb]);
|
||||
}, [isBrowser]);
|
||||
|
||||
const handleToggleChange = async (checked: boolean) => {
|
||||
if (!isWeb) {
|
||||
if (isDesktop) {
|
||||
setNativeNotificationsEnabled(checked);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isBrowser) {
|
||||
setNativeNotificationsEnabled(checked);
|
||||
return;
|
||||
}
|
||||
if (checked && typeof Notification !== 'undefined' && Notification.permission === 'default') {
|
||||
@@ -86,7 +94,7 @@ export const NotificationSettings: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const canShowNotifications = isWeb && typeof Notification !== 'undefined' && Notification.permission === 'granted';
|
||||
const canShowNotifications = isDesktop || (isBrowser && typeof Notification !== 'undefined' && Notification.permission === 'granted');
|
||||
|
||||
const base64UrlToUint8Array = (base64Url: string): Uint8Array<ArrayBuffer> => {
|
||||
const padding = '='.repeat((4 - (base64Url.length % 4)) % 4);
|
||||
@@ -365,92 +373,93 @@ export const NotificationSettings: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* General Notification Settings */}
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-1 pt-2">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">
|
||||
Notification Preferences
|
||||
When to notify
|
||||
</h3>
|
||||
<p className="typography-ui text-muted-foreground">
|
||||
Configure how and when you receive notifications.
|
||||
Customize when notifications show up.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<span className="typography-ui text-foreground">
|
||||
Notify for subtasks
|
||||
Enable notifications
|
||||
</span>
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
When off, no notifications for child sessions created during multi-run.
|
||||
Turns notifications on or off.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={notifyOnSubtasks}
|
||||
onCheckedChange={(checked) => setNotifyOnSubtasks(checked)}
|
||||
checked={nativeNotificationsEnabled && canShowNotifications}
|
||||
onCheckedChange={handleToggleChange}
|
||||
className="data-[state=checked]:bg-status-info"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isWeb && (
|
||||
<>
|
||||
{/* Foreground Notifications */}
|
||||
<div className="space-y-1 pt-4">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">
|
||||
Foreground Notifications
|
||||
</h3>
|
||||
<p className="typography-ui text-muted-foreground">
|
||||
Uses the browser Notification API while OpenChamber is open.
|
||||
{isBrowser && (
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
Your browser may ask for permission the first time.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{nativeNotificationsEnabled && canShowNotifications && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<span className="typography-ui text-foreground">
|
||||
Include subagent results
|
||||
</span>
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
Also notify for child sessions started by the main one.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={notifyOnSubtasks}
|
||||
onCheckedChange={(checked) => setNotifyOnSubtasks(checked)}
|
||||
className="data-[state=checked]:bg-status-info"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
{nativeNotificationsEnabled && canShowNotifications && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<span className="typography-ui text-foreground">
|
||||
Enable foreground notifications
|
||||
Notify while app is focused
|
||||
</span>
|
||||
<Switch
|
||||
checked={nativeNotificationsEnabled && canShowNotifications}
|
||||
onCheckedChange={handleToggleChange}
|
||||
className="data-[state=checked]:bg-status-info"
|
||||
/>
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
When off, only notify when you are not looking at OpenChamber.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={notificationMode === 'always'}
|
||||
onCheckedChange={(checked) => setNotificationMode(checked ? 'always' : 'hidden-only')}
|
||||
className="data-[state=checked]:bg-status-info"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nativeNotificationsEnabled && canShowNotifications && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<span className="typography-ui text-foreground">
|
||||
Notify even when visible
|
||||
</span>
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
When off, only notifies when the tab is hidden or the window is not focused.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={notificationMode === 'always'}
|
||||
onCheckedChange={(checked) => setNotificationMode(checked ? 'always' : 'hidden-only')}
|
||||
className="data-[state=checked]:bg-status-info"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isBrowser && (
|
||||
<>
|
||||
{notificationPermission === 'denied' && (
|
||||
<p className="typography-micro text-destructive">
|
||||
Notification permission denied. Enable notifications in your browser settings.
|
||||
Notification permission denied. Enable it in your browser settings.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{notificationPermission === 'granted' && !nativeNotificationsEnabled && (
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
Permission granted, but foreground notifications are disabled.
|
||||
Permission granted, but notifications are disabled.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Background Notifications */}
|
||||
<div className="space-y-1 pt-4">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">
|
||||
Background Notifications (Push)
|
||||
Background (Push)
|
||||
</h3>
|
||||
<p className="typography-ui text-muted-foreground">
|
||||
Uses push notifications; works when OpenChamber is closed.
|
||||
Get notified even if this page is closed.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -460,7 +469,7 @@ export const NotificationSettings: React.FC = () => {
|
||||
</p>
|
||||
) : (
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
Desktop Chrome/Edge and Android support push in the browser. iOS requires an installed PWA.
|
||||
Desktop Chrome/Edge and Android support push. iOS requires an installed PWA.
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -468,10 +477,10 @@ export const NotificationSettings: React.FC = () => {
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="space-y-0.5">
|
||||
<span className="typography-ui text-foreground">
|
||||
Enable background notifications
|
||||
Enable push notifications
|
||||
</span>
|
||||
<p className="typography-micro text-muted-foreground">
|
||||
Opens chat with /?session=<id> deep link.
|
||||
Clicking a notification opens the relevant session.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -499,6 +508,17 @@ export const NotificationSettings: React.FC = () => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{isVSCode && (
|
||||
<div className="space-y-1 pt-4">
|
||||
<h3 className="typography-ui-header font-semibold text-foreground">
|
||||
Delivery
|
||||
</h3>
|
||||
<p className="typography-ui text-muted-foreground">
|
||||
VS Code runtime handles notifications separately.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -62,19 +62,9 @@ export const OpenChamberSidebar: React.FC<OpenChamberSidebarProps> = ({
|
||||
const { isMobile } = useDeviceInfo();
|
||||
const showAbout = isMobile && isWebRuntime();
|
||||
|
||||
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return typeof window.opencodeDesktop !== 'undefined';
|
||||
});
|
||||
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
const isWeb = React.useMemo(() => isWebRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||
}, []);
|
||||
|
||||
const visibleSections = React.useMemo(() => {
|
||||
return OPENCHAMBER_SECTION_GROUPS.filter((group) => {
|
||||
if (group.webOnly && !isWeb) return false;
|
||||
@@ -86,11 +76,7 @@ export const OpenChamberSidebar: React.FC<OpenChamberSidebarProps> = ({
|
||||
// Desktop app: transparent for blur effect
|
||||
// VS Code: bg-background (same as page content)
|
||||
// Web/mobile: bg-sidebar
|
||||
const bgClass = isDesktopRuntime
|
||||
? 'bg-transparent'
|
||||
: isVSCode
|
||||
? 'bg-background'
|
||||
: 'bg-sidebar';
|
||||
const bgClass = isVSCode ? 'bg-background' : 'bg-sidebar';
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col', bgClass)}>
|
||||
|
||||
@@ -20,23 +20,9 @@ export const ProvidersSidebar: React.FC<ProvidersSidebarProps> = ({ onItemSelect
|
||||
const setSelectedProvider = useConfigStore((state) => state.setSelectedProvider);
|
||||
const { isMobile } = useDeviceInfo();
|
||||
|
||||
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return typeof window.opencodeDesktop !== 'undefined';
|
||||
});
|
||||
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||
}, []);
|
||||
|
||||
const bgClass = isDesktopRuntime
|
||||
? 'bg-transparent'
|
||||
: isVSCode
|
||||
? 'bg-background'
|
||||
: 'bg-sidebar';
|
||||
const bgClass = isVSCode ? 'bg-background' : 'bg-sidebar';
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col', bgClass)}>
|
||||
|
||||
@@ -33,26 +33,12 @@ export const SettingsSidebarLayout: React.FC<SettingsSidebarLayoutProps> = ({
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return typeof window.opencodeDesktop !== 'undefined';
|
||||
});
|
||||
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||
}, []);
|
||||
|
||||
// Desktop app: transparent for blur effect
|
||||
// VS Code: bg-background (same as page content)
|
||||
// Web/mobile: bg-sidebar
|
||||
const bgClass = isDesktopRuntime
|
||||
? 'bg-transparent'
|
||||
: isVSCode
|
||||
? 'bg-background'
|
||||
: 'bg-sidebar';
|
||||
const bgClass = isVSCode ? 'bg-background' : 'bg-sidebar';
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -47,27 +47,13 @@ export const SkillsSidebar: React.FC<SkillsSidebarProps> = ({ onItemSelect }) =>
|
||||
const { setSidebarOpen } = useUIStore();
|
||||
const { isMobile } = useDeviceInfo();
|
||||
|
||||
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return typeof window.opencodeDesktop !== 'undefined';
|
||||
});
|
||||
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
loadSkills();
|
||||
}, [loadSkills]);
|
||||
|
||||
const bgClass = isDesktopRuntime
|
||||
? 'bg-transparent'
|
||||
: isVSCode
|
||||
? 'bg-background'
|
||||
: 'bg-sidebar';
|
||||
const bgClass = isVSCode ? 'bg-background' : 'bg-sidebar';
|
||||
|
||||
const handleCreateNew = () => {
|
||||
// Generate unique name
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { RiGitRepositoryLine } from '@remixicon/react';
|
||||
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
import { getDesktopSettings, isDesktopRuntime, isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import type { DesktopSettings, SkillCatalogConfig } from '@/lib/desktop';
|
||||
import { useSkillsCatalogStore } from '@/stores/useSkillsCatalogStore';
|
||||
@@ -50,10 +50,6 @@ type IdentityOption = { id: string; name: string };
|
||||
|
||||
const loadSettings = async (): Promise<DesktopSettings | null> => {
|
||||
try {
|
||||
if (isDesktopRuntime()) {
|
||||
return await getDesktopSettings();
|
||||
}
|
||||
|
||||
const runtimeSettings = getRegisteredRuntimeAPIs()?.settings;
|
||||
if (runtimeSettings) {
|
||||
const result = await runtimeSettings.load();
|
||||
|
||||
@@ -17,7 +17,6 @@ import { useSkillsCatalogStore } from '@/stores/useSkillsCatalogStore';
|
||||
import type { SkillsCatalogItem } from '@/lib/api/types';
|
||||
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
import { getDesktopSettings, isDesktopRuntime } from '@/lib/desktop';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import type { DesktopSettings, SkillCatalogConfig } from '@/lib/desktop';
|
||||
|
||||
@@ -33,10 +32,6 @@ interface SkillsCatalogPageProps {
|
||||
|
||||
const loadSettings = async (): Promise<DesktopSettings | null> => {
|
||||
try {
|
||||
if (isDesktopRuntime()) {
|
||||
return await getDesktopSettings();
|
||||
}
|
||||
|
||||
const runtimeSettings = getRegisteredRuntimeAPIs()?.settings;
|
||||
if (runtimeSettings) {
|
||||
const result = await runtimeSettings.load();
|
||||
|
||||
@@ -43,18 +43,8 @@ export const UsageSidebar: React.FC<UsageSidebarProps> = ({ onItemSelect }) => {
|
||||
const loadUsageSettings = useQuotaStore((state) => state.loadSettings);
|
||||
const { isMobile } = useDeviceInfo();
|
||||
|
||||
const [isDesktopRuntime, setIsDesktopRuntime] = React.useState<boolean>(() => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return typeof window.opencodeDesktop !== 'undefined';
|
||||
});
|
||||
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
void loadUsageSettings();
|
||||
}, [loadUsageSettings]);
|
||||
@@ -89,14 +79,7 @@ export const UsageSidebar: React.FC<UsageSidebarProps> = ({ onItemSelect }) => {
|
||||
void persistUsageSettings({ usageDisplayMode: value });
|
||||
}, [persistUsageSettings, setUsageDisplayMode]);
|
||||
|
||||
|
||||
|
||||
|
||||
const bgClass = isDesktopRuntime
|
||||
? 'bg-transparent'
|
||||
: isVSCode
|
||||
? 'bg-background'
|
||||
: 'bg-sidebar';
|
||||
const bgClass = isVSCode ? 'bg-background' : 'bg-sidebar';
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col', bgClass)}>
|
||||
|
||||
Reference in New Issue
Block a user