mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
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:
parent
fce5aaae4b
commit
56cded1ae1
8 changed files with 69 additions and 3 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
3
apps/desktop/src/global.d.ts
vendored
3
apps/desktop/src/global.d.ts
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: 'コマンドセンターを閉じる',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: '關閉命令中心',
|
||||
|
|
|
|||
|
|
@ -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: '关闭命令中心',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue