feat(web): workspace header hover card with cwd and creation time

This commit is contained in:
imccyu
2026-07-30 01:00:12 +08:00
committed by imccyu
parent 4c33ca8bec
commit 245aeb414c
4 changed files with 50 additions and 11 deletions

View File

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

View File

@@ -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: <IconTrashOutline16 />, 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 (
<div className={css.hoverContent}>
<div className={css.hoverTitle}>{label}</div>
<div className={css.hoverPath}>{cwd}</div>
<div className={css.hoverTime}>{`Created ${new Date(createdAt).toLocaleString()}`}</div>
</div>
)
}
/**
* 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 = (
<div
className={clsx(css.projectRow, menuOpen && css.menuOpen)}
role="treeitem"
@@ -107,6 +123,15 @@ export function ProjectRowItem({ group, onToggle, onCreate, actions }: {
</span>
</div>
)
// The ungrouped bucket has no backing Workspace: no card to show.
if (row.createdAt === undefined) return ownRow
return (
<HoverCard
anchor={ownRow}
content={<WorkspaceHoverContent label={row.label} cwd={row.cwd} createdAt={row.createdAt} />}
disabled={menuOpen}
/>
)
}
/**

View File

@@ -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<SessionId, SessionSummary>
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,

View File

@@ -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(<ProjectRowItem group={group} onToggle={onToggle} onCreate={onCreate} />)
@@ -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(<ProjectRowItem
@@ -130,7 +130,7 @@ describe('workspace browser rows', () => {
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(<ProjectRowItem group={group} onToggle={vi.fn()} onCreate={vi.fn()} />)