From 245aeb414ca73932dd56643fbae7ccac7bb9e853 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Thu, 30 Jul 2026 01:00:12 +0800 Subject: [PATCH] feat(web): workspace header hover card with cwd and creation time --- .../src/client/rows/Rows.module.css | 7 ++++ .../ui-workspace/src/client/rows/Rows.tsx | 35 ++++++++++++++++--- .../client/ui-workspace/src/client/tree.ts | 13 +++++-- .../client/ui-workspace/tests/rows.spec.tsx | 6 ++-- 4 files changed, 50 insertions(+), 11 deletions(-) diff --git a/packages/client/ui-workspace/src/client/rows/Rows.module.css b/packages/client/ui-workspace/src/client/rows/Rows.module.css index 7b19284b66..5a5cac914e 100644 --- a/packages/client/ui-workspace/src/client/rows/Rows.module.css +++ b/packages/client/ui-workspace/src/client/rows/Rows.module.css @@ -192,6 +192,13 @@ overflow-wrap: break-word; } +.hoverPath { + font-size: 12px; + line-height: 16px; + color: #CFD3D6; + word-break: break-all; +} + .hoverTime { font-size: 12px; line-height: 16px; diff --git a/packages/client/ui-workspace/src/client/rows/Rows.tsx b/packages/client/ui-workspace/src/client/rows/Rows.tsx index d75fabdd8b..a507ac2991 100644 --- a/packages/client/ui-workspace/src/client/rows/Rows.tsx +++ b/packages/client/ui-workspace/src/client/rows/Rows.tsx @@ -2,8 +2,8 @@ * Workspace browser tree row components (figma Cell set 14:3080): pure presentational — * all data and callbacks arrive via props. Hover swaps (folder->chevron, * time->ellipsis, action buttons) are CSS-only. Row ... menus are visual-only - * except workspace Rename/Delete and session Rename; the session hover card is - * suppressed while a menu is open. + * except workspace Rename/Delete and session Rename; the session and workspace + * hover cards are suppressed while a menu is open. */ import { useState } from 'react' import clsx from 'clsx' @@ -30,10 +30,26 @@ const WORKSPACE_MENU_ITEMS = [ { id: 'delete', label: 'Delete workspace', icon: , danger: true }, ] +/** Hover-card body: workspace title, full directory path, absolute creation time. */ +function WorkspaceHoverContent({ label, cwd, createdAt }: { + label: string + cwd: string | undefined + createdAt: number +}) { + return ( +
+
{label}
+
{cwd}
+
{`Created ${new Date(createdAt).toLocaleString()}`}
+
+ ) +} + /** * Project (workspace) header row: 54px, folder + title + session count; - * hover reveals the chevron and create button. `containsCurrent` arrives on - * the node (derivation fact, no renderer scan). + * hover reveals the chevron and create button, and dwelling on a real + * Workspace shows its hover card (the ungrouped bucket has none). + * `containsCurrent` arrives on the node (derivation fact, no renderer scan). * @param props.group - derived group node. * @param props.onToggle - expand/collapse the group. * @param props.onCreate - start a frontend Session inside this Workspace. @@ -50,7 +66,7 @@ export function ProjectRowItem({ group, onToggle, onCreate, actions }: { const active = group.expanded && group.containsCurrent const count = `${row.sessionCount} ${row.sessionCount === 1 ? 'session' : 'sessions'}` const [menuOpen, setMenuOpen] = useState(false) - return ( + const ownRow = (
) + // The ungrouped bucket has no backing Workspace: no card to show. + if (row.createdAt === undefined) return ownRow + return ( + } + disabled={menuOpen} + /> + ) } /** diff --git a/packages/client/ui-workspace/src/client/tree.ts b/packages/client/ui-workspace/src/client/tree.ts index c0adfadd6f..de47454d84 100644 --- a/packages/client/ui-workspace/src/client/tree.ts +++ b/packages/client/ui-workspace/src/client/tree.ts @@ -31,6 +31,8 @@ export interface GroupNode { /** Backing Workspace id; absent only for the ungrouped bucket. */ workspaceId: WorkspaceId | undefined cwd: string | undefined + /** Workspace creation time (epoch ms); absent only for the ungrouped bucket. */ + createdAt: number | undefined label: string /** Total visible sessions in the group. */ sessionCount: number @@ -52,6 +54,7 @@ interface Group { key: string workspaceId: WorkspaceId | undefined cwd: string | undefined + createdAt: number | undefined label: string summaries: Map roots: SessionId[] @@ -91,6 +94,7 @@ function buildGroup( key: string, workspaceId: WorkspaceId | undefined, cwd: string | undefined, + createdAt: number | undefined, label: string, members: readonly SessionSummary[], order: 'account' | 'recency', @@ -142,7 +146,7 @@ function buildGroup( for (const m of members) { if (!reachable.has(m.id)) rootIds.push(m.id) } - return { key, workspaceId, cwd, label, summaries, roots: rootIds, children } + return { key, workspaceId, cwd, createdAt, label, summaries, roots: rootIds, children } } /** @@ -163,7 +167,8 @@ function groupByWorkspace(list: SessionListState, workspaces: readonly Workspace members.push(summary) } groups.push(buildGroup( - workspace.workspaceId, workspace.workspaceId, workspace.path, workspace.title, members, 'account', + workspace.workspaceId, workspace.workspaceId, workspace.path, + Date.parse(workspace.createdAt), workspace.title, members, 'account', )) } const stray = list.ids @@ -171,7 +176,7 @@ function groupByWorkspace(list: SessionListState, workspaces: readonly Workspace .filter((s): s is SessionSummary => s !== undefined && !accounted.has(s.id) && sessionVisible(s, list.current)) if (stray.length > 0) { - groups.push(buildGroup(UNGROUPED_KEY, undefined, undefined, UNGROUPED_LABEL, stray, 'recency')) + groups.push(buildGroup(UNGROUPED_KEY, undefined, undefined, undefined, UNGROUPED_LABEL, stray, 'recency')) } return groups } @@ -267,6 +272,7 @@ export function deriveGroups( key: g.key, workspaceId: g.workspaceId, cwd: g.cwd, + createdAt: g.createdAt, label: g.label, sessionCount: g.summaries.size, expanded, @@ -280,6 +286,7 @@ export function deriveGroups( key: g.key, workspaceId: g.workspaceId, cwd: g.cwd, + createdAt: g.createdAt, label: g.label, sessionCount: g.summaries.size, expanded: visible.size > 0, diff --git a/packages/client/ui-workspace/tests/rows.spec.tsx b/packages/client/ui-workspace/tests/rows.spec.tsx index bfaa8a36dd..31f1d0ceec 100644 --- a/packages/client/ui-workspace/tests/rows.spec.tsx +++ b/packages/client/ui-workspace/tests/rows.spec.tsx @@ -42,7 +42,7 @@ describe('workspace browser rows', () => { const onToggle = vi.fn() const onCreate = vi.fn() const group: GroupNode = { - key: 'project', workspaceId: wid('project'), cwd: '/projects/project', label: 'Project', + key: 'project', workspaceId: wid('project'), cwd: '/projects/project', createdAt: 0, label: 'Project', sessionCount: 1, expanded: true, containsCurrent: true, sessions: [], } render() @@ -103,7 +103,7 @@ describe('workspace browser rows', () => { const onDelete = vi.fn() const onToggle = vi.fn() const group: GroupNode = { - key: 'project', workspaceId: wid('project'), cwd: '/projects/project', label: 'Project', + key: 'project', workspaceId: wid('project'), cwd: '/projects/project', createdAt: 0, label: 'Project', sessionCount: 0, expanded: false, containsCurrent: false, sessions: [], } render( { it('ungrouped bucket renders no workspace menu', () => { const group: GroupNode = { - key: '', workspaceId: undefined, cwd: undefined, label: 'Ungrouped', + key: '', workspaceId: undefined, cwd: undefined, createdAt: undefined, label: 'Ungrouped', sessionCount: 0, expanded: false, containsCurrent: false, sessions: [], } render()