fix(desktop): respect local-origin tauri shell for directory access (#391)
* feat: enable local-origin desktop features and copy in terminal support - Enable arbitrary web loads in desktop web content for TAURI apps. - Guard directory dialog access behind local-origin origin check. - Add copy-to-clipboard support for terminal selections via common shortcuts. * fix: improve initial directory resolution and persistence in directory store
This commit is contained in:
committed by
GitHub
parent
844562749d
commit
3afe0bb45d
@@ -18,5 +18,10 @@
|
|||||||
<string>OpenChamber needs microphone access for voice input.</string>
|
<string>OpenChamber needs microphone access for voice input.</string>
|
||||||
<key>NSSpeechRecognitionUsageDescription</key>
|
<key>NSSpeechRecognitionUsageDescription</key>
|
||||||
<string>OpenChamber needs speech recognition to transcribe voice input.</string>
|
<string>OpenChamber needs speech recognition to transcribe voice input.</string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoadsInWebContent</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -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){}");
|
init_script.push_str("\ntry{var old=document.getElementById('__oc-instance-switcher');if(old)old.remove();}catch(_e){}");
|
||||||
|
|
||||||
if !cfg!(debug_assertions) {
|
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
|
init_script
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { useSessionStore } from '@/stores/useSessionStore';
|
|||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||||
import { useFileSystemAccess } from '@/hooks/useFileSystemAccess';
|
import { useFileSystemAccess } from '@/hooks/useFileSystemAccess';
|
||||||
import { isTauriShell } from '@/lib/desktop';
|
import { isDesktopLocalOriginActive, isTauriShell } from '@/lib/desktop';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { sessionEvents } from '@/lib/sessionEvents';
|
import { sessionEvents } from '@/lib/sessionEvents';
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ export const SessionDialogs: React.FC = () => {
|
|||||||
|
|
||||||
setHasShownInitialDirectoryPrompt(true);
|
setHasShownInitialDirectoryPrompt(true);
|
||||||
|
|
||||||
if (isTauriShell()) {
|
if (isTauriShell() && isDesktopLocalOriginActive()) {
|
||||||
requestAccess('')
|
requestAccess('')
|
||||||
.then(async (result) => {
|
.then(async (result) => {
|
||||||
if (!result.success || !result.path) {
|
if (!result.success || !result.path) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Session } from '@opencode-ai/sdk/v2';
|
import type { Session } from '@opencode-ai/sdk/v2';
|
||||||
import { toast } from '@/components/ui';
|
import { toast } from '@/components/ui';
|
||||||
import { isDesktopShell, isTauriShell } from '@/lib/desktop';
|
import { isDesktopLocalOriginActive, isDesktopShell, isTauriShell } from '@/lib/desktop';
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragOverlay,
|
DragOverlay,
|
||||||
@@ -1035,7 +1035,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleOpenDirectoryDialog = React.useCallback(() => {
|
const handleOpenDirectoryDialog = React.useCallback(() => {
|
||||||
if (!tauriIpcAvailable) {
|
if (!tauriIpcAvailable || !isDesktopLocalOriginActive()) {
|
||||||
sessionEvents.requestDirectoryDialog();
|
sessionEvents.requestDirectoryDialog();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,6 +377,36 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const hasCopyableSelectionInViewport = React.useCallback((): boolean => {
|
||||||
|
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(() => {
|
const resetWriteState = React.useCallback(() => {
|
||||||
pendingWriteRef.current = '';
|
pendingWriteRef.current = '';
|
||||||
if (writeScheduledRef.current !== null && typeof window !== 'undefined') {
|
if (writeScheduledRef.current !== null && typeof window !== 'undefined') {
|
||||||
@@ -1229,6 +1259,18 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
|||||||
const handleHiddenKeyDown = React.useCallback(
|
const handleHiddenKeyDown = React.useCallback(
|
||||||
(event: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
(event: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
event.stopPropagation();
|
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 target = event.currentTarget as HTMLElement;
|
||||||
const nativeEvent = event.nativeEvent as KeyboardEvent | undefined;
|
const nativeEvent = event.nativeEvent as KeyboardEvent | undefined;
|
||||||
if (nativeEvent?.isComposing) {
|
if (nativeEvent?.isComposing) {
|
||||||
@@ -1272,7 +1314,7 @@ const TerminalViewport = React.forwardRef<TerminalController, TerminalViewportPr
|
|||||||
|
|
||||||
scheduleKeyProbe(target);
|
scheduleKeyProbe(target);
|
||||||
},
|
},
|
||||||
[clearEditableValue, readEditableValue, scheduleKeyProbe]
|
[clearEditableValue, copySelectionToClipboard, hasCopyableSelectionInViewport, readEditableValue, scheduleKeyProbe]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleHiddenKeyUp = React.useCallback(
|
const handleHiddenKeyUp = React.useCallback(
|
||||||
|
|||||||
@@ -201,8 +201,8 @@ export const getDesktopHomeDirectory = async (): Promise<string | null> => {
|
|||||||
export const requestDirectoryAccess = async (
|
export const requestDirectoryAccess = async (
|
||||||
directoryPath: string
|
directoryPath: string
|
||||||
): Promise<{ success: boolean; path?: string; projectId?: string; error?: string }> => {
|
): Promise<{ success: boolean; path?: string; projectId?: string; error?: string }> => {
|
||||||
// Desktop shell: use native folder picker.
|
// Desktop shell on local instance: use native folder picker.
|
||||||
if (isTauriShell()) {
|
if (isTauriShell() && isDesktopLocalOriginActive()) {
|
||||||
try {
|
try {
|
||||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||||
const selected = await tauri?.dialog?.open?.({
|
const selected = await tauri?.dialog?.open?.({
|
||||||
|
|||||||
@@ -71,15 +71,48 @@ const resolveDirectoryPath = (path: string, homeDir?: string | null): string =>
|
|||||||
return normalizeDirectoryPath(expanded);
|
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 = () => {
|
const getHomeDirectory = () => {
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
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;
|
if (cachedHomeDirectory) return cachedHomeDirectory;
|
||||||
|
|
||||||
const desktopHome =
|
const desktopHome =
|
||||||
@@ -93,17 +126,18 @@ const getHomeDirectory = () => {
|
|||||||
return desktopHome;
|
return desktopHome;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const storedHome = getStoredHomeDirectory();
|
||||||
if (storedHome && !isVSCodeRuntime()) {
|
if (storedHome && !isVSCodeRuntime()) {
|
||||||
cachedHomeDirectory = storedHome;
|
cachedHomeDirectory = storedHome;
|
||||||
return storedHome;
|
return storedHome;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeHome = typeof process !== 'undefined' && process?.env?.HOME;
|
const processHome = getProcessHomeDirectory();
|
||||||
if (nodeHome) {
|
if (processHome) {
|
||||||
return nodeHome;
|
return processHome;
|
||||||
}
|
}
|
||||||
return process?.cwd?.() || '/';
|
return '/';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -198,8 +232,16 @@ const getVsCodeWorkspaceFolder = (): string | null => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initialHomeDirectory = getVsCodeWorkspaceFolder() || getHomeDirectory();
|
const initialHomeDirectory = getVsCodeWorkspaceFolder() || getHomeDirectory();
|
||||||
if (initialHomeDirectory) {
|
const initialCurrentDirectory = (() => {
|
||||||
opencodeClient.setDirectory(initialHomeDirectory);
|
const persisted = getStoredLastDirectory();
|
||||||
|
if (persisted && !isVSCodeRuntime()) {
|
||||||
|
return resolveDirectoryPath(persisted, initialHomeDirectory);
|
||||||
|
}
|
||||||
|
return initialHomeDirectory;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (initialCurrentDirectory) {
|
||||||
|
opencodeClient.setDirectory(initialCurrentDirectory);
|
||||||
}
|
}
|
||||||
const initialIsHomeReady = Boolean(initialHomeDirectory && initialHomeDirectory !== '/');
|
const initialIsHomeReady = Boolean(initialHomeDirectory && initialHomeDirectory !== '/');
|
||||||
|
|
||||||
@@ -207,8 +249,8 @@ export const useDirectoryStore = create<DirectoryStore>()(
|
|||||||
devtools(
|
devtools(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
|
|
||||||
currentDirectory: initialHomeDirectory,
|
currentDirectory: initialCurrentDirectory,
|
||||||
directoryHistory: [initialHomeDirectory],
|
directoryHistory: [initialCurrentDirectory],
|
||||||
historyIndex: 0,
|
historyIndex: 0,
|
||||||
homeDirectory: initialHomeDirectory,
|
homeDirectory: initialHomeDirectory,
|
||||||
hasPersistedDirectory: initialHasPersistedDirectory,
|
hasPersistedDirectory: initialHasPersistedDirectory,
|
||||||
|
|||||||
Reference in New Issue
Block a user