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

@@ -19,7 +19,7 @@ export { WorkspaceArchiveError, WorkspaceController, WorkspaceCreateError } from
export type { IWorkspaces, WorkspaceSource } from './service.ts'
export type {
SessionActivity, SessionActivityItem, SessionActivityKind, SessionActivityKindMap, WorkspaceId,
WorkspaceInitializeDefaultRequest, WorkspaceView,
WorkspaceView,
} from '../types.ts'
type WorkspaceBaselineFrame = Extract<WorkspaceFollowFrame, { type: 'baseline' }>
@@ -12,7 +12,6 @@ import type {
WorkspaceCreateValue,
WorkspaceDeleteValue,
WorkspaceInsertSessionBeforeRequest,
WorkspaceInitializeDefaultRequest,
WorkspaceOrderValue,
WorkspacePinSessionRequest,
WorkspacePinValue,
@@ -104,14 +103,11 @@ export class ClientWorkspaceModel implements WorkspaceFollowSink {
/**
* Initialize the default Workspace and merge its authoritative row.
* @param request - initial directory name and title.
* @param signal - caller lifetime.
* @returns generated Remote result.
*/
async initializeDefault(
request: WorkspaceInitializeDefaultRequest, signal?: AbortSignal,
): Promise<RemoteResult<WorkspaceValue | undefined>> {
const result = await this.remote.initializeDefault(request, signal)
async initializeDefault(signal?: AbortSignal): Promise<RemoteResult<WorkspaceValue | undefined>> {
const result = await this.remote.initializeDefault(signal)
if (result.ok && result.value !== undefined) this.upsert(result.value.workspace)
return result
}
@@ -4,7 +4,7 @@ import { Service, type Context } from '@deepseek-ai/cordis'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { RemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
import type { WorkspaceInitializeDefaultRequest, WorkspaceView } from '../types.ts'
import type { WorkspaceView } from '../types.ts'
import type { ClientWorkspaceModel, WorkspaceSnapshot } from './model.ts'
/** Structured create failure for callers that distinguish Host business errors. */
@@ -55,11 +55,10 @@ export interface IWorkspaces {
create(input: { path: string }): Promise<WorkspaceView>
/**
* Initialize or reuse the default Workspace.
* @param request - initial directory name and title.
* @param signal - caller lifetime.
* @returns the prepared Workspace, or undefined when first-use initialization is ineligible; rejects on preparation failure.
*/
initializeDefault(request: WorkspaceInitializeDefaultRequest, signal?: AbortSignal): Promise<WorkspaceView | undefined>
initializeDefault(signal?: AbortSignal): Promise<WorkspaceView | undefined>
/**
* Rename a Workspace.
* @param workspaceId - target Workspace.
@@ -134,8 +133,8 @@ export class WorkspaceController extends Service implements IWorkspaces {
return result.value.workspace
}
async initializeDefault(request: WorkspaceInitializeDefaultRequest, signal?: AbortSignal): Promise<WorkspaceView | undefined> {
const result = await this.model.initializeDefault(request, signal)
async initializeDefault(signal?: AbortSignal): Promise<WorkspaceView | undefined> {
const result = await this.model.initializeDefault(signal)
if (!result.ok) throw new WorkspaceCreateError(result.error)
return result.value?.workspace
}
@@ -3,6 +3,7 @@
import { homedir } from 'node:os'
import { posix, win32 } from 'node:path'
import { runNativeCommand, type NativeCommandRunner } from '@deepseek-ai/dsh-native-command'
import { DEFAULT_WORKSPACE_DIRECTORY } from './default-workspace.ts'
/** Platform observations replaceable in directory-resolution tests. */
interface DocumentsDirectoryInternals {
@@ -28,14 +29,12 @@ export function validateDocumentsDirectory(directory: string, platform: NodeJS.P
/**
* Resolve the first-use directory on the Host without creating files.
* @param directoryName - validated single directory name supplied by the Client.
* @param documentsDirectory - explicit deployment override for the system Documents directory.
* @param signal - caller lifetime and lookup deadline.
* @param internals - platform facts and native command runner.
* @returns the absolute candidate path.
*/
export async function defaultWorkspaceDirectory(
directoryName: string,
documentsDirectory: string | undefined,
signal: AbortSignal,
internals: DocumentsDirectoryInternals = {},
@@ -75,5 +74,5 @@ export async function defaultWorkspaceDirectory(
}
directory = validateDocumentsDirectory(directory, platform)
signal.throwIfAborted()
return paths.join(directory, 'deepseek-harness', directoryName)
return paths.join(directory, 'deepseek-harness', DEFAULT_WORKSPACE_DIRECTORY)
}
@@ -0,0 +1,30 @@
/**
* Fixed first-use Workspace naming, shared by the Host that creates the
* directory and by browser consumers that label the resulting row. A pure fold
* with no imports, so client bundles inline it instead of requesting a
* module-table row this package does not publish.
* @module @deepseek-ai/dsh-api-workspace-controller/default-workspace
*/
/**
* Leaf directory name of the first-use Workspace under
* `<Documents>/deepseek-harness`. Language-neutral, so one installation keeps
* one on-disk path across language switches. The registry derives the initial
* title from this same segment, which is the title
* {@link workspaceDisplayTitle} recognizes as automatic.
*/
export const DEFAULT_WORKSPACE_DIRECTORY = 'default-workspace'
/**
* The text one Workspace is labeled with. A Workspace still carrying the
* automatic first-use title reads as the caller's localized default name; every
* other title reads verbatim in every language. A title the user typed as
* exactly {@link DEFAULT_WORKSPACE_DIRECTORY} is labeled as the default too;
* nothing else depends on the distinction.
* @param title - stored Workspace title.
* @param localizedDefault - the default Workspace name in the active language.
* @returns the title to display.
*/
export function workspaceDisplayTitle(title: string, localizedDefault: string): string {
return title === DEFAULT_WORKSPACE_DIRECTORY ? localizedDefault : title
}
+8 -13
View File
@@ -2,7 +2,7 @@
import { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
import { WorkspaceCommands } from './commands.ts'
import { DirectoryPickerController } from './directory-picker.ts'
import { WorkspaceFeed, workspaceView } from './feed.ts'
@@ -16,7 +16,6 @@ import type {
WorkspaceDeleteValue,
WorkspaceFollowFrame,
WorkspaceInsertBeforeRequest,
WorkspaceInitializeDefaultRequest,
WorkspaceInsertSessionBeforeRequest,
WorkspaceOrderValue,
WorkspacePinSessionRequest,
@@ -89,24 +88,20 @@ export class WorkspaceController extends TypertRemoteService {
}
/**
* Initialize or reuse the default Workspace during first-use startup.
* @param request - initial directory name and title; never rename an existing default.
* Initialize or reuse the default Workspace during first-use startup. The
* directory name is fixed, so the Host never renames or relocates an
* existing default; its initial title is that same name, which browser
* consumers label in the reader's language.
* @param signal - caller lifetime; cancels native directory lookup.
* @returns the durable Workspace, or undefined when first-use initialization is ineligible; creates no Session or message.
*/
@Remote('initializeDefault')
async initializeDefault(request: WorkspaceInitializeDefaultRequest, signal: AbortSignal): Promise<WorkspaceValue | undefined> {
const { directoryName, title } = request
if (directoryName.trim() === '' || directoryName !== directoryName.trim()
|| directoryName.endsWith('.') || /[/\\:\0]/.test(directoryName) || title.trim() === '') {
throw new RemoteError('gateway/bad-request', 'default Workspace requires a directory name and non-blank title', {})
}
async initializeDefault(signal: AbortSignal): Promise<WorkspaceValue | undefined> {
const workspace = await this.ctx.workspaceRegistry.initializeDefault(async () => {
const timeout = AbortSignal.timeout(this.config.documentsLookupTimeoutMs)
const path = await defaultWorkspaceDirectory(
directoryName, this.config.documentsDirectory, AbortSignal.any([signal, timeout]),
return await defaultWorkspaceDirectory(
this.config.documentsDirectory, AbortSignal.any([signal, timeout]),
)
return { path, title }
})
return workspace === undefined ? undefined : { workspace: workspaceView(workspace) }
}
@@ -66,14 +66,6 @@ export interface WorkspaceCreateRequest {
readonly path: string
}
/** Names chosen by the first-use Client before default Workspace initialization. */
export interface WorkspaceInitializeDefaultRequest {
/** Host rejects blank names, separators, colon, NUL, surrounding whitespace, and trailing dots; OS filename restrictions also apply. */
readonly directoryName: string
/** Initial display title, retained across language changes. */
readonly title: string
}
/** Created or previously registered Workspace. */
export interface WorkspaceCreateValue {
readonly workspace: WorkspaceView