Przeglądaj źródła

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

imccyu 1 miesiąc temu
rodzic
commit
245aeb414c

+ 7 - 0
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;

+ 30 - 5
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: <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}
+    />
+  )
 }
 
 /**

+ 10 - 3
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<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,

+ 3 - 3
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(<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()} />)