Show the working directory on Session hover cards

This commit is contained in:
Turtle committed 2026-10-19 18:00:28 +08:00
1 parent 9ab971fde5
commit 446e1ea400
7 files changed
+47 -9

No files matched your search

@@ -54,8 +54,8 @@
en: 83031e7505fc228a
zh: 03e17c9428fe29fd
/deepseek-ai-dsh-client-ui-workspace/understand-the-implementation/hover-cards:
en: ea89ec5a3ee1f167
zh: e11f350571bcfff7
en: b15e684f12936689
zh: df3ef53e56863bd9
/deepseek-ai-dsh-client-ui-workspace/further-exploration:
en: 2abbf27abbd71ba4
zh: ace33fa2ea042357
+1 -1
View File
@@ -184,7 +184,7 @@ Row motion belongs to [AnimatedRows](src/client/rows/AnimatedRows.tsx). It measu
### Hover cards
Workspace and Session hover cards copy the value their row clips: activating a Workspace card writes its full directory path, while activating a non-blank Session card writes its full display title. A provisional blank New Session card remains read-only because its localized label is a placeholder rather than session content. A Session card also renders its `sidebar.session.row.hover` seat between the relative time and the trailing status lines whenever the card is open, independent of the row's own state. The first Workspace or Session card in a physical list waits 800ms; after a card is displayed, moving to another Workspace or Session in the same list replaces it immediately. The list and its active portaled card form one pointer interaction: entering the card to read or copy preserves immediate handoff to a neighboring Workspace or Session. After the pointer leaves both, the shared dwell resets when the active card closes through its existing pointer-grace and close lifecycle, with no separate group timer. Leaving a list with no active card resets the dwell immediately. Small action tooltips coexist with the right-side hover card; pin, archive, Workspace new-session, and search-result unarchive tooltips use portals to stay above it. Hovering actions, including More, preserves the card; opening the More menu or starting a Session drag dismisses it.
Workspace and Session hover cards copy the value their row clips: activating a Workspace card writes its full directory path, while activating a non-blank Session card writes its full display title. A provisional blank New Session card remains read-only because its localized label is a placeholder rather than session content. A Session card shows its working directory below the title, abbreviating a POSIX home prefix as `~`, and omits that line when the Host reports no directory. A Session card also renders its `sidebar.session.row.hover` seat between the relative time and the trailing status lines whenever the card is open, independent of the row's own state. The first Workspace or Session card in a physical list waits 800ms; after a card is displayed, moving to another Workspace or Session in the same list replaces it immediately. The list and its active portaled card form one pointer interaction: entering the card to read or copy preserves immediate handoff to a neighboring Workspace or Session. After the pointer leaves both, the shared dwell resets when the active card closes through its existing pointer-grace and close lifecycle, with no separate group timer. Leaving a list with no active card resets the dwell immediately. Small action tooltips coexist with the right-side hover card; pin, archive, Workspace new-session, and search-result unarchive tooltips use portals to stay above it. Hovering actions, including More, preserves the card; opening the More menu or starting a Session drag dismisses it.
</details>
+1 -1
View File
@@ -184,7 +184,7 @@ Workspace 基线就绪后,浏览器持久化的展开状态和 Session 顺序
### 悬浮卡片
Workspace 与 Session 悬浮卡片会复制对应行被截断的值:激活 Workspace 卡片会写入其完整目录路径,激活非空白 Session 卡片则会写入其完整显示标题。临时的空白「新会话」卡片保持只读,因为其本地化标签是占位文案,并非会话内容。 Session 卡片在卡片打开期间还会在相对时间与行尾状态行之间渲染 `sidebar.session.row.hover` seat,与行自身状态无关。同一物理列表中的首张 Workspace 或 Session 卡片等待 800ms;实际显示一张后,移到该列表的其他 Workspace 或 Session 会立即替换卡片。列表与其当前活动的 portal 卡片构成一次指针交互:进入卡片阅读或复制后,移到相邻 Workspace 或 Session 仍会立即切换。指针离开两者后,共享等待在活动卡片通过原有指针宽限及关闭生命周期关闭时重置,不另设分组计时器。列表没有活动卡片时,指针离开便立即重置等待。小型操作提示与右侧悬浮卡片同时显示;置顶、归档、Workspace 新建会话及搜索结果取消归档的提示通过 portal 保持在卡片上层。悬停操作按钮(包括「更多」)保留卡片;打开「更多」菜单或开始拖拽 Session 会关闭卡片。
Workspace 与 Session 悬浮卡片会复制对应行被截断的值:激活 Workspace 卡片会写入其完整目录路径,激活非空白 Session 卡片则会写入其完整显示标题。临时的空白「新会话」卡片保持只读,因为其本地化标签是占位文案,并非会话内容。Session 卡片在标题下方显示其工作目录,POSIX 主目录前缀缩写为 `~`;Host 未报告目录时省略该行。 Session 卡片在卡片打开期间还会在相对时间与行尾状态行之间渲染 `sidebar.session.row.hover` seat,与行自身状态无关。同一物理列表中的首张 Workspace 或 Session 卡片等待 800ms;实际显示一张后,移到该列表的其他 Workspace 或 Session 会立即替换卡片。列表与其当前活动的 portal 卡片构成一次指针交互:进入卡片阅读或复制后,移到相邻 Workspace 或 Session 仍会立即切换。指针离开两者后,共享等待在活动卡片通过原有指针宽限及关闭生命周期关闭时重置,不另设分组计时器。列表没有活动卡片时,指针离开便立即重置等待。小型操作提示与右侧悬浮卡片同时显示;置顶、归档、Workspace 新建会话及搜索结果取消归档的提示通过 portal 保持在卡片上层。悬停操作按钮(包括「更多」)保留卡片;打开「更多」菜单或开始拖拽 Session 会关闭卡片。
</details>
@@ -420,13 +420,14 @@ function PinnedIndicator({ t }: { t: RowTranslate }) {
}
/**
* Hover-card body: full title, relative time, the Session's own scheduled-task
* Hover-card body: full title, display working directory, relative time, the Session's own scheduled-task
* section, and every relevant live status. The task section sits above the
* status lines so they stay the card's trailing status line.
*/
function SessionHoverContent({ node, now, renderSlot, t }: {
function SessionHoverContent({ node, now, home, renderSlot, t }: {
node: SessionNode
now: number
home: string | undefined
renderSlot: RowRenderSlots
t: RowTranslate
}) {
@@ -437,6 +438,7 @@ function SessionHoverContent({ node, now, renderSlot, t }: {
return (
<div className={css.hoverContent}>
<div className={css.hoverTitle}>{displayTitle(node, t)}</div>
{node.cwd !== undefined && <div className={css.hoverPath}>{abbreviateHomePath(node.cwd, home)}</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>}
@@ -539,11 +541,12 @@ export function SearchResultItem({ result, currentId, onOpen, onUnarchive, t }:
* its leading decoration, and its hover-card section.
* @param props.onReveal - scroll this row into view after search navigation, then acknowledge it.
* @param props.drag - optional row-drag target wiring; blank rows cannot start a drag.
* @param props.home - host account home for POSIX hover-path abbreviation.
* @param props.t - the browser root's locale seat.
* @returns the session row.
*/
export function SessionNodeItem({
node, currentId, now, onOpen, onRenameRequest, renderSlot, onReveal, drag, t,
node, currentId, now, onOpen, onRenameRequest, renderSlot, onReveal, drag, home, t,
}: {
node: SessionNode
currentId: string | undefined
@@ -555,6 +558,8 @@ export function SessionNodeItem({
onReveal?: (() => void) | undefined
/** Present on reorderable-list rows so every row can remain a drop target. */
drag?: RowDragProps | undefined
/** Host account home; POSIX home-rooted hover paths display as `~`. */
home?: string | undefined
t: RowTranslate
} & PropsRenderSlots<
| 'sidebar.workspaces.session.menu.item'
@@ -693,7 +698,7 @@ export function SessionNodeItem({
return (
<HoverCard
anchor={ownRow}
content={<SessionHoverContent node={node} now={now} renderSlot={renderSlot} t={t} />}
content={<SessionHoverContent node={node} now={now} home={home} renderSlot={renderSlot} t={t} />}
openDelayMs={800}
grouped
disabled={menuOpen || drag?.active === true}
@@ -633,6 +633,7 @@ function SessionTree({
? () => { onSessionRevealed(node.id) }
: undefined}
drag={dragProps}
home={home}
t={t}
/>
)
@@ -689,7 +690,7 @@ function SessionTree({
function FlatList({
list, sessionIds, rowState, onLeaveArchivedOnly, useSessionStatus, open, onSessionRenameRequest,
usePanelInfo, setSessionOrder, workspaceReady, animationResetKey,
revealSessionId, onSessionRevealed, renderSlot, t,
revealSessionId, onSessionRevealed, renderSlot, home, t,
}: Pick<
SessionTreeProps,
| 'useSessionStatus'
@@ -704,6 +705,7 @@ function FlatList({
| 'onSessionRevealed'
| 'rowState'
| 'onLeaveArchivedOnly'
| 'home'
| 't'
> & {
list: SessionListState
@@ -778,6 +780,7 @@ function FlatList({
dropCommitted.current = false
},
}}
home={home}
t={t}
/>
)
@@ -1434,6 +1437,7 @@ export function WorkspaceBrowser({
setSessionOrder={saveSessionOrder}
revealSessionId={revealSessionId}
onSessionRevealed={acknowledgeSessionReveal}
home={home}
t={t}
/>
)
@@ -59,6 +59,8 @@ export interface SessionNode {
/** In the registry-global archive set: shown grayed in place and not openable. */
archived: boolean
updatedAt: number
/** Session working directory; absent when the Host reports none. */
cwd?: string
}
/** Session order selected by the Workspace browser. */
@@ -413,6 +415,7 @@ function sessionNode(
pinned: !archived.has(s.id) && pinned.has(s.id),
archived: archived.has(s.id),
updatedAt: s.updatedAt,
...(s.cwd === undefined ? {} : { cwd: s.cwd }),
...(pendingInteraction === undefined ? {} : { pendingInteraction }),
}
}
@@ -685,6 +685,32 @@ describe('workspace browser rows', () => {
}
})
it('Session hover card shows the home-abbreviated working directory only when the Host reports one', () => {
vi.useFakeTimers()
try {
const node: SessionNode = {
id: sid('s-cwd'), title: 'Located', blank: false, running: false,
runningSubagentCount: 0, completed: false, updatedAt: 0, pinned: false, archived: false,
cwd: '/home/u/project',
}
const hover = (): void => {
fireEvent.pointerEnter(screen.getByRole('treeitem').parentElement as HTMLElement)
act(() => { vi.advanceTimersByTime(800) })
}
const view = render(<SessionNodeItem node={node} currentId={undefined} now={0} onOpen={vi.fn()} home="/home/u" t={t} />)
hover()
expect(screen.getByText('~/project')).toBeTruthy()
view.unmount()
const { cwd: _, ...withoutCwd } = node
render(<SessionNodeItem node={withoutCwd} currentId={undefined} now={0} onOpen={vi.fn()} home="/home/u" t={t} />)
hover()
expect(screen.getAllByText('Located').length).toBeGreaterThanOrEqual(2)
expect(screen.queryByText(/project/)).toBeNull()
} finally {
vi.useRealTimers()
}
})
it('session row menu opens without opening the session and Escape closes it', () => {
const onOpen = vi.fn()
const node: SessionNode = {