diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index f766ac0d7f7..8b47104d292 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -4578,7 +4578,7 @@ function buildSshBlock(input, existingBlock = {}) { // and is shared by the per-profile, env, and global resolution paths. `token` // is the DECRYPTED static token (or null in OAuth mode). `source` is a label // for diagnostics ('profile' | 'env' | 'settings'). -async function buildRemoteConnection(rawUrl, authMode, token, source, remoteHost) { +async function buildRemoteConnection(rawUrl, authMode, token, source, remoteHost, remoteKind = 'url') { const baseUrl = normalizeRemoteBaseUrl(rawUrl) // For token/oauth remotes the meaningful host is the real backend URL; for // SSH remotes the caller passes the entered/resolved host explicitly (the @@ -4621,6 +4621,7 @@ async function buildRemoteConnection(rawUrl, authMode, token, source, remoteHost source, authMode: 'oauth', remoteHost: host || undefined, + remoteKind, // No static token in OAuth mode; REST is cookie-authed via the partition. token: null, wsUrl: buildGatewayWsUrlWithTicket(baseUrl, ticket) @@ -4640,6 +4641,7 @@ async function buildRemoteConnection(rawUrl, authMode, token, source, remoteHost source, authMode: 'token', remoteHost: host || undefined, + remoteKind, token, wsUrl: buildGatewayWsUrl(baseUrl, token) } @@ -4774,7 +4776,7 @@ async function bootstrapSshConnection(profile, sshConfig, reuseToken, source) { // Hand the existing token-remote machinery the loopback baseUrl. The pill's // host is the SSH host, NOT 127.0.0.1. - return buildRemoteConnection(result.baseUrl, 'token', result.token, source, hostLabel) + return buildRemoteConnection(result.baseUrl, 'token', result.token, source, hostLabel, 'ssh') } // Save the served token back into the SSH connection entry (encrypted), so a diff --git a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx index a95ac3217f5..487ce8472f7 100644 --- a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +++ b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx @@ -13,6 +13,7 @@ import { Command, Hash, Loader2, + Network, Sparkles, Terminal, Zap, @@ -47,7 +48,7 @@ import { } from '@/store/updates' import type { StatusResponse } from '@/types/hermes' -import { CRON_ROUTE } from '../../routes' +import { CRON_ROUTE, SETTINGS_ROUTE } from '../../routes' import type { StatusbarItem, StatusbarSelectModifiers } from '../statusbar-controls' interface StatusbarItemsOptions { @@ -291,6 +292,44 @@ export function useStatusbarItems({ copy ]) + // Connection-identity pill (VS Code's load-bearing "where am I?" cue). Shown + // only for remote connections; hidden in local mode (the unmarked default). + // SSH remotes read "SSH: user@host"; token/oauth remotes read "Remote: host" + // — closing the same gap for the existing remote modes. Clicking opens the + // gateway connection settings so the pill doubles as the switch/disconnect + // entry point. + const connectionItem = useMemo(() => { + if (connection?.mode !== 'remote') { + return null + } + // Prefer the host main.cjs put on the descriptor; fall back to parsing the + // backend URL (never the 127.0.0.1 tunnel — that's only the SSH baseUrl, + // and SSH descriptors always carry remoteHost). + let host = connection.remoteHost ?? '' + if (!host && connection.baseUrl) { + try { + host = new URL(connection.baseUrl).host + } catch { + host = '' + } + } + if (!host) { + return null + } + + const isSsh = connection.remoteKind === 'ssh' + const label = isSsh ? copy.connectionSsh(host) : copy.connectionRemote(host) + + return { + icon: , + id: 'connection', + label, + title: isSsh ? copy.connectionSshTooltip(host) : copy.connectionRemoteTooltip(host), + to: SETTINGS_ROUTE, + variant: 'link' + } + }, [connection?.mode, connection?.remoteHost, connection?.remoteKind, connection?.baseUrl, copy]) + const coreLeftStatusbarItems = useMemo( () => [ { @@ -421,6 +460,7 @@ export function useStatusbarItems({ title: terminalTakeover ? copy.hideTerminal : copy.showTerminal, variant: 'action' }, + ...(connectionItem ? [connectionItem] : []), clientVersionItem, ...(backendVersionItem ? [backendVersionItem] : []) ], @@ -435,6 +475,7 @@ export function useStatusbarItems({ terminalTakeover, toggleYolo, turnStartedAt, + connectionItem, clientVersionItem, backendVersionItem, yoloActive diff --git a/apps/desktop/src/global.d.ts b/apps/desktop/src/global.d.ts index 99bad3c09e7..bfcb1017bc9 100644 --- a/apps/desktop/src/global.d.ts +++ b/apps/desktop/src/global.d.ts @@ -289,6 +289,9 @@ export interface HermesConnection { // is the user@host the tunnel reaches; for token/oauth remotes it's the host // parsed from the real backend URL. Absent in local mode. remoteHost?: string + // Distinguishes an SSH-tunnelled remote ('ssh') from a direct URL remote + // ('url') so the pill can label it SSH: vs Remote:. Absent in local mode. + remoteKind?: 'ssh' | 'url' nativeOverlayWidth: number source?: 'env' | 'local' | 'settings' token: string diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index 7a3c3553a15..d2e6c4c349e 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -1622,6 +1622,10 @@ export const en: Translations = { backendVersion: version => `Backend v${version}`, clientLabel: version => `client v${version}`, backendLabel: version => `backend v${version}`, + connectionSsh: host => `SSH: ${host}`, + connectionRemote: host => `Remote: ${host}`, + connectionSshTooltip: host => `Connected over SSH to ${host} · click to manage`, + connectionRemoteTooltip: host => `Connected to remote backend ${host} · click to manage`, commit: sha => `commit ${sha}`, branch: branch => `branch ${branch}`, closeCommandCenter: 'Close Command Center', diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index 50a54d977ce..4f9b56e8189 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -1751,6 +1751,10 @@ export const ja = defineLocale({ backendVersion: version => `バックエンド v${version}`, clientLabel: version => `クライアント v${version}`, backendLabel: version => `バックエンド v${version}`, + connectionSsh: host => `SSH: ${host}`, + connectionRemote: host => `リモート: ${host}`, + connectionSshTooltip: host => `SSH 経由で ${host} に接続中 · クリックして管理`, + connectionRemoteTooltip: host => `リモートバックエンド ${host} に接続中 · クリックして管理`, commit: sha => `コミット ${sha}`, branch: branch => `ブランチ ${branch}`, closeCommandCenter: 'コマンドセンターを閉じる', diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts index 19f81f19b0f..10e5e71ad73 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -1255,6 +1255,10 @@ export interface Translations { backendVersion: (version: string) => string clientLabel: (version: string) => string backendLabel: (version: string) => string + connectionSsh: (host: string) => string + connectionRemote: (host: string) => string + connectionSshTooltip: (host: string) => string + connectionRemoteTooltip: (host: string) => string commit: (sha: string) => string branch: (branch: string) => string closeCommandCenter: string diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index efb457ee35d..23836783955 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -1694,6 +1694,10 @@ export const zhHant = defineLocale({ backendVersion: version => `後端 v${version}`, clientLabel: version => `用戶端 v${version}`, backendLabel: version => `後端 v${version}`, + connectionSsh: host => `SSH: ${host}`, + connectionRemote: host => `遠端: ${host}`, + connectionSshTooltip: host => `已透過 SSH 連線到 ${host} · 點擊管理`, + connectionRemoteTooltip: host => `已連線到遠端後端 ${host} · 點擊管理`, commit: sha => `提交 ${sha}`, branch: branch => `分支 ${branch}`, closeCommandCenter: '關閉命令中心', diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index 45549fbbe08..c2e556f8214 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -1799,6 +1799,10 @@ export const zh: Translations = { backendVersion: version => `后端 v${version}`, clientLabel: version => `客户端 v${version}`, backendLabel: version => `后端 v${version}`, + connectionSsh: host => `SSH: ${host}`, + connectionRemote: host => `远程: ${host}`, + connectionSshTooltip: host => `已通过 SSH 连接到 ${host} · 点击管理`, + connectionRemoteTooltip: host => `已连接到远程后端 ${host} · 点击管理`, commit: sha => `提交 ${sha}`, branch: branch => `分支 ${branch}`, closeCommandCenter: '关闭命令中心',