diff --git a/package.json b/package.json index b1b93b2a..3b635a5f 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", "electron": "^41.0.2", - "electron-builder": "^26.0.12", + "electron-builder": "^26.8.1", "electron-vite": "^5.0.0", "husky": "^9.1.7", "lint-staged": "^16.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a17d1a1..47cd11d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,7 +100,7 @@ importers: specifier: ^41.0.2 version: 41.0.2 electron-builder: - specifier: ^26.0.12 + specifier: ^26.8.1 version: 26.8.1(electron-builder-squirrel-windows@26.8.1) electron-vite: specifier: ^5.0.0 @@ -2019,8 +2019,8 @@ packages: '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} - '@types/debug@4.1.12': - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -3876,8 +3876,8 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sanitize-filename@1.6.3: - resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} + sanitize-filename@1.6.4: + resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} sax@1.6.0: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} @@ -4088,8 +4088,8 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - tar@7.5.11: - resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==} + tar@7.5.12: + resolution: {integrity: sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==} engines: {node: '>=18'} temp-file@3.4.0: @@ -4756,7 +4756,7 @@ snapshots: ora: 5.4.1 read-binary-file-arch: 1.0.6 semver: 7.7.4 - tar: 7.5.11 + tar: 7.5.12 yargs: 17.7.2 transitivePeerDependencies: - supports-color @@ -6177,7 +6177,7 @@ snapshots: '@types/node': 25.5.0 '@types/responselike': 1.0.3 - '@types/debug@4.1.12': + '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -6342,7 +6342,7 @@ snapshots: proper-lockfile: 4.1.2 resedit: 1.7.2 semver: 7.7.4 - tar: 7.5.11 + tar: 7.5.12 temp-file: 3.4.0 tiny-async-pool: 1.3.0 which: 5.0.0 @@ -6448,7 +6448,7 @@ snapshots: builder-util@26.8.1: dependencies: 7zip-bin: 5.2.0 - '@types/debug': 4.1.12 + '@types/debug': 4.1.13 app-builder-bin: 5.0.0-alpha.12 builder-util-runtime: 9.5.1 chalk: 4.1.2 @@ -6458,7 +6458,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 js-yaml: 4.1.1 - sanitize-filename: 1.6.3 + sanitize-filename: 1.6.4 source-map-support: 0.5.21 stat-mode: 1.0.0 temp-file: 3.4.0 @@ -6486,7 +6486,7 @@ snapshots: minipass-pipeline: 1.2.4 p-map: 7.0.4 ssri: 12.0.0 - tar: 7.5.11 + tar: 7.5.12 unique-filename: 4.0.0 cacheable-lookup@5.0.4: {} @@ -7773,7 +7773,7 @@ snapshots: nopt: 8.1.0 proc-log: 5.0.0 semver: 7.7.4 - tar: 7.5.11 + tar: 7.5.12 tinyglobby: 0.2.15 which: 5.0.0 transitivePeerDependencies: @@ -8255,7 +8255,7 @@ snapshots: safer-buffer@2.1.2: {} - sanitize-filename@1.6.3: + sanitize-filename@1.6.4: dependencies: truncate-utf8-bytes: 1.0.2 @@ -8522,7 +8522,7 @@ snapshots: tapable@2.3.0: {} - tar@7.5.11: + tar@7.5.12: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 diff --git a/resources/logo.svg b/resources/logo.svg new file mode 100644 index 00000000..79a0b764 --- /dev/null +++ b/resources/logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/src/main/index.ts b/src/main/index.ts index bca05a3b..a316b4a8 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -66,6 +66,23 @@ function createWindow(): BrowserWindow { return { action: 'deny' } }) + // File drag-and-drop: the preload script handles the drop event (because + // File.path is only available there), sends paths here, and we relay to renderer. + ipcMain.on('terminal:file-dropped-from-preload', (_event, args: { paths: string[] }) => { + if (!mainWindow.isDestroyed()) { + for (const p of args.paths) { + mainWindow.webContents.send('terminal:file-drop', { path: p }) + } + } + }) + + // Safety net: block any file:// navigation that might slip through + mainWindow.webContents.on('will-navigate', (event, url) => { + if (url.startsWith('file://')) { + event.preventDefault() + } + }) + // Handle zoom shortcuts reliably via before-input-event mainWindow.webContents.on('before-input-event', (_event, input) => { if (input.type !== 'keyDown') return @@ -101,8 +118,8 @@ app.whenReady().then(() => { electronApp.setAppUserModelId('com.stablyai.orca') app.setName('Orca') - if (process.platform === 'darwin') { - const dockIcon = nativeImage.createFromPath(is.dev ? devIcon : icon) + if (process.platform === 'darwin' && is.dev) { + const dockIcon = nativeImage.createFromPath(devIcon) app.dock?.setIcon(dockIcon) } diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index e15f0d81..a9168f1a 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -96,6 +96,7 @@ interface UIApi { set: (args: Partial) => Promise onOpenSettings: (callback: () => void) => () => void onTerminalZoom: (callback: (direction: 'in' | 'out' | 'reset') => void) => () => void + onFileDrop: (callback: (data: { path: string }) => void) => () => void getZoomLevel: () => number setZoomLevel: (level: number) => void } diff --git a/src/preload/index.ts b/src/preload/index.ts index 01818f7a..21535005 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1,6 +1,42 @@ -import { contextBridge, ipcRenderer, webFrame } from 'electron' +import { contextBridge, ipcRenderer, webFrame, webUtils } from 'electron' import { electronAPI } from '@electron-toolkit/preload' +// --------------------------------------------------------------------------- +// File drag-and-drop: handled here in the preload because webUtils (which +// resolves File objects to filesystem paths) is only available in Electron's +// preload/main worlds, not the renderer's isolated main world. +// --------------------------------------------------------------------------- +document.addEventListener( + 'dragover', + (e) => { + e.preventDefault() + if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy' + }, + true +) + +document.addEventListener( + 'drop', + (e) => { + e.preventDefault() + e.stopPropagation() + const files = e.dataTransfer?.files + if (!files || files.length === 0) return + + const paths: string[] = [] + for (let i = 0; i < files.length; i++) { + // webUtils.getPathForFile is the Electron 28+ replacement for File.path + const filePath = webUtils.getPathForFile(files[i]) + if (filePath) paths.push(filePath) + } + + if (paths.length > 0) { + ipcRenderer.send('terminal:file-dropped-from-preload', { paths }) + } + }, + true +) + // Custom APIs for renderer const api = { repos: { @@ -151,6 +187,11 @@ const api = { ipcRenderer.on('terminal:zoom', listener) return () => ipcRenderer.removeListener('terminal:zoom', listener) }, + onFileDrop: (callback: (data: { path: string }) => void): (() => void) => { + const listener = (_event: Electron.IpcRendererEvent, data: { path: string }) => callback(data) + ipcRenderer.on('terminal:file-drop', listener) + return () => ipcRenderer.removeListener('terminal:file-drop', listener) + }, getZoomLevel: (): number => webFrame.getZoomLevel(), setZoomLevel: (level: number): void => webFrame.setZoomLevel(level) } diff --git a/src/renderer/src/components/Landing.tsx b/src/renderer/src/components/Landing.tsx index 8d425063..4b4d1af0 100644 --- a/src/renderer/src/components/Landing.tsx +++ b/src/renderer/src/components/Landing.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react' import { FolderPlus, GitBranchPlus } from 'lucide-react' import { useAppStore } from '../store' -import logo from '../../../../resources/icon.png' +import logo from '../../../../resources/logo.svg' type ShortcutItem = { id: string @@ -38,11 +38,12 @@ export default function Landing(): React.JSX.Element {
- Orca logo +
+ Orca logo +

