mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
feat(web): workspace header hover card with cwd and creation time
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()} />)
|
||||
|
||||
Reference in New Issue
Block a user