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:
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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user