feat: harden local shell, SFTP, and desktop session reliability

Finder-launched terminals now inherit a login PATH so tools like ollama resolve, destructive SFTP/editor actions require confirmation, and terminal search plus window dragging work again. SSH forwards and RDP sessions keep listeners and survive reconnect without FastPath crashes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Logan.LiandCursor committed 2026-08-16 16:08:11 +08:00
1 parent 188f3556ed
commit 08696896f9
223 files changed
+13493 -3423

No files matched your search

@@ -121,7 +121,7 @@ export default function BatchCommandDialog({
} finally {
if (connectionId) await window.opsxApi.sshDisconnect(connectionId).catch(() => undefined)
}
}, [timeout, updateResult])
}, [timeout, updateResult, t])
const execute = useCallback(async () => {
const commandText = command.trim()
+1 -1
View File
@@ -126,7 +126,7 @@ export default function BatchExecView({ tabId, initialSessionIds = [] }: BatchEx
}
}
},
[tabId, timeout, updateResult]
[tabId, timeout, updateResult, t]
)
const execute = useCallback(async () => {
+53 -6
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Cable, Plus, Search, LogIn, Server, Terminal } from 'lucide-react'
import { Cable, Cast, Monitor, Plus, Search, LogIn, Server, Terminal } from 'lucide-react'
import Modal from '../common/Modal'
import { useSessionStore } from '../../stores/sessionStore'
import { useTabStore } from '../../stores/tabStore'
@@ -9,7 +9,11 @@ import {
type AvailableLocalShell,
} from '../../platform/localTerminal'
import { fallbackLocalShells } from '../../utils/platformShells'
import { buildOpenTabPayload } from '../../utils/sessionProtocol'
import {
buildOpenTabPayload,
defaultPortForProtocol,
sessionProtocol,
} from '../../utils/sessionProtocol'
import { useEntitlementStore } from '../../stores/entitlementStore'
interface QuickLauncherProps {
@@ -18,6 +22,8 @@ interface QuickLauncherProps {
onNewSession: () => void
onQuickConnect: () => void
onSerialConnect: () => void
onRdpConnect?: () => void
onVncConnect?: () => void
}
export default function QuickLauncher({
@@ -26,6 +32,8 @@ export default function QuickLauncher({
onNewSession,
onQuickConnect,
onSerialConnect,
onRdpConnect,
onVncConnect,
}: QuickLauncherProps) {
const { t } = useTranslation(['common', 'sessions'])
const [query, setQuery] = useState('')
@@ -86,7 +94,7 @@ export default function QuickLauncher({
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2">
<button type="button" onClick={() => run(onQuickConnect)} className="launcher-card">
<LogIn className="w-4 h-4 text-accent-blue" />
<span>
@@ -95,7 +103,7 @@ export default function QuickLauncher({
</span>
</button>
<button type="button" onClick={() => run(onNewSession)} className="launcher-card">
<Plus className="w-4 h-4 text-accent-green" />
<Plus className="w-4 h-4 shrink-0 text-accent-green" strokeWidth={2.5} />
<span>
<strong>{t('common:launcher.cards.saveSession.title')}</strong>
<small>{t('common:launcher.cards.saveSession.desc')}</small>
@@ -108,6 +116,42 @@ export default function QuickLauncher({
<small>{t('common:launcher.cards.serial.desc')}</small>
</span>
</button>
{onRdpConnect && (
<button
type="button"
onClick={() =>
run(() => {
if (!requirePro('rdp')) return
onRdpConnect()
})
}
className="launcher-card"
>
<Monitor className="w-4 h-4 text-accent-blue" />
<span>
<strong>{t('common:launcher.cards.rdp.title')}</strong>
<small>{t('common:launcher.cards.rdp.desc')}</small>
</span>
</button>
)}
{onVncConnect && (
<button
type="button"
onClick={() =>
run(() => {
if (!requirePro('vnc')) return
onVncConnect()
})
}
className="launcher-card"
>
<Cast className="w-4 h-4 text-accent-blue" />
<span>
<strong>{t('common:launcher.cards.vnc.title')}</strong>
<small>{t('common:launcher.cards.vnc.desc')}</small>
</span>
</button>
)}
</div>
<section>
@@ -131,7 +175,7 @@ export default function QuickLauncher({
}))}
className="launcher-row"
>
<Terminal className="w-4 h-4 text-emerald-400" />
<Terminal className="w-4 h-4 text-accent-green" />
<span className="flex-1 text-left"><strong>{shell.name}</strong><small>{shell.path}</small></span>
{shell.isDefault && <span className="launcher-badge">{t('common:launcher.systemDefault')}</span>}
</button>
@@ -161,7 +205,10 @@ export default function QuickLauncher({
<Server className="w-4 h-4 text-accent-blue" />
<span className="flex-1 min-w-0 text-left">
<strong className="truncate">{session.name}</strong>
<small className="truncate">{session.username}@{session.host}:{session.port || 22}</small>
<small className="truncate">
{session.username}@{session.host}:
{session.port || defaultPortForProtocol(sessionProtocol(session))}
</small>
</span>
<span className="launcher-badge">{session.group || 'default'}</span>
</button>
+78 -68
View File
@@ -20,6 +20,8 @@ import {
} from '../../utils/sessionGroups'
import { useSettingsStore } from '../../stores/settingsStore'
import { usePromptDialog } from '../common/PromptDialog'
import DesktopResolutionPicker from '../desktop/DesktopResolutionPicker'
import { getDisplayResolution } from '../../utils/desktopResolution'
const logger = createLogger('SessionDialog')
@@ -152,14 +154,17 @@ export default function SessionDialog({
: [],
domain: editSession.domain || '',
security: editSession.security || 'nla',
width: editSession.width || 1920,
height: editSession.height || 1080,
width: editSession.width || getDisplayResolution().width,
height: editSession.height || getDisplayResolution().height,
})
setShowAdvanced(hasAdvanced)
} else {
const display = getDisplayResolution()
setFormData({
...emptyForm,
group: normalizeGroupId(defaultGroup),
width: display.width,
height: display.height,
})
setTarget('')
setShowAdvanced(false)
@@ -185,12 +190,15 @@ export default function SessionDialog({
prev.port === defaultPortForProtocol(prev.protocol)
? defaultPortForProtocol(protocol)
: prev.port
const display = getDisplayResolution()
const applyDisplaySize = protocol !== 'ssh' && prev.protocol === 'ssh'
return {
...prev,
protocol,
username: nextUser,
port: nextPort,
authType: 'password',
...(applyDisplaySize ? { width: display.width, height: display.height } : {}),
}
})
// Keep host-only target when switching protocol.
@@ -394,6 +402,50 @@ export default function SessionDialog({
{ value: 'key', label: t('dialog.authKey'), icon: Key },
]
const groupField = (
<div>
<label htmlFor="session-group" className="ui-label">
<Folder className="inline w-3 h-3 mr-0.5" />
{t('dialog.group')}
</label>
<select
id="session-group"
value={normalizeGroupId(formData.group)}
onChange={(e) => {
const value = e.target.value
if (value !== '__new__') {
setFormData((prev) => ({ ...prev, group: normalizeGroupId(value) }))
return
}
void (async () => {
const raw = await prompt({
title: t('groups.new.title'),
message: t('groups.new.message'),
placeholder: t('groups.new.placeholder'),
confirmText: t('groups.new.confirm'),
})
if (raw == null) return
setFormData((prev) => ({ ...prev, group: normalizeGroupId(raw) }))
})()
}}
className="ui-input"
>
{knownGroups.map((group) => (
<option key={group} value={group}>
{groupDisplayName(group)}
</option>
))}
{!knownGroups.includes(normalizeGroupId(formData.group)) && (
<option value={normalizeGroupId(formData.group)}>
{groupDisplayName(formData.group)}
</option>
)}
<option value="__new__">{t('groups.newOption')}</option>
</select>
<p className="mt-1 text-[11px] text-txt-faint">{t('groups.hint')}</p>
</div>
)
return (
<>
<div className="fixed inset-0 z-[100] flex justify-end">
@@ -549,73 +601,30 @@ export default function SessionDialog({
/>
</div>
)}
{formData.protocol !== 'ssh' && (
<div className="grid grid-cols-2 gap-2">
<div>
<label className="ui-label" htmlFor="session-width">{t('dialog.width')}</label>
<input
id="session-width"
type="number"
value={formData.width}
onChange={(e) => setFormData({ ...formData, width: Number(e.target.value) || 1920 })}
className="ui-input font-mono"
/>
</div>
<div>
<label className="ui-label" htmlFor="session-height">{t('dialog.height')}</label>
<input
id="session-height"
type="number"
value={formData.height}
onChange={(e) => setFormData({ ...formData, height: Number(e.target.value) || 1080 })}
className="ui-input font-mono"
/>
</div>
{formData.protocol === 'rdp' && (
<div>
<label htmlFor="session-password" className="ui-label">{t('dialog.password')}</label>
<input
id="session-password"
type="password"
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
placeholder="••••••••"
className="ui-input"
autoComplete="current-password"
/>
</div>
)}
<div>
<label htmlFor="session-group" className="ui-label">
<Folder className="inline w-3 h-3 mr-0.5" />
{t('dialog.group')}
</label>
<select
id="session-group"
value={normalizeGroupId(formData.group)}
onChange={(e) => {
const value = e.target.value
if (value !== '__new__') {
setFormData((prev) => ({ ...prev, group: normalizeGroupId(value) }))
return
}
void (async () => {
const raw = await prompt({
title: t('groups.new.title'),
message: t('groups.new.message'),
placeholder: t('groups.new.placeholder'),
confirmText: t('groups.new.confirm'),
})
if (raw == null) return
setFormData((prev) => ({ ...prev, group: normalizeGroupId(raw) }))
})()
}}
className="ui-input"
>
{knownGroups.map((group) => (
<option key={group} value={group}>
{groupDisplayName(group)}
</option>
))}
{!knownGroups.includes(normalizeGroupId(formData.group)) && (
<option value={normalizeGroupId(formData.group)}>
{groupDisplayName(formData.group)}
</option>
)}
<option value="__new__">{t('groups.newOption')}</option>
</select>
<p className="mt-1 text-[11px] text-txt-faint">
{t('groups.hint')}
</p>
</div>
{formData.protocol !== 'ssh' && (
<DesktopResolutionPicker
ns="sessions"
width={formData.width}
height={formData.height}
onChange={(next) => setFormData((prev) => ({ ...prev, width: next.width, height: next.height }))}
/>
)}
{/* RDP:分组仍在连接区;SSH / VNC 放到密码下方 */}
{formData.protocol === 'rdp' && groupField}
</section>
{/* 凭据 */}
@@ -647,7 +656,7 @@ export default function SessionDialog({
</div>
)}
{(formData.authType === 'password' || !isSsh) && (
{(formData.authType === 'password' || formData.protocol === 'vnc') && (
<div>
<label htmlFor="session-password" className="ui-label">{t('dialog.password')}</label>
<input
@@ -774,6 +783,7 @@ export default function SessionDialog({
</div>
)}
{(isSsh || formData.protocol === 'vnc') && groupField}
</section>
{/* 高级 */}
+7 -2
View File
@@ -10,7 +10,11 @@ import { openLocalShellTab, type ShellOption } from '../layout/sidebar/LocalShel
import appIconUrl from '../../assets/opsxshell-app-icon.png'
import { shellDisplayFromPath } from '../../utils/shellName'
import { fallbackLocalShells, modKeyLabel } from '../../utils/platformShells'
import { buildOpenTabPayload } from '../../utils/sessionProtocol'
import {
buildOpenTabPayload,
defaultPortForProtocol,
sessionProtocol,
} from '../../utils/sessionProtocol'
interface WelcomeViewProps {
onNewSession?: () => void
@@ -273,7 +277,8 @@ export default function WelcomeView({
<span className="min-w-0 flex-1">
<span className="block truncate text-sm text-content-primary">{session.name}</span>
<span className="block truncate font-mono text-[11px] text-txt-faint">
{session.username}@{session.host}:{session.port || 22}
{session.username}@{session.host}:
{session.port || defaultPortForProtocol(sessionProtocol(session))}
</span>
</span>
{session.group ? (