ORCA

diff --git a/src/renderer/src/components/TerminalPane.tsx b/src/renderer/src/components/TerminalPane.tsx index ed79bd55..b0553768 100644 --- a/src/renderer/src/components/TerminalPane.tsx +++ b/src/renderer/src/components/TerminalPane.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react' +import { createPortal } from 'react-dom' import type { CSSProperties } from 'react' import type { ITheme } from '@xterm/xterm' import { @@ -1159,9 +1160,32 @@ export default function TerminalPane({ ) } - // Get the search addon for the active pane + // Get the search addon for the active pane and its container for portal const activePane = managerRef.current?.getActivePane() const activeSearchAddon = activePane?.searchAddon ?? null + const activePaneContainer = activePane?.container ?? null + + // Drag & drop file paths into terminal. + // The preload script handles dragover/drop (File.path is only available there), + // sends paths to main process, which relays them here via IPC. + useEffect(() => { + if (!isActive) return + + const shellEscape = (p: string): string => { + if (/^[a-zA-Z0-9_./@:-]+$/.test(p)) return p + return "'" + p.replace(/'/g, "'\\''") + "'" + } + + return window.api.ui.onFileDrop(({ path: filePath }) => { + const manager = managerRef.current + if (!manager) return + const pane = manager.getActivePane() ?? manager.getPanes()[0] + if (!pane) return + const transport = paneTransportsRef.current.get(pane.id) + if (!transport) return + transport.sendInput(shellEscape(filePath)) + }) + }, [isActive]) return ( <> @@ -1193,11 +1217,15 @@ export default function TerminalPane({ setTerminalMenuOpen(true) }} /> - setSearchOpen(false)} - searchAddon={activeSearchAddon} - /> + {activePaneContainer && + createPortal( + setSearchOpen(false)} + searchAddon={activeSearchAddon} + />, + activePaneContainer + )}