mirror of
https://github.com/stablyai/orca
synced 2026-04-21 14:17:16 +00:00
terminal improvements
This commit is contained in:
parent
173965b57f
commit
b093a5ab96
10 changed files with 184 additions and 52 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
23
resources/logo.svg
Normal file
23
resources/logo.svg
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
width="318.60233"
|
||||
height="202.66667"
|
||||
viewBox="0 0 318.60232 202.66667"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
id="g1"
|
||||
style="display:inline"
|
||||
transform="translate(-6.6666669,-70.666669)">
|
||||
<path
|
||||
style="display:inline;fill:#ffffff"
|
||||
d="m 177.81311,248.33334 c 23.82304,-41.29793 40.54045,-66.84626 49.51207,-75.66667 6.81685,-6.70196 10.07373,-8.7374 20.07265,-12.54475 34.57822,-13.16655 61.04674,-26.78733 72.37222,-37.24295 9.62924,-8.88966 9.34286,-9.01142 -23.43671,-9.964 -35.71756,-1.03796 -43.72989,0.42119 -62.17546,11.323 -16.72118,9.88265 -34.20103,30.11225 -42.74704,49.47157 -2.57353,5.82985 -14.81294,44.3056 -27.96399,87.90747 -2.86036,9.48343 -3.02466,11.71633 -0.86213,11.71633 0.44382,0 7.29659,-11.25 15.22839,-25 z m -65.14644,-8.32267 C 120,239.3326 130.5,237.50979 136,235.95998 c 5.5,-1.5498 12.25,-3.13783 15,-3.52895 2.75,-0.39111 5,-0.95485 5,-1.25275 0,-0.29789 2.15135,-7.58487 4.78078,-16.19328 8.49209,-27.80201 12.21334,-40.41629 21.13747,-71.65166 4.81891,-16.86667 11.23502,-39.185 14.25802,-49.596301 5.12803,-17.66103 5.74763,-23.07037 2.64253,-23.07037 -1.84887,0 -4.07048,6.908293 -16.72243,52.000001 -21.78975,77.65896 -20.80806,74.74393 -26.84794,79.72251 -7.5925,6.25838 -25.03916,14.82524 -36.10856,17.73044 -17.0947,4.48656 -33.410599,3.86724 -53.116765,-2.01622 -18.569242,-5.54403 -23.142662,-5.80284 -33.639754,-1.9037 -5.875424,2.18242 -9.864152,5.04363 -16.716684,11.99127 -4.95,5.0187 -9.0000001,10.02884 -9.0000001,11.13364 0,1.75174 5.9276921,2.00299 46.3333351,1.96383 25.483334,-0.0247 52.333338,-0.59969 59.666668,-1.27777 z M 252.69513,104.63708 c 12.18267,-3.48651 15.77304,-7.895503 9.63821,-11.835773 -10.19296,-6.546726 -36.19849,-1.77301 -41.19436,7.561863 -1.2556,2.3461 -0.98698,3.2037 1.68353,5.375 2.69471,2.19098 4.59991,2.47691 12.53928,1.88189 5.14899,-0.3859 12.94899,-1.72824 17.33334,-2.98298 z"
|
||||
id="path1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
1
src/preload/index.d.ts
vendored
1
src/preload/index.d.ts
vendored
|
|
@ -96,6 +96,7 @@ interface UIApi {
|
|||
set: (args: Partial<PersistedUIState>) => Promise<void>
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
<div className="flex-1 flex items-center justify-center bg-background">
|
||||
<div className="w-full max-w-lg px-6">
|
||||
<div className="flex flex-col items-center gap-4 py-8">
|
||||
<img
|
||||
src={logo}
|
||||
alt="Orca logo"
|
||||
className="size-16 rounded-xl shadow-lg shadow-black/40"
|
||||
/>
|
||||
<div
|
||||
className="flex items-center justify-center size-20 rounded-2xl border border-border/80 shadow-lg shadow-black/40"
|
||||
style={{ backgroundColor: '#12181e' }}
|
||||
>
|
||||
<img src={logo} alt="Orca logo" className="size-12" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold text-foreground tracking-tight">ORCA</h1>
|
||||
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
<TerminalSearch
|
||||
isOpen={searchOpen}
|
||||
onClose={() => setSearchOpen(false)}
|
||||
searchAddon={activeSearchAddon}
|
||||
/>
|
||||
{activePaneContainer &&
|
||||
createPortal(
|
||||
<TerminalSearch
|
||||
isOpen={searchOpen}
|
||||
onClose={() => setSearchOpen(false)}
|
||||
searchAddon={activeSearchAddon}
|
||||
/>,
|
||||
activePaneContainer
|
||||
)}
|
||||
<DropdownMenu open={terminalMenuOpen} onOpenChange={setTerminalMenuOpen} modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -366,8 +366,28 @@ export class PaneManager {
|
|||
const fitAddon = new FitAddon()
|
||||
const searchAddon = new SearchAddon()
|
||||
const unicode11Addon = new Unicode11Addon()
|
||||
// URL tooltip element — Ghostty-style bottom-left hint on hover
|
||||
const linkTooltip = document.createElement('div')
|
||||
linkTooltip.className = 'pane-link-tooltip'
|
||||
linkTooltip.style.cssText =
|
||||
'display:none;position:absolute;bottom:4px;left:8px;z-index:40;' +
|
||||
'padding:2px 8px;border-radius:4px;font-size:11px;font-family:inherit;' +
|
||||
'color:#a1a1aa;background:rgba(24,24,27,0.85);border:1px solid rgba(63,63,70,0.6);' +
|
||||
'pointer-events:none;max-width:80%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;'
|
||||
container.appendChild(linkTooltip)
|
||||
|
||||
const webLinksAddon = new WebLinksAddon(
|
||||
this.options.onLinkClick ? (_event, uri) => this.options.onLinkClick!(uri) : undefined
|
||||
this.options.onLinkClick ? (_event, uri) => this.options.onLinkClick!(uri) : undefined,
|
||||
{
|
||||
hover: (event, uri) => {
|
||||
if (event.type === 'mouseover' && uri) {
|
||||
linkTooltip.textContent = uri
|
||||
linkTooltip.style.display = ''
|
||||
} else {
|
||||
linkTooltip.style.display = 'none'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const pane: ManagedPaneInternal = {
|
||||
|
|
|
|||
|
|
@ -5,29 +5,30 @@ export const TERMINAL_THEMES: Record<string, ITheme> = {
|
|||
// Defaults
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
// Exact colors from Ghostty source: src/terminal/color.zig + src/config/Config.zig
|
||||
'Ghostty Default Style Dark': {
|
||||
background: '#282c34',
|
||||
foreground: '#c0c5ce',
|
||||
cursor: '#c0c5ce',
|
||||
foreground: '#ffffff',
|
||||
cursor: '#ffffff',
|
||||
cursorAccent: '#282c34',
|
||||
selectionBackground: '#3e4451',
|
||||
selectionForeground: '#c0c5ce',
|
||||
black: '#1d2021',
|
||||
red: '#cc241d',
|
||||
green: '#98971a',
|
||||
yellow: '#d79921',
|
||||
blue: '#458588',
|
||||
magenta: '#b16286',
|
||||
cyan: '#689d6a',
|
||||
white: '#a89984',
|
||||
brightBlack: '#928374',
|
||||
brightRed: '#fb4934',
|
||||
brightGreen: '#b8bb26',
|
||||
brightYellow: '#fabd2f',
|
||||
brightBlue: '#83a598',
|
||||
brightMagenta: '#d3869b',
|
||||
brightCyan: '#8ec07c',
|
||||
brightWhite: '#ebdbb2'
|
||||
selectionForeground: '#ffffff',
|
||||
black: '#1d1f21',
|
||||
red: '#cc6666',
|
||||
green: '#b5bd68',
|
||||
yellow: '#f0c674',
|
||||
blue: '#81a2be',
|
||||
magenta: '#b294bb',
|
||||
cyan: '#8abeb7',
|
||||
white: '#c5c8c6',
|
||||
brightBlack: '#666666',
|
||||
brightRed: '#d54e53',
|
||||
brightGreen: '#b9ca4a',
|
||||
brightYellow: '#e7c547',
|
||||
brightBlue: '#7aa6da',
|
||||
brightMagenta: '#c397d8',
|
||||
brightCyan: '#70c0b1',
|
||||
brightWhite: '#eaeaea'
|
||||
},
|
||||
|
||||
'Builtin Tango Light': {
|
||||
|
|
|
|||
Loading…
Reference in a new issue