From 446e1ea400c220600bca33d778cf69d8b4746d76 Mon Sep 17 00:00:00 2001 From: Turtle Date: Mon, 19 Oct 2026 17:57:03 +0800 Subject: [PATCH] Show the working directory on Session hover cards --- packages/client/ui-workspace/README.i18n.yaml | 4 +-- packages/client/ui-workspace/README.md | 2 +- packages/client/ui-workspace/README.zh.md | 2 +- .../ui-workspace/src/client/rows/Rows.tsx | 13 +++++++--- .../src/client/rows/WorkspaceBrowser.tsx | 6 ++++- .../client/ui-workspace/src/client/tree.ts | 3 +++ .../ui-workspace/tests/rows.client.spec.tsx | 26 +++++++++++++++++++ 7 files changed, 47 insertions(+), 9 deletions(-) diff --git a/packages/client/ui-workspace/README.i18n.yaml b/packages/client/ui-workspace/README.i18n.yaml index 60a03011cf..ce7fbb558f 100644 --- a/packages/client/ui-workspace/README.i18n.yaml +++ b/packages/client/ui-workspace/README.i18n.yaml @@ -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 diff --git a/packages/client/ui-workspace/README.md b/packages/client/ui-workspace/README.md index 2654447b77..3809d5a076 100644 --- a/packages/client/ui-workspace/README.md +++ b/packages/client/ui-workspace/README.md @@ -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. diff --git a/packages/client/ui-workspace/README.zh.md b/packages/client/ui-workspace/README.zh.md index c655995453..e2df6e96dc 100644 --- a/packages/client/ui-workspace/README.zh.md +++ b/packages/client/ui-workspace/README.zh.md @@ -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 会关闭卡片。 diff --git a/packages/client/ui-workspace/src/client/rows/Rows.tsx b/packages/client/ui-workspace/src/client/rows/Rows.tsx index 216a6fc00f..db278c2754 100644 --- a/packages/client/ui-workspace/src/client/rows/Rows.tsx +++ b/packages/client/ui-workspace/src/client/rows/Rows.tsx @@ -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 (
{displayTitle(node, t)}
+ {node.cwd !== undefined &&
{abbreviateHomePath(node.cwd, home)}
} {/* Same placeholder rule as the row's trailing cell: no timestamp before the first prompt. */} {!node.blank &&
{hoverTimeLabel(node.updatedAt, now, t)}
} @@ -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 ( } + content={} openDelayMs={800} grouped disabled={menuOpen || drag?.active === true} diff --git a/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.tsx b/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.tsx index df871654ee..be4c78a9ef 100644 --- a/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.tsx +++ b/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.tsx @@ -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} /> ) diff --git a/packages/client/ui-workspace/src/client/tree.ts b/packages/client/ui-workspace/src/client/tree.ts index eb67d0ebe3..9596c49428 100644 --- a/packages/client/ui-workspace/src/client/tree.ts +++ b/packages/client/ui-workspace/src/client/tree.ts @@ -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 }), } } diff --git a/packages/client/ui-workspace/tests/rows.client.spec.tsx b/packages/client/ui-workspace/tests/rows.client.spec.tsx index f02cf3ca85..8a392726c9 100644 --- a/packages/client/ui-workspace/tests/rows.client.spec.tsx +++ b/packages/client/ui-workspace/tests/rows.client.spec.tsx @@ -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() + hover() + expect(screen.getByText('~/project')).toBeTruthy() + view.unmount() + const { cwd: _, ...withoutCwd } = node + render() + 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 = {