diff --git a/.agents/notes/implemented/feature/2026-08-18-web-home-path-tilde.i18n.yaml b/.agents/notes/implemented/feature/2026-08-18-web-home-path-tilde.i18n.yaml index e6983cb11f..0c4a921c34 100644 --- a/.agents/notes/implemented/feature/2026-08-18-web-home-path-tilde.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-18-web-home-path-tilde.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-18-web-home-path-tilde.md -2026-08-18-web-home-path-tilde.md: 4b9b24454bbeeb394480c0c30470b7383a257790 -2026-08-18-web-home-path-tilde.zh.md: d901caab361755de44f6384d1016faf125175822 +2026-08-18-web-home-path-tilde.md: b148833bab09eadce4c9c1a362dd99d04eba5977 +2026-08-18-web-home-path-tilde.zh.md: 9d15cd6dca1128927389d5731dff6bf831cffe76 diff --git a/.agents/notes/implemented/feature/2026-08-18-web-home-path-tilde.md b/.agents/notes/implemented/feature/2026-08-18-web-home-path-tilde.md index 4b9b24454b..b148833bab 100644 --- a/.agents/notes/implemented/feature/2026-08-18-web-home-path-tilde.md +++ b/.agents/notes/implemented/feature/2026-08-18-web-home-path-tilde.md @@ -14,7 +14,7 @@ Workspace hover cards and Tool call summaries showed full POSIX home paths. Thos `abbreviateHomePath` in `dsh-client-runtime` is the display-only helper. It returns `~` or `~/…` when the path is the POSIX home or a descendant, and leaves the path unchanged when `home` is missing, empty, or `/`, when either value is a Windows drive or UNC path, or when the match is only a prefix (`/Users/u` does not claim `/Users/u2`). Tool summaries run workspace-relative shortening first, then this helper, so a path inside the session cwd stays short. `filePath`, Host open, and Workspace hover copy keep the authored filesystem path. -`ui-tool` and `ui-workspace` inject `connection.hostDescription` at their own slot registrations. ChatView does not grow a Host-description hook. A missing `hostDescription` on an incomplete test fake falls back to an absent source, so abbreviation does not run. +`ui-tool` and `ui-workspace` inject `connection.hostDescription` at their own slot registrations. ChatView does not grow a Host-description hook. The field is required on `ConnectionHandle`; test fakes supply a source whose snapshot may be undefined before connect. The fixture Host home is `/home/fixture`. A second fixture Workspace at `/home/fixture/Documents/project` lets assembled replay hover `~/Documents/project` without moving the existing `/tmp/fixture` account. TerminalBlock's own prompt-label collapse is unchanged. @@ -30,7 +30,7 @@ The fixture Host home is `/home/fixture`. A second fixture Workspace at `/home/f ## Consequences -POSIX home-rooted Workspace hover paths and leftover Tool path summaries display as `~`. Copy and open still use the full path. Windows drive and UNC paths never become `~`. A Host that reports `/` as home does not turn the whole filesystem into `~`. Incomplete test connection fakes without `hostDescription` render unabbreviated paths instead of hanging or throwing. +POSIX home-rooted Workspace hover paths and leftover Tool path summaries display as `~`. Copy and open still use the full path. Windows drive and UNC paths never become `~`. A Host that reports `/` as home does not turn the whole filesystem into `~`. Before the first describe, or while reconnecting, the source snapshot is undefined and paths stay unabbreviated. ## Testing diff --git a/.agents/notes/implemented/feature/2026-08-18-web-home-path-tilde.zh.md b/.agents/notes/implemented/feature/2026-08-18-web-home-path-tilde.zh.md index d901caab36..9d15cd6dca 100644 --- a/.agents/notes/implemented/feature/2026-08-18-web-home-path-tilde.zh.md +++ b/.agents/notes/implemented/feature/2026-08-18-web-home-path-tilde.zh.md @@ -14,7 +14,7 @@ Workspace 悬停卡片和 Tool 调用摘要会显示完整的 POSIX 家目录路 `dsh-client-runtime` 中的 `abbreviateHomePath` 是仅用于展示的辅助函数。当路径是 POSIX 家目录或其后代时返回 `~` 或 `~/…`;`home` 缺失、为空或为 `/`,任一侧是 Windows 盘符或 UNC 路径,或只是前缀命中(`/Users/u` 不能收走 `/Users/u2`)时,路径保持不变。Tool 摘要先做工作区相对缩短,再调用该辅助函数,因此会话 cwd 内的路径仍然更短。`filePath`、Host 打开以及 Workspace 悬停复制仍使用作者给出的文件系统路径。 -`ui-tool` 与 `ui-workspace` 在各自的 slot 注册上注入 `connection.hostDescription`。ChatView 不增加 Host 描述钩子。测试假对象若缺少 `hostDescription`,会回退到空来源,因此不会进行缩写。 +`ui-tool` 与 `ui-workspace` 在各自的 slot 注册上注入 `connection.hostDescription`。ChatView 不增加 Host 描述钩子。该字段在 `ConnectionHandle` 上是必填的;测试假对象提供一个来源,其快照在连接完成前可以为 undefined。 fixture 的 Host 家目录是 `/home/fixture`。第二个 fixture Workspace 位于 `/home/fixture/Documents/project`,组装回放可以悬停出 `~/Documents/project`,而不必移动现有的 `/tmp/fixture` 账户。TerminalBlock 自有的提示符标签折叠保持不变。 @@ -30,7 +30,7 @@ fixture 的 Host 家目录是 `/home/fixture`。第二个 fixture Workspace 位 ## Consequences -POSIX 家目录下的 Workspace 悬停路径,以及缩短 cwd 后仍落在家目录里的 Tool 路径摘要,会显示为 `~`。复制与打开仍使用完整路径。Windows 盘符和 UNC 路径永远不会变成 `~`。若 Host 把 `/` 报成 home,不会把整个文件系统收成 `~`。缺少 `hostDescription` 的不完整测试连接假对象会渲染未缩写路径,而不是挂起或抛错。 +POSIX 家目录下的 Workspace 悬停路径,以及缩短 cwd 后仍落在家目录里的 Tool 路径摘要,会显示为 `~`。复制与打开仍使用完整路径。Windows 盘符和 UNC 路径永远不会变成 `~`。若 Host 把 `/` 报成 home,不会把整个文件系统收成 `~`。首次 describe 之前或重连期间,来源快照为 undefined,路径保持未缩写。 ## Testing diff --git a/packages/client/ui-tool/tests/read-card.client.spec.tsx b/packages/client/ui-tool/tests/read-card.client.spec.tsx index a6001d0d8f..8ae0b21cfc 100644 --- a/packages/client/ui-tool/tests/read-card.client.spec.tsx +++ b/packages/client/ui-tool/tests/read-card.client.spec.tsx @@ -261,7 +261,12 @@ describe('ReadRow keyed toolview', () => { }) describe('DetailsPanel Output section (read)', () => { - function mount(snapshot: ConversationSnapshot, selection: SelectionTarget | null, cwd?: string) { + function mount( + snapshot: ConversationSnapshot, + selection: SelectionTarget | null, + cwd?: string, + description?: Parameters[1], + ) { localStorage.clear() const chat = createChatStore().create() if (selection !== null) chat.actions.select(selection) @@ -282,7 +287,7 @@ describe('DetailsPanel Output section (read)', () => { return render( snapshot, subscribe: () => () => {} })} @@ -342,6 +347,15 @@ describe('DetailsPanel Output section (read)', () => { expect(view.getByText('输出').closest('section')?.querySelector('pre')?.textContent).toBe('plain result') }) + it('abbreviates a leftover POSIX home path on the read card label', () => { + const view = mount(snapshot({ + nodes: [settled({ resultView: resultRead({ path: '/Users/u/notes.md' }) })], + }), target, '/tmp/ws', { + version: '0', cwd: '/tmp', attachedSessions: 0, home: '/Users/u', canOpenPath: false, + }) + expect(view.getByText('~/notes.md')).toBeTruthy() + }) + it('a running read keeps the 运行中… placeholder (no result view)', () => { const view = mount(snapshot({ runningCalls: [running()] }), target) expect(view.getByText('运行中…')).toBeTruthy() diff --git a/packages/client/ui-tool/tests/tool-call-tree.client.spec.tsx b/packages/client/ui-tool/tests/tool-call-tree.client.spec.tsx index 441052556d..7f18800303 100644 --- a/packages/client/ui-tool/tests/tool-call-tree.client.spec.tsx +++ b/packages/client/ui-tool/tests/tool-call-tree.client.spec.tsx @@ -2,6 +2,7 @@ /** ToolCallTree-owned root/subcall markers and selection projection. */ import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, render } from '@testing-library/react' +import type { HostDescription } from '@deepseek-ai/dsh-client-connection/client' import type { ConversationSnapshot, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client' import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' @@ -21,6 +22,7 @@ const root = (callId: string, call: ToolResultNode['call']): ToolResultNode => ( function props( block: ToolResultNode, selectedCallId?: string, + description?: HostDescription, ): ToolTreeProps { const snapshot = {} as ConversationSnapshot const useSession = ((selector: (value: ConversationSnapshot) => unknown) => selector(snapshot)) as ToolTreeProps['useSession'] @@ -44,7 +46,7 @@ function props( inspectCall: vi.fn(), forkAt: vi.fn(), fileMentions: vi.fn(), - useHostDescription: (selector => selector(undefined)) as ToolTreeProps['useHostDescription'], + useHostDescription: (selector => selector(description)) as ToolTreeProps['useHostDescription'], t, } as unknown as ToolTreeProps } @@ -79,4 +81,12 @@ describe('ToolCallTree', () => { expect(view.container.querySelector('[data-chat-call-id="parent:code:1:code:1"]')?.getAttribute('data-selected')).toBe('true') expect(nests).toHaveLength(2) }) + + it('abbreviates a POSIX home path in the generic tool summary', () => { + const block = root('w1', { name: 'read', argsRaw: '{"path":"/h/docs/a.ts"}' }) + const view = render() + expect(view.getByText('~/docs/a.ts')).toBeTruthy() + }) }) diff --git a/packages/client/ui-tool/tests/tool-details-render.client.tsx b/packages/client/ui-tool/tests/tool-details-render.client.tsx index 7d3ffefe7c..c0332e9ac6 100644 --- a/packages/client/ui-tool/tests/tool-details-render.client.tsx +++ b/packages/client/ui-tool/tests/tool-details-render.client.tsx @@ -1,4 +1,5 @@ /** Test adapter for the production conversation.details.tool registration. */ +import type { HostDescription } from '@deepseek-ai/dsh-client-connection/client' import type { ChatConversationViewNode, ChatSnapshot, ConversationNode, RunningToolCall, SessionId, } from '@deepseek-ai/dsh-client-runtime/client' @@ -51,9 +52,13 @@ export function toolChatSnapshot( /** * Bind ui-tool's details renderer to the conversation slot callback shape. * @param t - conversation locale seat used by Tool cards. + * @param description - optional Host description so the details card can abbreviate home paths. * @returns a direct-test renderSlot implementation. */ -export function renderToolDetails(t: TranslateNS<'conversation'>): DetailsSlotProps['renderSlot'] { +export function renderToolDetails( + t: TranslateNS<'conversation'>, + description?: HostDescription, +): DetailsSlotProps['renderSlot'] { return (_key, owner) => { // PropsRenderSlots keeps its key generic even for this one-key share; // recover the concrete owner selected by the adapter's fixed slot. @@ -61,7 +66,7 @@ export function renderToolDetails(t: TranslateNS<'conversation'>): DetailsSlotPr return selector(undefined)} + useHostDescription={selector => selector(description)} t={t} /> } diff --git a/packages/client/ui-tool/tests/tool-row.client.spec.tsx b/packages/client/ui-tool/tests/tool-row.client.spec.tsx index bedea5d720..f1a78401e2 100644 --- a/packages/client/ui-tool/tests/tool-row.client.spec.tsx +++ b/packages/client/ui-tool/tests/tool-row.client.spec.tsx @@ -5,7 +5,6 @@ import { cleanup, fireEvent, render } from '@testing-library/react' import type { RunningToolCall, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client' import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' import { zh as commonZh } from '@deepseek-ai/dsh-client-locale/src/locales/zh.ts' -import { resolveWorkspacePath } from '@deepseek-ai/dsh-client-runtime/client' import { classifyTool, resultText, toolRowModel } from '../src/client/tool/models/tool-call-model.ts' import { ToolRow } from '../src/client/tool/components/ToolRow.tsx' import { GenericToolCard, type GenericToolCardProps } from '../src/client/tool/toolviews/GenericToolCard.tsx' @@ -120,13 +119,6 @@ describe('tool-call-model', () => { expect(toolRowModel('bash', running()).filePath).toBeUndefined() }) - it('resolveWorkspacePath joins relative paths under cwd and passes absolute through', () => { - expect(resolveWorkspacePath('/w', 'src/a.ts')).toBe('/w/src/a.ts') - expect(resolveWorkspacePath('/w/', '/abs/a.ts')).toBe('/abs/a.ts') - expect(resolveWorkspacePath(undefined, 'src/a.ts')).toBe('src/a.ts') - expect(resolveWorkspacePath('/w', 'C:\\x\\a.ts')).toBe('C:\\x\\a.ts') - }) - it('displays workspace-rooted paths relative to the session cwd', () => { const cwd = '/Users/u/ws/' expect(toolRowModel('edit', running({ name: 'edit', argsRaw: '{"file_path":"/Users/u/ws/src/x.ts"}' }), cwd).summary).toBe('src/x.ts') diff --git a/packages/client/ui-workspace/src/client/contract/slots.ts b/packages/client/ui-workspace/src/client/contract/slots.ts index 3714a4fb98..bdd85052a0 100644 --- a/packages/client/ui-workspace/src/client/contract/slots.ts +++ b/packages/client/ui-workspace/src/client/contract/slots.ts @@ -22,8 +22,8 @@ * and a hole has exactly one declaring entry — they carry the same owner * contract and the same occupant. */ -import type { HostDescription, HostDescriptionSource } from '@deepseek-ai/dsh-client-connection/client' -import type { HostObservable, PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots' +import type { HostDescriptionSource } from '@deepseek-ai/dsh-client-connection/client' +import type { HostObservable, PropsHooks, PropsLocale, PropsRenderSlots, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots' // Type-only: pull the owner SlotMap merges into programs that resolve the // runtime shares below. import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client' @@ -80,17 +80,14 @@ export type DirectoryPickingInjected = { } /** Component-side view of the picking share: the bound occupancy selector hook. */ -export type DirectoryPickingHooks = { - /** Selector hook over this surface's directory-flow occupancy. */ - useDirectoryFlow: SnapshotSelectorHook -} +export type DirectoryPickingHooks = PropsHooks /** * Browser-private injected share (arrives via the register inject factory). * Data reads use the global framework hooks; these are the Host actions the * browsing region drives. */ -export type WorkspaceBrowserInjected = DirectoryPickingInjected & { +export type WorkspaceBrowserInjected = { hooks: DirectoryPickingInjected['hooks'] & { /** Current generation's Host description, bound by the slot renderer. */ hostDescription: HostDescriptionSource @@ -148,11 +145,7 @@ export type WorkspaceBrowserProps = & PropsRenderSlots<'sidebar.workspaces.directoryFlow'> & PropsStore> & Omit - & DirectoryPickingHooks - & { - /** Selector hook over the current generation's Host description. */ - useHostDescription: SnapshotSelectorHook - } + & PropsHooks & PropsLocale<'workspace'> /** diff --git a/packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx b/packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx index 7b59ffd9ea..a8cfad6a10 100644 --- a/packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx +++ b/packages/client/ui-workspace/tests/workspace-browser.client.spec.tsx @@ -96,6 +96,27 @@ function rerender(b: ReturnType, overrides: Partial { + it('workspace hover card shows a POSIX home descendant as ~', () => { + vi.useFakeTimers() + try { + mount({ + useWorkspaces: hook(workspaceState([{ + ...workspace('project', []), + path: '/home/u/Documents/project', + title: 'Project', + }])), + useHostDescription: selector => selector({ + version: '0', cwd: '/tmp', attachedSessions: 0, home: '/home/u', canOpenPath: false, + }), + }) + fireEvent.pointerEnter(screen.getByRole('treeitem').parentElement as HTMLElement) + act(() => { vi.advanceTimersByTime(500) }) + expect(screen.getByText('~/Documents/project')).toBeTruthy() + } finally { + vi.useRealTimers() + } + }) + it('prunes deleted Workspace view state only after the Workspace baseline is ready', async () => { const pending = { ...workspaceState([]),