From afce441ea0ff241b9c77d866f9bee5c75299fa6b Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Thu, 30 Jul 2026 04:32:15 -0700 Subject: [PATCH] fix(web-ui): make remote workspace rows legible in the nav panel The remote connection line rendered as a bare status dot plus raw text at 10px `--color-text-muted` with an additional `opacity: 0.75`. Double-dimmed it fell well below readable contrast, and because the dot sat inline in the text flow the host name started ~11px right of the workspace title with nothing to justify the offset. Render the connection as a chip that reuses the existing branch-pill language, so its left edge aligns exactly with the title text and the dot reads as part of a deliberate group: - Single-dimmed `--color-text-secondary`; error/disconnected/connecting carry semantic background and text colour. - Status shown in words only when it needs attention; a green dot alone suffices for the connected case, so the healthy state stays quiet. - Localized tooltip (connection, host, status) and a translated aria-label replacing the raw enum that screen readers were reading out. - `Server` icon for remote workspaces instead of the same `FolderOpen` local ones use. Remote rows previously kept the single-line 30px title block and hung the subtitle beneath it, giving a top-heavy title with an orphaned second line in a 43px row. Collapse the title block so both lines read as one centred unit at 38px, and transition the hover gutter so revealing row actions glides instead of snapping. Also drop the dead `.is-unknown` rule that was overridden by its own later declaration, and fold its colour into the dot's base. --- .../sections/workspaces/WorkspaceItem.tsx | 68 ++++++++++++--- .../workspaces/WorkspaceListSection.scss | 85 +++++++++++++++---- src/web-ui/src/locales/en-US/common.json | 11 +++ src/web-ui/src/locales/zh-CN/common.json | 11 +++ src/web-ui/src/locales/zh-TW/common.json | 11 +++ 5 files changed, 160 insertions(+), 26 deletions(-) diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx index 9d3045a8d8..238d05a3f3 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx @@ -1,6 +1,6 @@ import React, { lazy, Suspense, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'; import { createPortal } from 'react-dom'; -import { Folder, FolderOpen, MoreHorizontal, FolderSearch, Plus, ChevronDown, Trash2, RotateCcw, Copy, FileText, Bot, Link2, ListChecks, Loader2, Clock3, ShieldCheck, Pencil } from 'lucide-react'; +import { Folder, FolderOpen, MoreHorizontal, FolderSearch, Plus, ChevronDown, Trash2, RotateCcw, Copy, FileText, Bot, Link2, ListChecks, Loader2, Clock3, ShieldCheck, Pencil, Server } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { DotMatrixArrowRightIcon } from './DotMatrixArrowRightIcon'; import { Button, ConfirmDialog, InputDialog, Modal, Tooltip } from '@/component-library'; @@ -202,6 +202,35 @@ const WorkspaceItem: React.FC = ({ ? sshContext.workspaceStatuses[workspace.connectionId] : undefined; + const remoteMeta = useMemo(() => { + if (!workspaceIsRemote) { + return null; + } + + const status = remoteConnStatus ?? 'unknown'; + const statusLabel = t(`nav.workspaces.remote.status.${status}`, { + defaultValue: t('nav.workspaces.remote.status.unknown'), + }); + const connectionLabel = workspace.connectionName?.trim() || workspace.sshHost?.trim() || ''; + + return { + status, + statusLabel, + connectionLabel, + /** A green dot already reads as "fine"; spell out only the states that need attention. */ + showStatusText: status !== 'connected', + tooltip: t('nav.workspaces.remote.tooltip', { + connection: connectionLabel, + host: workspace.sshHost?.trim() || connectionLabel, + status: statusLabel, + }), + ariaLabel: t('nav.workspaces.remote.ariaLabel', { + connection: connectionLabel, + status: statusLabel, + }), + }; + }, [remoteConnStatus, t, workspace.connectionName, workspace.sshHost, workspaceIsRemote]); + const searchIndexIndicator = useMemo(() => { if (!canShowSearchIndex) { return null; @@ -1016,6 +1045,8 @@ const WorkspaceItem: React.FC = ({ + ) : workspaceIsRemote ? ( + ) : ( )} @@ -1026,7 +1057,7 @@ const WorkspaceItem: React.FC = ({
-
+
- {isRemoteWorkspace(workspace) && ( - - - {workspace.connectionName} - + {remoteMeta && ( +
+ + + + +
)}
diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss index ec63d296b0..314c24bf95 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss +++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceListSection.scss @@ -258,11 +258,29 @@ display: flex; flex-direction: column; align-items: stretch; + justify-content: center; gap: 1px; flex: 1 1 0; min-width: 0; max-width: 100%; padding: 0 4px; + /** Row actions appear on hover and widen the reserved gutter — glide instead of snapping. */ + transition: padding-right $motion-fast $easing-standard; + + /** + * Remote rows carry a second metadata line. Collapse the single-line 30px + * title block so both lines read as one centred unit at the same row rhythm + * as local workspaces instead of a top-heavy title with an orphaned subtitle. + */ + &.is-remote { + gap: 1px; + padding-top: 2px; + padding-bottom: 2px; + + .bitfun-nav-panel__workspace-item-name-btn { + min-height: 17px; + } + } } &__workspace-item-name-row { @@ -448,48 +466,83 @@ } &__workspace-item-subtitle { + display: flex; + align-items: center; + min-width: 0; + max-width: 100%; + } + + /** + * Remote connection chip. Shares the branch-pill language so remote metadata + * reads as a deliberate part of the row rather than a stray second line. + */ + &__workspace-item-remote { display: inline-flex; align-items: center; - gap: 4px; + gap: 5px; min-width: 0; max-width: 100%; - color: var(--color-text-muted); + padding: 0 6px 0 5px; + border-radius: 999px; + background: color-mix(in srgb, var(--element-bg-medium) 62%, transparent); + color: var(--color-text-secondary); font-size: var(--font-size-xxs); - line-height: 1.2; - opacity: 0.75; + line-height: 1.6; + cursor: default; + transition: background $motion-fast $easing-standard, + color $motion-fast $easing-standard; - svg { - flex-shrink: 0; - color: var(--color-success); + &.is-error, + &.is-disconnected { + background: color-mix(in srgb, var(--color-error) 14%, transparent); + color: var(--color-error); } - span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + &.is-connecting { + background: color-mix(in srgb, var(--color-warning) 14%, transparent); + color: var(--color-warning); } } + &__workspace-item-remote-name { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__workspace-item-remote-status { + flex-shrink: 0; + white-space: nowrap; + opacity: 0.85; + + &::before { + content: '·'; + margin-right: 4px; + opacity: 0.6; + } + } + + &__workspace-item:not(.is-active) &__workspace-item-remote { + opacity: 0.8; + } + &__workspace-item-status-dot { flex-shrink: 0; width: 6px; height: 6px; border-radius: 50%; + background: var(--color-text-muted); &.is-connected { background: var(--color-success); } - &.is-unknown, &.is-error, &.is-disconnected { background: var(--color-error); } - &.is-unknown { - background: var(--color-text-muted); - } - &.is-connecting { background: var(--color-warning); animation: bitfun-status-dot-pulse 1.4s ease-in-out infinite; diff --git a/src/web-ui/src/locales/en-US/common.json b/src/web-ui/src/locales/en-US/common.json index 6bd6a25737..121c997f84 100644 --- a/src/web-ui/src/locales/en-US/common.json +++ b/src/web-ui/src/locales/en-US/common.json @@ -386,6 +386,17 @@ "projects": "Projects", "assistants": "Personal Assistants" }, + "remote": { + "tooltip": "Remote workspace · {{connection}} ({{host}})\nConnection: {{status}}", + "ariaLabel": "Remote connection {{connection}}, {{status}}", + "status": { + "connected": "Connected", + "connecting": "Connecting", + "disconnected": "Disconnected", + "error": "Connection error", + "unknown": "Status unknown" + } + }, "actions": { "more": "More actions", "newSession": "New session", diff --git a/src/web-ui/src/locales/zh-CN/common.json b/src/web-ui/src/locales/zh-CN/common.json index 54fd55f3ac..40389c837b 100644 --- a/src/web-ui/src/locales/zh-CN/common.json +++ b/src/web-ui/src/locales/zh-CN/common.json @@ -386,6 +386,17 @@ "projects": "普通工作区", "assistants": "个人助理" }, + "remote": { + "tooltip": "远程工作区 · {{connection}}({{host}})\n连接状态:{{status}}", + "ariaLabel": "远程连接 {{connection}},{{status}}", + "status": { + "connected": "已连接", + "connecting": "连接中", + "disconnected": "已断开", + "error": "连接异常", + "unknown": "状态未知" + } + }, "actions": { "more": "更多操作", "newSession": "新建会话", diff --git a/src/web-ui/src/locales/zh-TW/common.json b/src/web-ui/src/locales/zh-TW/common.json index 8522d29945..4cf136fb7d 100644 --- a/src/web-ui/src/locales/zh-TW/common.json +++ b/src/web-ui/src/locales/zh-TW/common.json @@ -386,6 +386,17 @@ "projects": "普通工作區", "assistants": "個人助理" }, + "remote": { + "tooltip": "遠端工作區 · {{connection}}({{host}})\n連線狀態:{{status}}", + "ariaLabel": "遠端連線 {{connection}},{{status}}", + "status": { + "connected": "已連線", + "connecting": "連線中", + "disconnected": "已中斷", + "error": "連線異常", + "unknown": "狀態未知" + } + }, "actions": { "more": "更多操作", "newSession": "新增會話",