Show the working directory on Session hover cards
This commit is contained in:
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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in new issue
Block a user