Browse Source

feat(ui-workspace): blank New Session rows render no menu and no time

A blank row is a provisional placeholder: nothing has happened in it,
so the row verbs (rename/fork/archive) and a 'now' stamp would act on
content that does not exist. The trailing cells and the hover card's
time line stay off until the first prompt lands.
imccyu 2 months ago
parent
commit
d2dff40560

+ 35 - 27
packages/client/ui-workspace/src/client/rows/Rows.tsx

@@ -175,7 +175,9 @@ function SessionHoverContent({ node, now, t }: { node: SessionNode; now: number;
   return (
     <div className={css.hoverContent}>
       <div className={css.hoverTitle}>{displayTitle(node, t)}</div>
-      <div className={css.hoverTime}>{hoverTimeLabel(node.updatedAt, now, t)}</div>
+      {/* Same placeholder rule as the row's trailing cell: no timestamp
+          before the first prompt. */}
+      {!node.blank && <div className={css.hoverTime}>{hoverTimeLabel(node.updatedAt, now, t)}</div>}
       <div className={css.hoverStatus}>
         <StateDot state={node.running ? 'ongoing' : 'done'} />
         <span>{node.running ? t('status.running') : t('status.idle')}</span>
@@ -306,32 +308,38 @@ export function SessionNodeItem({ node, currentId, now, onOpen, onRename, onFork
     >
       <span className={css.slot}>{row.running && <StateDot state="ongoing" />}</span>
       <span className={css.title}>{title}</span>
-      <span className={css.time}>{timeLabel(row.updatedAt, now, t)}</span>
-      <span className={css.rowActions}>
-        <Menu
-          open={menuOpen}
-          onClose={() => { setMenuOpen(false) }}
-          items={sessionMenuItems}
-          onSelect={(id) => {
-            setMenuOpen(false)
-            if (id === 'rename') onRename(node.id, row.title)
-            if (id === 'fork') onFork(node.id)
-            if (id === 'archive') onArchive(node.id)
-          }}
-          portal
-          closeOnPointerLeave
-          anchor={(
-            <button
-              type="button"
-              className={css.iconButton}
-              aria-label={t('actions.session.aria', { name: title })}
-              onClick={(e) => { e.stopPropagation(); setMenuOpen(v => !v) }}
-            >
-              <IconEllipsisOutline16 />
-            </button>
-          )}
-        />
-      </span>
+      {/* A blank New Session row is a provisional placeholder: nothing has
+          happened in it yet, so a "now" timestamp and the row verbs
+          (rename/fork/archive) would all act on content that does not
+          exist — both trailing cells stay off until the first prompt. */}
+      {!row.blank && <span className={css.time}>{timeLabel(row.updatedAt, now, t)}</span>}
+      {!row.blank && (
+        <span className={css.rowActions}>
+          <Menu
+            open={menuOpen}
+            onClose={() => { setMenuOpen(false) }}
+            items={sessionMenuItems}
+            onSelect={(id) => {
+              setMenuOpen(false)
+              if (id === 'rename') onRename(node.id, row.title)
+              if (id === 'fork') onFork(node.id)
+              if (id === 'archive') onArchive(node.id)
+            }}
+            portal
+            closeOnPointerLeave
+            anchor={(
+              <button
+                type="button"
+                className={css.iconButton}
+                aria-label={t('actions.session.aria', { name: title })}
+                onClick={(e) => { e.stopPropagation(); setMenuOpen(v => !v) }}
+              >
+                <IconEllipsisOutline16 />
+              </button>
+            )}
+          />
+        </span>
+      )}
     </div>
   )
   return (

+ 23 - 0
packages/client/ui-workspace/tests/rows.spec.tsx

@@ -157,6 +157,29 @@ describe('workspace browser rows', () => {
     expect(screen.queryByRole('button', { name: /工作区/ })).toBeNull()
   })
 
+  it('blank New Session rows carry no menu, no time label, and no hover-card time', () => {
+    vi.useFakeTimers()
+    try {
+      const node: SessionNode = {
+        id: sid('s-blank'), title: 'ignored', blank: true, running: false, updatedAt: 0,
+      }
+      render(<SessionNodeItem node={node} currentId={node.id} now={0} onOpen={vi.fn()}
+        onRename={vi.fn()} onFork={vi.fn()} onArchive={vi.fn()} t={t} />)
+      // The placeholder has no content yet: no row verbs, no "now" stamp.
+      expect(screen.queryByRole('button', { name: /会话.*的操作/ })).toBeNull()
+      expect(screen.queryByText('刚刚')).toBeNull()
+      // The hover card keeps title + status but drops the timestamp line.
+      const wrapper = screen.getByRole('treeitem').parentElement as HTMLElement
+      fireEvent.pointerEnter(wrapper)
+      act(() => { vi.advanceTimersByTime(500) })
+      expect(screen.getAllByText('新会话').length).toBeGreaterThanOrEqual(2)
+      expect(screen.getByText('空闲')).toBeTruthy()
+      expect(screen.queryByText('刚刚')).toBeNull()
+    } finally {
+      vi.useRealTimers()
+    }
+  })
+
   it('session row menu opens without opening the session and dispatches rename, fork, and archive', () => {
     const onOpen = vi.fn()
     const onRename = vi.fn()