diff --git a/packages/desktop/src-tauri/Info.plist b/packages/desktop/src-tauri/Info.plist index 963d30fd..9225c2ca 100644 --- a/packages/desktop/src-tauri/Info.plist +++ b/packages/desktop/src-tauri/Info.plist @@ -18,5 +18,10 @@ OpenChamber needs microphone access for voice input. NSSpeechRecognitionUsageDescription OpenChamber needs speech recognition to transcribe voice input. + NSAppTransportSecurity + + NSAllowsArbitraryLoadsInWebContent + + diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs index 9a1782c9..f55d3055 100644 --- a/packages/desktop/src-tauri/src/main.rs +++ b/packages/desktop/src-tauri/src/main.rs @@ -1961,7 +1961,7 @@ fn build_init_script(local_origin: &str) -> String { init_script.push_str("\ntry{var old=document.getElementById('__oc-instance-switcher');if(old)old.remove();}catch(_e){}"); if !cfg!(debug_assertions) { - init_script.push_str("\ntry{document.addEventListener('contextmenu',function(e){e.preventDefault();},true);}catch(_e){}"); + init_script.push_str("\ntry{document.addEventListener('contextmenu',function(e){var t=e&&e.target;if(!t||typeof t.closest!=='function'){e.preventDefault();return;}if(t.closest('.terminal-viewport-container,[data-oc-allow-native-contextmenu],input,textarea,[contenteditable=\"true\"]')){return;}e.preventDefault();},true);}catch(_e){}"); } init_script diff --git a/packages/ui/src/components/session/SessionDialogs.tsx b/packages/ui/src/components/session/SessionDialogs.tsx index 6ef82274..e55ac05f 100644 --- a/packages/ui/src/components/session/SessionDialogs.tsx +++ b/packages/ui/src/components/session/SessionDialogs.tsx @@ -22,7 +22,7 @@ import { useSessionStore } from '@/stores/useSessionStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { useFileSystemAccess } from '@/hooks/useFileSystemAccess'; -import { isTauriShell } from '@/lib/desktop'; +import { isDesktopLocalOriginActive, isTauriShell } from '@/lib/desktop'; import { useDeviceInfo } from '@/lib/device'; import { sessionEvents } from '@/lib/sessionEvents'; @@ -129,7 +129,7 @@ export const SessionDialogs: React.FC = () => { setHasShownInitialDirectoryPrompt(true); - if (isTauriShell()) { + if (isTauriShell() && isDesktopLocalOriginActive()) { requestAccess('') .then(async (result) => { if (!result.success || !result.path) { diff --git a/packages/ui/src/components/session/SessionSidebar.tsx b/packages/ui/src/components/session/SessionSidebar.tsx index be3aaad6..54e85453 100644 --- a/packages/ui/src/components/session/SessionSidebar.tsx +++ b/packages/ui/src/components/session/SessionSidebar.tsx @@ -1,7 +1,7 @@ import React from 'react'; import type { Session } from '@opencode-ai/sdk/v2'; import { toast } from '@/components/ui'; -import { isDesktopShell, isTauriShell } from '@/lib/desktop'; +import { isDesktopLocalOriginActive, isDesktopShell, isTauriShell } from '@/lib/desktop'; import { DndContext, DragOverlay, @@ -1035,7 +1035,7 @@ export const SessionSidebar: React.FC = ({ ); const handleOpenDirectoryDialog = React.useCallback(() => { - if (!tauriIpcAvailable) { + if (!tauriIpcAvailable || !isDesktopLocalOriginActive()) { sessionEvents.requestDirectoryDialog(); return; } diff --git a/packages/ui/src/components/terminal/TerminalViewport.tsx b/packages/ui/src/components/terminal/TerminalViewport.tsx index 115ee4cf..3cc7cd89 100644 --- a/packages/ui/src/components/terminal/TerminalViewport.tsx +++ b/packages/ui/src/components/terminal/TerminalViewport.tsx @@ -377,6 +377,36 @@ const TerminalViewport = React.forwardRef { + if (typeof window === 'undefined') { + return false; + } + const selection = window.getSelection(); + if (!selection) { + return false; + } + const text = selection.toString(); + if (!text.trim()) { + return false; + } + + const container = containerRef.current; + if (!container) { + return false; + } + + const anchorNode = selection.anchorNode; + const focusNode = selection.focusNode; + if (anchorNode && !container.contains(anchorNode)) { + return false; + } + if (focusNode && !container.contains(focusNode)) { + return false; + } + + return true; + }, []); + const resetWriteState = React.useCallback(() => { pendingWriteRef.current = ''; if (writeScheduledRef.current !== null && typeof window !== 'undefined') { @@ -1229,6 +1259,18 @@ const TerminalViewport = React.forwardRef) => { event.stopPropagation(); + + const normalizedKey = event.key.toLowerCase(); + const isMacCopyShortcut = event.metaKey && !event.ctrlKey && !event.altKey && normalizedKey === 'c'; + const isWindowsLinuxCopyShortcut = + event.ctrlKey && event.shiftKey && !event.metaKey && !event.altKey && normalizedKey === 'c'; + + if ((isMacCopyShortcut || isWindowsLinuxCopyShortcut) && hasCopyableSelectionInViewport()) { + event.preventDefault(); + void copySelectionToClipboard(); + return; + } + const target = event.currentTarget as HTMLElement; const nativeEvent = event.nativeEvent as KeyboardEvent | undefined; if (nativeEvent?.isComposing) { @@ -1272,7 +1314,7 @@ const TerminalViewport = React.forwardRef => { export const requestDirectoryAccess = async ( directoryPath: string ): Promise<{ success: boolean; path?: string; projectId?: string; error?: string }> => { - // Desktop shell: use native folder picker. - if (isTauriShell()) { + // Desktop shell on local instance: use native folder picker. + if (isTauriShell() && isDesktopLocalOriginActive()) { try { const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__; const selected = await tauri?.dialog?.open?.({ diff --git a/packages/ui/src/stores/useDirectoryStore.ts b/packages/ui/src/stores/useDirectoryStore.ts index f27a5901..77ab79b8 100644 --- a/packages/ui/src/stores/useDirectoryStore.ts +++ b/packages/ui/src/stores/useDirectoryStore.ts @@ -71,15 +71,48 @@ const resolveDirectoryPath = (path: string, homeDir?: string | null): string => return normalizeDirectoryPath(expanded); }; +const getStoredHomeDirectory = (): string | null => { + const raw = safeStorage.getItem('homeDirectory'); + if (typeof raw !== 'string' || raw.trim().length === 0) { + return null; + } + const normalized = normalizeDirectoryPath(raw); + return normalized.length > 0 ? normalized : null; +}; + +const getStoredLastDirectory = (): string | null => { + const raw = safeStorage.getItem('lastDirectory'); + if (typeof raw !== 'string' || raw.trim().length === 0) { + return null; + } + const normalized = normalizeDirectoryPath(raw); + return normalized.length > 0 ? normalized : null; +}; + +const getProcessHomeDirectory = (): string | null => { + if (typeof process === 'undefined') { + return null; + } + + const env = process?.env; + const nodeHome = env?.HOME || env?.USERPROFILE || ((env?.HOMEDRIVE && env?.HOMEPATH) ? `${env.HOMEDRIVE}${env.HOMEPATH}` : undefined); + if (typeof nodeHome === 'string' && nodeHome.trim().length > 0) { + const normalized = normalizeDirectoryPath(nodeHome); + return normalized.length > 0 ? normalized : null; + } + + const cwd = process?.cwd?.(); + if (typeof cwd === 'string' && cwd.trim().length > 0) { + const normalized = normalizeDirectoryPath(cwd); + return normalized.length > 0 ? normalized : null; + } + + return null; +}; + const getHomeDirectory = () => { if (typeof window !== 'undefined') { - const storedHome = safeStorage.getItem('homeDirectory') || cachedHomeDirectory || null; - const saved = safeStorage.getItem('lastDirectory'); - if (saved && !isVSCodeRuntime()) { - return resolveDirectoryPath(saved, storedHome); - } - if (cachedHomeDirectory) return cachedHomeDirectory; const desktopHome = @@ -93,17 +126,18 @@ const getHomeDirectory = () => { return desktopHome; } + const storedHome = getStoredHomeDirectory(); if (storedHome && !isVSCodeRuntime()) { cachedHomeDirectory = storedHome; return storedHome; } } - const nodeHome = typeof process !== 'undefined' && process?.env?.HOME; - if (nodeHome) { - return nodeHome; + const processHome = getProcessHomeDirectory(); + if (processHome) { + return processHome; } - return process?.cwd?.() || '/'; + return '/'; }; @@ -198,8 +232,16 @@ const getVsCodeWorkspaceFolder = (): string | null => { }; const initialHomeDirectory = getVsCodeWorkspaceFolder() || getHomeDirectory(); -if (initialHomeDirectory) { - opencodeClient.setDirectory(initialHomeDirectory); +const initialCurrentDirectory = (() => { + const persisted = getStoredLastDirectory(); + if (persisted && !isVSCodeRuntime()) { + return resolveDirectoryPath(persisted, initialHomeDirectory); + } + return initialHomeDirectory; +})(); + +if (initialCurrentDirectory) { + opencodeClient.setDirectory(initialCurrentDirectory); } const initialIsHomeReady = Boolean(initialHomeDirectory && initialHomeDirectory !== '/'); @@ -207,8 +249,8 @@ export const useDirectoryStore = create()( devtools( (set, get) => ({ - currentDirectory: initialHomeDirectory, - directoryHistory: [initialHomeDirectory], + currentDirectory: initialCurrentDirectory, + directoryHistory: [initialCurrentDirectory], historyIndex: 0, homeDirectory: initialHomeDirectory, hasPersistedDirectory: initialHasPersistedDirectory,