fix(workspace): keep the default Workspace path language-neutral

The first-use directory name and its stored title came from the Client's
startup language, so one product decision carried two incompatible
requirements: a path addressed by shell commands, tool arguments, @path
references, and session logs, and a label a reader reads.

The controller now owns the fixed `default-workspace` directory name and
the registry titles the Workspace after that same segment, so
`workspace.initializeDefault` takes no request at all —
WorkspaceInitializeDefaultRequest and its name validation are gone, and
the registry resolver returns a path instead of a path and title. No
language reaches the Host.

Browser consumers label a Workspace through the new
`workspaceDisplayTitle`: a title still equal to the automatic one reads
as the localized default name from the `common` namespace, every other
title reads verbatim. The Workspace browser resolves it once at the store
read, so rows, hover card, search meta, and the rename and delete dialogs
inherit it; the hero picker and the Conversation chip resolve it too.
Confirming a rename stores the text on screen, which is what stops a row
following the language.
This commit is contained in:
_Kerman committed 2026-09-23 17:46:32 +08:00
1 parent 5e25475857
commit ff1c7d415f
53 files changed
+297 -195

No files matched your search

@@ -4,36 +4,36 @@ import { defaultWorkspaceDirectory, validateDocumentsDirectory } from '../src/de
describe('system Documents directory', () => {
it.each([
['darwin', '/Users/a/文档/\n', '/Users/a/文档/deepseek-harness/默认工作区', 'osascript'],
['win32', 'D:\\Redirected Documents\r\n', 'D:\\Redirected Documents\\deepseek-harness\\默认工作区', 'powershell.exe'],
['linux', '/home/a/My Documents\n', '/home/a/My Documents/deepseek-harness/默认工作区', 'xdg-user-dir'],
['darwin', '/Users/a/文档/\n', '/Users/a/文档/deepseek-harness/default-workspace', 'osascript'],
['win32', 'D:\\Redirected Documents\r\n', 'D:\\Redirected Documents\\deepseek-harness\\default-workspace', 'powershell.exe'],
['linux', '/home/a/My Documents\n', '/home/a/My Documents/deepseek-harness/default-workspace', 'xdg-user-dir'],
] as const)('uses the %s account directory and preserves spaces and Unicode', async (platform, stdout, path, command) => {
const run = vi.fn<NativeCommandRunner>(async () => ({ stdout, stderr: '' }))
const signal = new AbortController().signal
await expect(defaultWorkspaceDirectory('默认工作区', undefined, signal, { platform, run })).resolves.toBe(path)
await expect(defaultWorkspaceDirectory(undefined, signal, { platform, run })).resolves.toBe(path)
expect(run).toHaveBeenCalledWith(command, expect.any(Array), signal)
})
it.each(['Default workspace', '默认工作区', 'default-workspace'])('uses the configured directory with name %s', async (folder) => {
it('uses the configured directory without a system lookup', async () => {
const run = vi.fn<NativeCommandRunner>()
await expect(defaultWorkspaceDirectory(folder, '/documents', new AbortController().signal, { platform: 'linux', run }))
.resolves.toBe(`/documents/deepseek-harness/${folder}`)
await expect(defaultWorkspaceDirectory('/documents', new AbortController().signal, { platform: 'linux', run }))
.resolves.toBe('/documents/deepseek-harness/default-workspace')
expect(run).not.toHaveBeenCalled()
})
it.each(['', '\r\n', '/home/a\n'])('rejects an unavailable XDG directory %j', async (stdout) => {
const run: NativeCommandRunner = async () => ({ stdout, stderr: '' })
await expect(defaultWorkspaceDirectory('Default workspace', undefined, new AbortController().signal, { platform: 'linux', home: '/home/a', run }))
await expect(defaultWorkspaceDirectory(undefined, new AbortController().signal, { platform: 'linux', home: '/home/a', run }))
.rejects.toThrow('unavailable')
})
it('propagates lookup failure and cancellation', async () => {
const run: NativeCommandRunner = async () => { throw new Error('lookup denied') }
await expect(defaultWorkspaceDirectory('Default workspace', undefined, new AbortController().signal, { platform: 'darwin', run }))
await expect(defaultWorkspaceDirectory(undefined, new AbortController().signal, { platform: 'darwin', run }))
.rejects.toThrow('lookup denied')
await expect(defaultWorkspaceDirectory('Default workspace', '/documents', AbortSignal.abort(), { platform: 'linux' }))
await expect(defaultWorkspaceDirectory('/documents', AbortSignal.abort(), { platform: 'linux' }))
.rejects.toThrow()
await expect(defaultWorkspaceDirectory('Default workspace', undefined, new AbortController().signal, { platform: 'freebsd' }))
await expect(defaultWorkspaceDirectory(undefined, new AbortController().signal, { platform: 'freebsd' }))
.rejects.toThrow('unavailable')
})
@@ -176,18 +176,18 @@ describe('ClientWorkspaceModel', () => {
const model = modelFor(remote)
baseline(model)
const signal = new AbortController().signal
await expect(model.initializeDefault({ directoryName: '默认工作区', title: '默认工作区' }, signal)).resolves.toMatchObject({ ok: true })
expect(remote.initializeDefault).toHaveBeenCalledWith({ directoryName: '默认工作区', title: '默认工作区' }, signal)
await expect(model.initializeDefault(signal)).resolves.toMatchObject({ ok: true })
expect(remote.initializeDefault).toHaveBeenCalledWith(signal)
expect(model.getSnapshot().items.map(item => item.workspaceId)).toEqual(['default'])
const before = model.getSnapshot()
remote.initializeDefault.mockResolvedValueOnce(remoteOk(undefined))
await expect(model.initializeDefault({ directoryName: 'Default workspace', title: 'Default workspace' }))
await expect(model.initializeDefault())
.resolves.toEqual({ ok: true, value: undefined })
expect(model.getSnapshot()).toBe(before)
remote.initializeDefault.mockResolvedValueOnce(workspaceError(
new RemoteError('gateway/bad-request', 'choose a folder', {}),
))
await expect(model.initializeDefault({ directoryName: 'Default workspace', title: 'Default workspace' })).resolves.toMatchObject({ ok: false })
await expect(model.initializeDefault()).resolves.toMatchObject({ ok: false })
expect(model.getSnapshot()).toBe(before)
})
@@ -318,8 +318,7 @@ describe('WorkspaceController', () => {
const controller = new WorkspaceController(client.ctx, model)
const before = model.getSnapshot()
mock.remote.workspace.initializeDefault.mockResolvedValueOnce({ ok: true, value: undefined })
await expect(controller.initializeDefault({ directoryName: 'Default workspace', title: 'Default workspace' }))
.resolves.toBeUndefined()
await expect(controller.initializeDefault()).resolves.toBeUndefined()
expect(model.getSnapshot()).toBe(before)
})
@@ -331,7 +330,7 @@ describe('WorkspaceController', () => {
expect(controller.list).toBe(model)
expect(client.ctx.workspaces.list).toBe(model)
await expect(controller.initializeDefault({ directoryName: '默认工作区', title: '默认工作区' }, new AbortController().signal)).resolves.toMatchObject({ workspaceId: 'default' })
await expect(controller.initializeDefault(new AbortController().signal)).resolves.toMatchObject({ workspaceId: 'default' })
await expect(controller.create({ path: '/work/created' })).resolves.toMatchObject({ workspaceId: 'created' })
await expect(controller.rename(wid('one'), 'renamed')).resolves.toMatchObject({ title: 'renamed' })
await expect(controller.insertBefore(wid('one'))).resolves.toBeUndefined()
@@ -344,7 +343,6 @@ describe('WorkspaceController', () => {
await expect(controller.unpinSession(sid('session'))).resolves.toBeUndefined()
await expect(controller.delete(wid('one'))).resolves.toBeUndefined()
// Each command crosses the wire as one positional request object.
expect(mock.log.requests('workspace/initializeDefault')).toEqual([{ directoryName: '默认工作区', title: '默认工作区' }])
expect(mock.log.requests('workspace/create')).toEqual([{ path: '/work/created' }])
expect(mock.log.requests('workspace/rename')).toEqual([{ workspaceId: 'one', title: 'renamed' }])
expect(mock.log.requests('workspace/insertBefore')).toEqual([{ workspaceId: 'one' }])
@@ -364,7 +362,7 @@ describe('WorkspaceController', () => {
const missingSession = new RemoteError('session/not-found', 'missing session', { sessionId: sid('session') })
mock.remote.workspace.initializeDefault.mockResolvedValueOnce(err(new RemoteError('gateway/internal', 'directory denied', {})))
await expect(controller.initializeDefault({ directoryName: 'Default workspace', title: 'Default workspace' })).rejects.toBeInstanceOf(WorkspaceCreateError)
await expect(controller.initializeDefault()).rejects.toBeInstanceOf(WorkspaceCreateError)
mock.remote.workspace.create.mockResolvedValueOnce(err(new RemoteError('workspace/invalid-path', 'missing path', { path: '/missing' })))
const create = controller.create({ path: '/missing' })
@@ -1,6 +1,6 @@
import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { basename, join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
@@ -10,6 +10,7 @@ import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
import WorkspaceRegistry from '@deepseek-ai/dsh-workspace'
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
import WorkspaceController from '../src/index.ts'
import { DEFAULT_WORKSPACE_DIRECTORY } from '../src/default-workspace.ts'
import { WorkspaceFeed } from '../src/feed.ts'
import type { WorkspaceFollowFrame } from '../src/types.ts'
import { MemoryStorageBackend } from '../../../storage/storage-domain/tests/helpers/memory-backend.ts'
@@ -450,53 +451,30 @@ describe('WorkspaceController follow', () => {
describe('first-use Remote', () => {
it('reuses an initialized Workspace without looking up system Documents', async () => {
const { controller, ctx, root } = await harness({ systemDocuments: true })
const workspace = await ctx.workspaceRegistry.initializeDefault(async () => ({ path: root, title: 'Existing default' }))
const workspace = await ctx.workspaceRegistry.initializeDefault(async () => root)
const signal = AbortSignal.abort()
await expect(controller.initializeDefault({ directoryName: 'Default workspace', title: 'Default workspace' }, signal))
.resolves.toMatchObject({ workspace: { workspaceId: workspace!.id, path: root, title: 'Existing default' } })
await expect(controller.initializeDefault(signal))
.resolves.toMatchObject({ workspace: { workspaceId: workspace!.id, path: root, title: basename(root) } })
})
it.each(['', ' ', '.', '..', '../outside', 'nested/name', 'nested\\name', 'C:outside', 'name\0', '.. ', 'tail.'])(
'rejects invalid directory name %j before invoking the registry', async (directoryName) => {
const { controller, ctx } = await harness()
const initialize = vi.spyOn(ctx.workspaceRegistry, 'initializeDefault').mockResolvedValue(undefined)
await expect(controller.initializeDefault({ directoryName, title: 'Default workspace' }, new AbortController().signal))
.rejects.toMatchObject({ code: 'gateway/bad-request' })
expect(initialize).not.toHaveBeenCalled()
},
)
it('rejects a blank initial title before invoking the registry', async () => {
const { controller, ctx } = await harness()
const initialize = vi.spyOn(ctx.workspaceRegistry, 'initializeDefault').mockResolvedValue(undefined)
await expect(controller.initializeDefault({ directoryName: 'default-workspace', title: ' ' }, new AbortController().signal))
.rejects.toMatchObject({ code: 'gateway/bad-request' })
expect(initialize).not.toHaveBeenCalled()
})
it('uses the requested display title independently of the directory name', async () => {
const { controller, root } = await harness()
const result = await controller.initializeDefault({ directoryName: 'default-workspace', title: 'Default workspace' }, new AbortController().signal)
expect(result?.workspace).toMatchObject({ path: join(root, 'deepseek-harness', 'default-workspace'), title: 'Default workspace' })
})
it('returns a durable Workspace without allocating a Session', async () => {
it('returns a durable Workspace named after its fixed directory without allocating a Session', async () => {
const { controller, ctx, root } = await harness()
const signal = new AbortController().signal
const result = await controller.initializeDefault({ directoryName: 'Default workspace', title: 'Default workspace' }, signal)
expect(result!.workspace.path).toBe(join(root, 'deepseek-harness', 'Default workspace'))
const result = await controller.initializeDefault(signal)
expect(result!.workspace.path).toBe(join(root, 'deepseek-harness', DEFAULT_WORKSPACE_DIRECTORY))
expect(result!.workspace.title).toBe(DEFAULT_WORKSPACE_DIRECTORY)
expect(existsSync(result!.workspace.path)).toBe(true)
expect(ctx.sessions.list()).toEqual([])
expect(await controller.initializeDefault({ directoryName: '默认工作区', title: '默认工作区' }, signal)).toEqual(result)
expect(await controller.initializeDefault(signal)).toEqual(result)
})
it('skips ineligible first use and propagates preparation failures', async () => {
const { controller, ctx, root } = await harness()
await ctx.workspaceRegistry.create(root)
await expect(controller.initializeDefault({ directoryName: 'Default workspace', title: 'Default workspace' }, new AbortController().signal))
await expect(controller.initializeDefault(new AbortController().signal))
.resolves.toBeUndefined()
vi.spyOn(ctx.workspaceRegistry, 'initializeDefault').mockRejectedValueOnce(new Error('permission denied'))
await expect(controller.initializeDefault({ directoryName: 'Default workspace', title: 'Default workspace' }, new AbortController().signal))
await expect(controller.initializeDefault(new AbortController().signal))
.rejects.toThrow('permission denied')
})
})