@@ -9,15 +9,17 @@ import { useCallback, useState } from 'react'
import {
Button , IconFolderClose16 , IconPlusOutline16 , Menu , Modal , type MenuEntry ,
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { WorkspaceId , WorkspaceListState , WorkspaceView } from '@deepseek-ai/dsh-client-runtime/client'
import {
WorkspaceCreateError ,
type WorkspaceId , type WorkspaceListState , type WorkspaceView ,
} from '@deepseek-ai/dsh-client-runtime/client'
import type { WorkspacePickerProps } from './contract/slots.ts'
import css from './WorkspacePicker.module.css'
const CREATE_WORKSPACE = '::create-workspace '
const USE_EXISTING = '::use-existing'
const OPEN_LOCAL_FOLDER = '::open-local-folder '
const CREATE_NEW = '::create-new'
type ModalKind = 'p ath ' | 'create ' | null
type ModalKind = 'cre ate ' | 'folder-error ' | null
/** Core flow props: the owner supplies popover control and pick semantics. */
export interface WorkspaceCreateFlowProps {
@@ -29,6 +31,8 @@ export interface WorkspaceCreateFlowProps {
useWorkspaces : < S > ( selector : ( state : WorkspaceListState ) = > S ) = > S
/** Create or adopt a real Host Workspace. */
createWorkspace : ( input : { name : string } | { path : string } ) = > Promise < WorkspaceView >
/** Open the Host's native single-directory picker. */
pickDirectory : ( ) = > Promise < string | null >
/** A real Workspace was picked or created. */
onPick : ( workspaceId : WorkspaceId ) = > void
/** Close the popover (outside click / Escape / post-pick). */
@@ -45,6 +49,7 @@ export function WorkspaceCreateFlow({
anchorRef ,
useWorkspaces ,
createWorkspace ,
pickDirectory ,
onPick ,
onClose ,
} : WorkspaceCreateFlowProps ) {
@@ -55,10 +60,11 @@ export function WorkspaceCreateFlow({
[ anchorRef ] ,
)
const [ modalKind , setModalKind ] = useState < ModalKind > ( null )
const [ pathDraft , setPathDraft ] = useState ( '' )
const [ workspaceName , setWorkspaceName ] = useState ( '' )
const [ creating , setCreating ] = useState ( false )
const [ modalError , setModalError ] = useState < string | null > ( null )
const [ pickingFolder , setPickingFolder ] = useState ( false )
const [ folderConflict , setFolderConflict ] = useState ( false )
const normalizedWorkspaceName = workspaceName . trim ( )
const duplicateWorkspaceName = ! creating && normalizedWorkspaceName !== ''
&& workspaces . some ( workspace = > workspace . title === normalizedWorkspaceName )
@@ -68,17 +74,11 @@ export function WorkspaceCreateFlow({
id : workspace.workspaceId as string ,
label : workspace.title ,
icon : < IconFolderClose16 size = { 16 } / > ,
disabled : pickingFolder ,
} ) ) ,
. . . ( workspaces . length > 0 ? [ { type : 'separator' as const , id : 'sep-create' } ] : [ ] ) ,
{
id : CREATE_WORKSPACE ,
label : 'Create workspace' ,
icon : < IconPlusOutline16 size = { 16 } / > ,
submenu : [
{ id : USE_EXISTING , label : 'Use an existing folder' } ,
{ id : CREATE_NEW , label : 'Create a new workspace' } ,
] ,
} ,
{ id : OPEN_LOCAL_FOLDER , label : 'Open local folder…' , icon : < IconFolderClose16 size = { 16 } / > , disabled : pickingFolder } ,
{ id : CREATE_NEW , label : 'Create a new workspace' , icon : < IconPlusOutline16 size = { 16 } / > , disabled : pickingFolder } ,
]
const closeModal = ( ) : void = > {
@@ -87,12 +87,31 @@ export function WorkspaceCreateFlow({
setModalError ( null )
}
const openLocalFolder = ( ) : void = > {
if ( pickingFolder ) return
onClose ( )
setModalKind ( null )
setModalError ( null )
setFolderConflict ( false )
setPickingFolder ( true )
void pickDirectory ( ) . then ( async ( path ) = > {
if ( path === null ) return
const workspace = await createWorkspace ( { path } )
onPick ( workspace . workspaceId )
} ) . catch ( ( reason : unknown ) = > {
setFolderConflict (
reason instanceof WorkspaceCreateError
&& reason . rpcError . code === 'workspace-name-conflict' ,
)
setModalError ( reason instanceof Error ? reason.message : String ( reason ) )
setModalKind ( 'folder-error' )
} ) . finally ( ( ) = > { setPickingFolder ( false ) } )
}
const handleSelect = ( id : string ) : void = > {
if ( id === USE_EXISTING ) {
onClose ( )
setPathDraft ( '' )
setModalError ( null )
setModalKind ( 'path' )
if ( pickingFolder ) return
if ( id === OPEN_LOCAL_FOLDER ) {
openLocalFolder ( )
return
}
if ( id === CREATE_NEW ) {
@@ -120,11 +139,6 @@ export function WorkspaceCreateFlow({
} )
}
const confirmPath = ( ) : void = > {
const path = pathDraft . trim ( )
if ( path !== '' ) create ( { path } )
}
const confirmCreate = ( ) : void = > {
if ( normalizedWorkspaceName !== '' && ! duplicateWorkspaceName ) {
create ( { name : normalizedWorkspaceName } )
@@ -144,40 +158,21 @@ export function WorkspaceCreateFlow({
/ >
{ open && workspaceSnapshot . phase === 'pending' && < div className = { css . menuStatus } role = "status" > Loading workspaces … < / div > }
< Modal
open = { modalKind === 'path ' }
open = { modalKind === 'folder-error ' }
onClose = { closeModal }
title = "Use an existing folder"
title = { folderConflict ? 'A workspace with this name already exists' : 'Couldn’ t open folder' }
footer = { (
< >
< Button variant = "outline" className = { css . modalAction ! } disabled = { creating } onClick = { closeModal } > Cancel < / Button >
< Button
variant = "primary"
className = { css . modalAction ! }
disabled = { creating || pathDraft . trim ( ) === '' }
onClick = { confirmPath }
>
Use folder
< / Button >
< Button variant = "outline" className = { css . modalAction ! } onClick = { closeModal } > Cancel < / Button >
< Button variant = "primary" className = { css . modalAction ! } onClick = { openLocalFolder } > Choose again < / Button >
< / >
) }
>
< input
className = { css . modalInput }
value = { pathDraft }
aria-label = "Existing folder path"
autoFocus
disabled = { creating }
placeholder = "/path/to/project"
onChange = { ( event ) = > { setPathDraft ( event . target . value ) } }
onKeyDown = { ( event ) = > {
if ( event . key === 'Enter' ) {
event . preventDefault ( )
confirmPath ( )
}
} }
/ >
{ creating && < div className = { css . modalStatus } role = "status" > Creating workspace … < / div > }
{ modalError !== null && < div className = { css . modalError } role = "alert" > { modalError } < / div > }
< div className = { css . modalError } role = "alert" >
{ folderConflict
? 'Choose a folder with a different name.'
: modalError }
< / div >
< / Modal >
< Modal
open = { modalKind === 'create' }
@@ -235,6 +230,7 @@ export function WorkspacePicker({
onPick ,
onClose ,
createWorkspace ,
pickDirectory ,
} : WorkspacePickerProps ) {
return (
< WorkspaceCreateFlow
@@ -242,6 +238,7 @@ export function WorkspacePicker({
anchorRef = { anchorRef }
useWorkspaces = { useWorkspaces }
createWorkspace = { createWorkspace }
pickDirectory = { pickDirectory }
onPick = { onPick }
onClose = { onClose }
/ >