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": "新增會話",