mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
fix: ci
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<typeof renderToolDetails>[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(
|
||||
<DetailsPanel
|
||||
SessionProvider={SessionProviderStub}
|
||||
renderSlot={renderToolDetails(t)}
|
||||
renderSlot={renderToolDetails(t, description)}
|
||||
sessionId={SID}
|
||||
t={t}
|
||||
useSession={bindSnapshotSelector({ getSnapshot: () => 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()
|
||||
|
||||
@@ -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(<ToolCallTree {...props(block, 'w1', {
|
||||
version: '0', cwd: '/tmp', attachedSessions: 0, home: '/h', canOpenPath: false,
|
||||
})} />)
|
||||
expect(view.getByText('~/docs/a.ts')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 <ToolDetails
|
||||
block={details.block}
|
||||
cwd={details.cwd}
|
||||
useHostDescription={selector => selector(undefined)}
|
||||
useHostDescription={selector => selector(description)}
|
||||
t={t}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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<boolean>
|
||||
}
|
||||
export type DirectoryPickingHooks = PropsHooks<DirectoryPickingInjected['hooks']>
|
||||
|
||||
/**
|
||||
* 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<ReturnType<typeof createWorkspaceViewStore>>
|
||||
& Omit<WorkspaceBrowserInjected, 'hooks'>
|
||||
& DirectoryPickingHooks
|
||||
& {
|
||||
/** Selector hook over the current generation's Host description. */
|
||||
useHostDescription: SnapshotSelectorHook<HostDescription | undefined>
|
||||
}
|
||||
& PropsHooks<WorkspaceBrowserInjected['hooks']>
|
||||
& PropsLocale<'workspace'>
|
||||
|
||||
/**
|
||||
|
||||
@@ -96,6 +96,27 @@ function rerender(b: ReturnType<typeof mount>, overrides: Partial<WorkspaceBrows
|
||||
}
|
||||
|
||||
describe('WorkspaceBrowser', () => {
|
||||
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([]),
|
||||
|
||||
Reference in New Issue
Block a user