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.
This commit is contained in:
@@ -175,7 +175,9 @@ function SessionHoverContent({ node, now, t }: { node: SessionNode; now: number;
|
|||||||
return (
|
return (
|
||||||
<div className={css.hoverContent}>
|
<div className={css.hoverContent}>
|
||||||
<div className={css.hoverTitle}>{displayTitle(node, t)}</div>
|
<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}>
|
<div className={css.hoverStatus}>
|
||||||
<StateDot state={node.running ? 'ongoing' : 'done'} />
|
<StateDot state={node.running ? 'ongoing' : 'done'} />
|
||||||
<span>{node.running ? t('status.running') : t('status.idle')}</span>
|
<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.slot}>{row.running && <StateDot state="ongoing" />}</span>
|
||||||
<span className={css.title}>{title}</span>
|
<span className={css.title}>{title}</span>
|
||||||
<span className={css.time}>{timeLabel(row.updatedAt, now, t)}</span>
|
{/* A blank New Session row is a provisional placeholder: nothing has
|
||||||
<span className={css.rowActions}>
|
happened in it yet, so a "now" timestamp and the row verbs
|
||||||
<Menu
|
(rename/fork/archive) would all act on content that does not
|
||||||
open={menuOpen}
|
exist — both trailing cells stay off until the first prompt. */}
|
||||||
onClose={() => { setMenuOpen(false) }}
|
{!row.blank && <span className={css.time}>{timeLabel(row.updatedAt, now, t)}</span>}
|
||||||
items={sessionMenuItems}
|
{!row.blank && (
|
||||||
onSelect={(id) => {
|
<span className={css.rowActions}>
|
||||||
setMenuOpen(false)
|
<Menu
|
||||||
if (id === 'rename') onRename(node.id, row.title)
|
open={menuOpen}
|
||||||
if (id === 'fork') onFork(node.id)
|
onClose={() => { setMenuOpen(false) }}
|
||||||
if (id === 'archive') onArchive(node.id)
|
items={sessionMenuItems}
|
||||||
}}
|
onSelect={(id) => {
|
||||||
portal
|
setMenuOpen(false)
|
||||||
closeOnPointerLeave
|
if (id === 'rename') onRename(node.id, row.title)
|
||||||
anchor={(
|
if (id === 'fork') onFork(node.id)
|
||||||
<button
|
if (id === 'archive') onArchive(node.id)
|
||||||
type="button"
|
}}
|
||||||
className={css.iconButton}
|
portal
|
||||||
aria-label={t('actions.session.aria', { name: title })}
|
closeOnPointerLeave
|
||||||
onClick={(e) => { e.stopPropagation(); setMenuOpen(v => !v) }}
|
anchor={(
|
||||||
>
|
<button
|
||||||
<IconEllipsisOutline16 />
|
type="button"
|
||||||
</button>
|
className={css.iconButton}
|
||||||
)}
|
aria-label={t('actions.session.aria', { name: title })}
|
||||||
/>
|
onClick={(e) => { e.stopPropagation(); setMenuOpen(v => !v) }}
|
||||||
</span>
|
>
|
||||||
|
<IconEllipsisOutline16 />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -157,6 +157,29 @@ describe('workspace browser rows', () => {
|
|||||||
expect(screen.queryByRole('button', { name: /工作区/ })).toBeNull()
|
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', () => {
|
it('session row menu opens without opening the session and dispatches rename, fork, and archive', () => {
|
||||||
const onOpen = vi.fn()
|
const onOpen = vi.fn()
|
||||||
const onRename = vi.fn()
|
const onRename = vi.fn()
|
||||||
|
|||||||
Reference in New Issue
Block a user