feat(desktop): statusbar connection pill (SSH:/Remote: host) + i18n locale parity

Add a persistent connection-identity pill to the right-hand statusbar so the
user always knows WHERE a command lands — VS Code's load-bearing "am I local
or remote?" safety cue. More important in SSH mode precisely because the
experience is meant to feel identical to local.

- use-statusbar-items.tsx: new connectionItem rendered next to the version
  pills when connection.mode === "remote". SSH remotes read "SSH: user@host";
  token/oauth remotes read "Remote: host" — a free win that closes the same
  "where am I?" gap for the existing remote modes. Hidden in local mode.
  Clicking navigates to Settings (SETTINGS_ROUTE), so the pill doubles as the
  switch/disconnect entry point. Network (server) icon, distinct from the
  version Hash icon.
- main.cjs: buildRemoteConnection gains a remoteKind ("ssh" | "url") on every
  descriptor; the SSH bootstrap passes "ssh" so the pill can label it. The host
  is the SSH user@host (or the real backend host for url remotes), never the
  127.0.0.1 tunnel.
- global.d.ts: HermesConnection.remoteKind.
- i18n: connectionSsh / connectionRemote / *Tooltip added to types.ts AND every
  shipped locale (en, ja, zh, zh-hant) — locale parity.

Renderer typecheck/vitest deferred (no node_modules in the worktree on this
host). Delimiter/marker + i18n key-parity checks pass; main.cjs node --check OK.
This commit is contained in:
yoniebans 2026-06-19 11:59:00 +02:00
parent fce5aaae4b
commit 56cded1ae1
8 changed files with 69 additions and 3 deletions

View file

@ -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

View file

@ -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<StatusbarItem | null>(() => {
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: <Network className="size-3" />,
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<readonly StatusbarItem[]>(
() => [
{
@ -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

View file

@ -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

View file

@ -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',

View file

@ -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: 'コマンドセンターを閉じる',

View file

@ -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

View file

@ -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: '關閉命令中心',

View file

@ -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: '关闭命令中心',