Allow same-basename Workspace paths

This commit is contained in:
Tianyi Cui
2026-07-31 18:35:55 +08:00
parent bcb474d13e
commit 678ad97f9a
43 changed files with 203 additions and 135 deletions
@@ -13,9 +13,8 @@ import { useCallback, useEffect, useState } from 'react'
import {
Button, IconFolderClose16, IconPlusOutline16, Menu, Modal, type MenuEntry,
} from '@deepseek-ai/dsh-client-ui-primitives'
import {
WorkspaceCreateError,
type WorkspaceId, type WorkspaceListState, type WorkspaceView,
import type {
WorkspaceId, WorkspaceListState, WorkspaceView,
} from '@deepseek-ai/dsh-client-runtime/client'
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
import type { DirectoryFlowOwnerProps, WorkspacePickerProps } from './contract/slots.ts'
@@ -80,7 +79,6 @@ export function WorkspacePickFlow({
const [modalError, setModalError] = useState<string | null>(null)
const [flowOpen, setFlowOpen] = useState(false)
const [pickingFolder, setPickingFolder] = useState(false)
const [folderConflict, setFolderConflict] = useState(false)
// One picking interaction at a time: while the flow is open (native chooser
// pending, browse dialog up) or its pick is being adopted, every other
// menu action stays disabled — a late outcome must not race a concurrent
@@ -130,10 +128,6 @@ export function WorkspacePickFlow({
setFlowOpen(false)
onPick(workspace.workspaceId)
}).catch((reason: unknown) => {
setFolderConflict(
reason instanceof WorkspaceCreateError
&& reason.rpcError.code === 'workspace-name-conflict',
)
setModalError(reason instanceof Error ? reason.message : String(reason))
setFlowOpen(false)
setErrorOpen(true)
@@ -143,7 +137,6 @@ export function WorkspacePickFlow({
onClose()
setErrorOpen(false)
setModalError(null)
setFolderConflict(false)
setFlowOpen(true)
}, [onClose])
@@ -174,7 +167,6 @@ export function WorkspacePickFlow({
onCancel: () => { setFlowOpen(false) },
onError: (message) => {
setFlowOpen(false)
setFolderConflict(false)
setModalError(message)
setErrorOpen(true)
},
@@ -208,7 +200,7 @@ export function WorkspacePickFlow({
open={errorOpen}
onClose={closeModal}
closeLabel={t('close')}
title={folderConflict ? t('conflict.title') : t('folderError.title')}
title={t('folderError.title')}
footer={(
<>
<Button variant="outline" className={css.modalAction} onClick={closeModal}>{t('cancel')}</Button>
@@ -218,11 +210,7 @@ export function WorkspacePickFlow({
</>
)}
>
<div className={css.modalError} role="alert">
{folderConflict
? t('conflict.hint')
: modalError}
</div>
<div className={css.modalError} role="alert">{modalError}</div>
</Modal>
</>
)
@@ -13,8 +13,8 @@
* picking interaction — a renderless native-chooser driver or an in-app
* browsing dialog. ui-workspace owns the trigger (the "Add workspace…"
* entry, present only while the hole is occupied) and the adoption
* semantics (`createWorkspace({ path })`, the conflict/error dialog, Choose
* again); the occupant owns everything between `open` and the picked path,
* semantics (`createWorkspace({ path })`, the retryable error dialog,
* Choose again); the occupant owns everything between `open` and the picked path,
* including creating a new directory to hand back. That occupant-owned
* creation is why adding a workspace has a single route: an unoccupied hole
* leaves the surface with no add affordance at all.
@@ -26,8 +26,6 @@ export const zh = {
'search.hasMore': '仅显示前 {n} 条结果,请缩小搜索范围。',
'menu.addWorkspace': '添加工作区…',
'picker.loading': '正在加载工作区…',
'conflict.title': '已存在同名工作区',
'conflict.hint': '请选择其他名称的文件夹。',
'conflict.named': '已存在名为“{name}”的工作区。',
'folderError.title': '无法打开文件夹',
'folderError.retry': '重新选择',
@@ -84,8 +82,6 @@ export const en = {
'search.hasMore': 'Showing the first {n} results. Narrow your search.',
'menu.addWorkspace': 'Add workspace…',
'picker.loading': 'Loading workspaces…',
'conflict.title': 'A workspace with this name already exists',
'conflict.hint': 'Choose a folder with a different name.',
'conflict.named': 'A workspace named “{name}” already exists.',
'folderError.title': 'Couldnt open folder',
'folderError.retry': 'Choose again',