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,