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:
1 parent
188f3556ed
commit
08696896f9
223 files changed
+13493
-3423
No files matched your search
@@ -7,7 +7,7 @@ import type { ConnectionState } from '../../stores/connection'
|
||||
import { useConnectionPanelStore } from '../../stores/connectionPanelStore'
|
||||
import { useSessionStore, useEntitlementStore } from '../../stores'
|
||||
import type { HostMetrics } from '../../../shared/types/host-metrics.types'
|
||||
import { formatLocaleTime } from '../../i18n/format'
|
||||
import { formatLocaleTime, translateErrorMessage } from '../../i18n/format'
|
||||
import { useToast } from '../../hooks/useToast'
|
||||
import ForwardRuleDrawer, {
|
||||
DEFAULT_DYNAMIC_PORT,
|
||||
@@ -17,8 +17,18 @@ import ForwardRuleDrawer, {
|
||||
import {
|
||||
parseForwardRules,
|
||||
portForwardStorageKey,
|
||||
resolveForwardTargetHost,
|
||||
resolveLocalBindHost,
|
||||
resolveRemoteBindHost,
|
||||
type ForwardRule,
|
||||
} from '../connection/forwardRules'
|
||||
import {
|
||||
forgetForwardIntent,
|
||||
forwardIntentKey,
|
||||
rememberForwardIntent,
|
||||
restoreForwardIntent,
|
||||
tunnelCoversRule,
|
||||
} from '../connection/forwardIntent'
|
||||
import type { SSHForwardStatus } from '../../../shared/types/ipc.types'
|
||||
|
||||
// ============== Status Display Config ==============
|
||||
@@ -34,37 +44,37 @@ const STATUS_CONFIG: Record<string, Omit<StatusDisplay, 'labelKey'> & { labelKey
|
||||
connected: {
|
||||
icon: <Wifi className="w-3 h-3" />,
|
||||
labelKey: 'status.connected',
|
||||
colorClass: 'text-green-400',
|
||||
colorClass: 'text-feedback-success',
|
||||
animateClass: '',
|
||||
},
|
||||
connecting: {
|
||||
icon: <Loader2 className="w-3 h-3 animate-spin" />,
|
||||
labelKey: 'status.connecting',
|
||||
colorClass: 'text-yellow-400',
|
||||
colorClass: 'text-feedback-warning',
|
||||
animateClass: 'animate-pulse',
|
||||
},
|
||||
background: {
|
||||
icon: <Wifi className="w-3 h-3" />,
|
||||
labelKey: 'status.background',
|
||||
colorClass: 'text-green-400',
|
||||
colorClass: 'text-feedback-success',
|
||||
animateClass: '',
|
||||
},
|
||||
suspended: {
|
||||
icon: <WifiOff className="w-3 h-3" />,
|
||||
labelKey: 'status.suspended',
|
||||
colorClass: 'text-yellow-400',
|
||||
colorClass: 'text-feedback-warning',
|
||||
animateClass: '',
|
||||
},
|
||||
reconnecting: {
|
||||
icon: <Loader2 className="w-3 h-3 animate-spin" />,
|
||||
labelKey: 'status.reconnecting',
|
||||
colorClass: 'text-yellow-400',
|
||||
colorClass: 'text-feedback-warning',
|
||||
animateClass: 'animate-pulse',
|
||||
},
|
||||
error: {
|
||||
icon: <AlertCircle className="w-3 h-3" />,
|
||||
labelKey: 'status.error',
|
||||
colorClass: 'text-red-400',
|
||||
colorClass: 'text-feedback-error',
|
||||
animateClass: '',
|
||||
},
|
||||
disconnected: {
|
||||
@@ -127,7 +137,7 @@ function formatCpuPercent(pct: number): string {
|
||||
|
||||
function pctClass(pct?: number): string {
|
||||
if (pct == null) return 'text-txt-faint'
|
||||
if (pct >= WARN_PCT) return 'text-amber-400'
|
||||
if (pct >= WARN_PCT) return 'text-feedback-warning'
|
||||
return 'text-txt-muted'
|
||||
}
|
||||
|
||||
@@ -382,7 +392,6 @@ export default function StatusBar() {
|
||||
connectionState: undefined,
|
||||
hostLabel: undefined,
|
||||
stats: undefined,
|
||||
tabKind: activeTab?.type as string | undefined,
|
||||
connectionId: undefined,
|
||||
isRemoteSsh: false,
|
||||
sessionOsPretty: undefined as string | undefined,
|
||||
@@ -399,7 +408,6 @@ export default function StatusBar() {
|
||||
: activeTab.title
|
||||
: activeTab.title,
|
||||
stats: undefined,
|
||||
tabKind: activeTab.type,
|
||||
connectionId: undefined,
|
||||
isRemoteSsh: false,
|
||||
sessionOsPretty: undefined as string | undefined,
|
||||
@@ -411,7 +419,6 @@ export default function StatusBar() {
|
||||
connectionState: mapTabStatusToConnectionState(activeTab.connectionStatus),
|
||||
hostLabel: t('terminal:toolbar.labelLocal'),
|
||||
stats: undefined,
|
||||
tabKind: activeTab.type,
|
||||
connectionId: undefined,
|
||||
isRemoteSsh: false,
|
||||
sessionOsPretty: undefined as string | undefined,
|
||||
@@ -431,7 +438,6 @@ export default function StatusBar() {
|
||||
connectionState: conn.state,
|
||||
hostLabel: `${conn.config.username}@${conn.config.host}:${conn.config.port || 22}`,
|
||||
stats: conn.stats,
|
||||
tabKind: activeTab.type,
|
||||
connectionId: instance.connectionId,
|
||||
isRemoteSsh: true,
|
||||
sessionOsPretty,
|
||||
@@ -444,7 +450,6 @@ export default function StatusBar() {
|
||||
connectionState: mapTabStatusToConnectionState(activeTab.connectionStatus),
|
||||
hostLabel: `${session.username}@${session.host}:${session.port || 22}`,
|
||||
stats: undefined,
|
||||
tabKind: activeTab.type,
|
||||
connectionId: instance?.connectionId,
|
||||
isRemoteSsh: Boolean(instance?.connectionId),
|
||||
sessionOsPretty,
|
||||
@@ -461,7 +466,6 @@ export default function StatusBar() {
|
||||
connectionState: state,
|
||||
hostLabel: label,
|
||||
stats: undefined,
|
||||
tabKind: activeTab.type,
|
||||
connectionId: instance?.connectionId,
|
||||
isRemoteSsh: Boolean(instance?.connectionId),
|
||||
sessionOsPretty,
|
||||
@@ -481,6 +485,11 @@ export default function StatusBar() {
|
||||
return portForwardStorageKey(activeTab?.sessionId, activeTabId, connectionId)
|
||||
}, [activeTab?.sessionId, activeTabId, connectionId])
|
||||
|
||||
const intentKey = useMemo(
|
||||
() => (activeTabId ? forwardIntentKey(activeTab?.sessionId, activeTabId) : null),
|
||||
[activeTab?.sessionId, activeTabId]
|
||||
)
|
||||
|
||||
const forwardRuntimes = useMemo(() => {
|
||||
const map = new Map<string, { running: boolean; boundPort?: number }>()
|
||||
for (const tunnel of forwardTunnels) {
|
||||
@@ -489,10 +498,13 @@ export default function StatusBar() {
|
||||
boundPort: tunnel.boundPort,
|
||||
})
|
||||
}
|
||||
const dynamic = forwardRules.find((rule) => rule.type === 'dynamic')
|
||||
const dynamicTunnel = forwardTunnels.find((tunnel) => tunnel.kind === 'dynamic' && tunnel.running)
|
||||
if (dynamic && dynamicTunnel && !map.get(dynamic.id)?.running) {
|
||||
map.set(dynamic.id, { running: true, boundPort: dynamicTunnel.boundPort })
|
||||
// Rule ids can diverge from runtime ids (reconnect / legacy). Match by kind+listen port.
|
||||
for (const rule of forwardRules) {
|
||||
if (map.get(rule.id)?.running) continue
|
||||
const tunnel = forwardTunnels.find((item) => tunnelCoversRule([item], rule))
|
||||
if (tunnel) {
|
||||
map.set(rule.id, { running: true, boundPort: tunnel.boundPort })
|
||||
}
|
||||
}
|
||||
return map
|
||||
}, [forwardRules, forwardTunnels])
|
||||
@@ -585,6 +597,11 @@ export default function StatusBar() {
|
||||
return () => window.clearInterval(timer)
|
||||
}, [socksAvailable, loadForwardRules, refreshForwardState])
|
||||
|
||||
const previousForwardConnectionRef = useRef<string | undefined>(undefined)
|
||||
useEffect(() => {
|
||||
previousForwardConnectionRef.current = undefined
|
||||
}, [intentKey])
|
||||
|
||||
const openForwardDrawer = useCallback(() => {
|
||||
if (!connectionId || !activeTabId) return
|
||||
if (!requirePro('portForward')) return
|
||||
@@ -594,6 +611,10 @@ export default function StatusBar() {
|
||||
...emptyForwardDraft(),
|
||||
type: 'local',
|
||||
name: t('common:portForward.defaultLocalName'),
|
||||
localBindHost: '127.0.0.1',
|
||||
localPort: 8080,
|
||||
remoteHost: '127.0.0.1',
|
||||
remotePort: 80,
|
||||
})
|
||||
setForwardDrawerError(null)
|
||||
setForwardDrawerOpen(true)
|
||||
@@ -617,17 +638,19 @@ export default function StatusBar() {
|
||||
result = await window.opsxApi.sshForwardLocal(
|
||||
connectionId,
|
||||
rule.localPort,
|
||||
rule.remoteHost || '',
|
||||
resolveForwardTargetHost(rule.remoteHost),
|
||||
rule.remotePort || 0,
|
||||
rule.id
|
||||
rule.id,
|
||||
resolveLocalBindHost(rule.localBindHost)
|
||||
)
|
||||
} else if (rule.type === 'remote') {
|
||||
result = await window.opsxApi.sshForwardRemote(
|
||||
connectionId,
|
||||
rule.remotePort || 0,
|
||||
'127.0.0.1',
|
||||
resolveForwardTargetHost(rule.remoteHost),
|
||||
rule.localPort,
|
||||
rule.id
|
||||
rule.id,
|
||||
resolveRemoteBindHost(rule.remoteBindHost)
|
||||
)
|
||||
} else {
|
||||
result = await window.opsxApi.sshForwardDynamic(
|
||||
@@ -637,48 +660,73 @@ export default function StatusBar() {
|
||||
)
|
||||
}
|
||||
if (!result?.success) {
|
||||
setForwardDrawerError(result?.error || t('common:portForward.errorStartFailed'))
|
||||
setForwardDrawerError(
|
||||
result?.error
|
||||
? translateErrorMessage(result.error)
|
||||
: t('common:portForward.errorStartFailed')
|
||||
)
|
||||
return false
|
||||
}
|
||||
if (intentKey) rememberForwardIntent(intentKey, rule)
|
||||
await refreshForwardState()
|
||||
return true
|
||||
},
|
||||
[connectionId, refreshForwardState, t]
|
||||
[connectionId, intentKey, refreshForwardState, t]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!socksAvailable || !connectionId || !intentKey) return
|
||||
const previous = previousForwardConnectionRef.current
|
||||
previousForwardConnectionRef.current = connectionId
|
||||
if (!previous || previous === connectionId) return
|
||||
void (async () => {
|
||||
const listed = await window.opsxApi?.sshForwardList?.(connectionId)
|
||||
const tunnels = listed?.success ? listed.tunnels || [] : []
|
||||
const restored = await restoreForwardIntent(
|
||||
intentKey,
|
||||
(rule) => tunnelCoversRule(tunnels, rule),
|
||||
(rule) => startForwardRule(rule)
|
||||
)
|
||||
if (restored > 0) {
|
||||
toast.success(t('common:statusBar.forwardResumed', { count: restored }))
|
||||
}
|
||||
})()
|
||||
}, [socksAvailable, connectionId, intentKey, startForwardRule, t, toast])
|
||||
|
||||
const stopForwardRule = useCallback(
|
||||
async (rule: ForwardRule): Promise<boolean> => {
|
||||
if (!window.opsxApi?.sshForwardStop) return false
|
||||
const result = await window.opsxApi.sshForwardStop(rule.id)
|
||||
if (!result?.success) {
|
||||
// Dynamic may have been started with a different id historically.
|
||||
if (rule.type === 'dynamic' && connectionId) {
|
||||
const listed = await window.opsxApi.sshForwardList(connectionId)
|
||||
const match = listed?.tunnels?.find((tunnel) => tunnel.kind === 'dynamic' && tunnel.running)
|
||||
if (match) {
|
||||
const stopMatch = await window.opsxApi.sshForwardStop(match.forwardId)
|
||||
if (!stopMatch?.success) {
|
||||
setForwardDrawerError(
|
||||
stopMatch?.error || result?.error || t('common:portForward.errorStopFailed')
|
||||
)
|
||||
return false
|
||||
}
|
||||
await refreshForwardState()
|
||||
return true
|
||||
}
|
||||
let result = await window.opsxApi.sshForwardStop(rule.id)
|
||||
if (!result?.success && connectionId) {
|
||||
// Runtime id may differ from persisted rule id after reconnect.
|
||||
const listed = await window.opsxApi.sshForwardList(connectionId)
|
||||
const match = listed?.tunnels?.find((tunnel) => tunnelCoversRule([tunnel], rule))
|
||||
if (match) {
|
||||
result = await window.opsxApi.sshForwardStop(match.forwardId)
|
||||
}
|
||||
setForwardDrawerError(result?.error || t('common:portForward.errorStopFailed'))
|
||||
}
|
||||
if (!result?.success) {
|
||||
setForwardDrawerError(
|
||||
result?.error
|
||||
? translateErrorMessage(result.error)
|
||||
: t('common:portForward.errorStopFailed')
|
||||
)
|
||||
return false
|
||||
}
|
||||
if (intentKey) forgetForwardIntent(intentKey, rule.id)
|
||||
await refreshForwardState()
|
||||
return true
|
||||
},
|
||||
[connectionId, refreshForwardState, t]
|
||||
[connectionId, intentKey, refreshForwardState, t]
|
||||
)
|
||||
|
||||
const handleStartRule = useCallback(
|
||||
async (rule: ForwardRule) => {
|
||||
if (!connectionId || forwardSaving) return
|
||||
if (forwardRuntimes.get(rule.id)?.running) {
|
||||
toast.success(t('common:statusBar.forwardStarted'))
|
||||
return
|
||||
}
|
||||
setForwardSaving(true)
|
||||
setForwardDrawerError(null)
|
||||
try {
|
||||
@@ -701,16 +749,18 @@ export default function StatusBar() {
|
||||
}
|
||||
}
|
||||
const ok = await startForwardRule(target)
|
||||
if (ok && target.type === 'dynamic') {
|
||||
if (ok) {
|
||||
toast.success(
|
||||
t('common:statusBar.forwardStartedDynamic', {
|
||||
port: resolveDynamicPort(target.localPort),
|
||||
})
|
||||
target.type === 'dynamic'
|
||||
? t('common:statusBar.forwardStartedDynamic', {
|
||||
port: resolveDynamicPort(target.localPort),
|
||||
})
|
||||
: t('common:statusBar.forwardStarted')
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
setForwardDrawerError(
|
||||
(error instanceof Error ? error.message : String(error)) ||
|
||||
translateErrorMessage(error instanceof Error ? error.message : String(error)) ||
|
||||
t('common:portForward.errorStartFailed')
|
||||
)
|
||||
} finally {
|
||||
@@ -720,6 +770,7 @@ export default function StatusBar() {
|
||||
[
|
||||
connectionId,
|
||||
forwardRules,
|
||||
forwardRuntimes,
|
||||
forwardSaving,
|
||||
persistForwardRules,
|
||||
startForwardRule,
|
||||
@@ -800,11 +851,19 @@ export default function StatusBar() {
|
||||
return
|
||||
}
|
||||
if (!validPort(forwardDraft.remotePort)) {
|
||||
setForwardDrawerError(t('common:portForward.errorTargetPort'))
|
||||
setForwardDrawerError(
|
||||
forwardDraft.type === 'local'
|
||||
? t('common:portForward.errorTargetAddress')
|
||||
: t('common:portForward.errorRemoteListenAddress')
|
||||
)
|
||||
return
|
||||
}
|
||||
if (forwardDraft.type === 'local' && !forwardDraft.remoteHost?.trim()) {
|
||||
setForwardDrawerError(t('common:portForward.errorRemoteHost'))
|
||||
if (!forwardDraft.remoteHost?.trim()) {
|
||||
setForwardDrawerError(
|
||||
forwardDraft.type === 'local'
|
||||
? t('common:portForward.errorTargetAddress')
|
||||
: t('common:portForward.errorLocalTargetAddress')
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -813,7 +872,15 @@ export default function StatusBar() {
|
||||
type: forwardDraft.type || 'local',
|
||||
name: forwardDraft.name.trim(),
|
||||
localPort: forwardDraft.localPort,
|
||||
remoteHost: forwardDraft.remoteHost?.trim(),
|
||||
localBindHost:
|
||||
forwardDraft.type === 'local'
|
||||
? resolveLocalBindHost(forwardDraft.localBindHost)
|
||||
: undefined,
|
||||
remoteBindHost:
|
||||
forwardDraft.type === 'remote'
|
||||
? resolveRemoteBindHost(forwardDraft.remoteBindHost)
|
||||
: undefined,
|
||||
remoteHost: resolveForwardTargetHost(forwardDraft.remoteHost),
|
||||
remotePort: forwardDraft.remotePort,
|
||||
}
|
||||
|
||||
@@ -836,7 +903,7 @@ export default function StatusBar() {
|
||||
toast.success(t('common:statusBar.forwardStarted'))
|
||||
} catch (error) {
|
||||
setForwardDrawerError(
|
||||
(error instanceof Error ? error.message : String(error)) ||
|
||||
translateErrorMessage(error instanceof Error ? error.message : String(error)) ||
|
||||
t('common:portForward.errorStartFailed')
|
||||
)
|
||||
} finally {
|
||||
@@ -955,7 +1022,9 @@ export default function StatusBar() {
|
||||
}`}
|
||||
>
|
||||
{forwardSaving ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Globe className="h-3.5 w-3.5" />}
|
||||
<span className="tabular-nums">{t('common:statusBar.socksLabel')}</span>
|
||||
<span className="tabular-nums">
|
||||
{t('common:statusBar.socksLabel')} :{dynamicListenPort}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -972,6 +1041,7 @@ export default function StatusBar() {
|
||||
rules={forwardRules}
|
||||
runtimes={forwardRuntimes}
|
||||
busy={forwardSaving}
|
||||
viaSession={hostLabel}
|
||||
onChange={(next) => {
|
||||
setForwardDraft({
|
||||
type: next.type || 'local',
|
||||
@@ -980,6 +1050,14 @@ export default function StatusBar() {
|
||||
next.type === 'dynamic'
|
||||
? resolveDynamicPort(next.localPort)
|
||||
: next.localPort || 0,
|
||||
localBindHost:
|
||||
next.type === 'local'
|
||||
? resolveLocalBindHost(next.localBindHost)
|
||||
: '',
|
||||
remoteBindHost:
|
||||
next.type === 'remote'
|
||||
? resolveRemoteBindHost(next.remoteBindHost)
|
||||
: '',
|
||||
remoteHost: next.remoteHost || '',
|
||||
remotePort: next.remotePort || 0,
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user