Merge remote-tracking branch 'origin/master' into worktree-session-hover-menu-fix
# Conflicts: # apps/web/tests/workspace-management.e2e.ts # packages/client/ui-workspace/README.i18n.yaml
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* The workspace/session browsing region filling the sidebar shell's
|
||||
* `sidebar.workspaces` hole: section header (title + group-by + new
|
||||
* `sidebar.workspaces` hole: section header (title + group-by + add
|
||||
* workspace), search, the grouped tree or flat list, and the workspace
|
||||
* dialogs. Wide state renders the full browser; rail state renders the two
|
||||
* region icons (search / new workspace), each requesting shell expansion
|
||||
* through the owner share. The picker menu and create dialogs live in
|
||||
* WorkspacePicker (same package — direct composition, no slot between them).
|
||||
* region icons (search / add workspace), each requesting shell expansion
|
||||
* through the owner share. Adding is the header button's one action, so it
|
||||
* raises the directory flow with no menu in between; the flow and its error
|
||||
* dialog live in WorkspacePicker (same package — direct composition, no slot
|
||||
* between them).
|
||||
*/
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
@@ -20,7 +22,7 @@ import type { WorkspaceBrowserProps } from './contract/slots.ts'
|
||||
import type { SessionNode } from './tree.ts'
|
||||
import { deriveFlat, deriveGroups, deriveSearchResults, UNGROUPED_KEY } from './tree.ts'
|
||||
import { ProjectRowItem, SearchResultItem, SessionNodeItem } from './rows/Rows.tsx'
|
||||
import { WorkspaceCreateFlow } from './WorkspacePicker.tsx'
|
||||
import { WorkspacePickFlow } from './WorkspacePicker.tsx'
|
||||
import css from './WorkspaceBrowser.module.css'
|
||||
|
||||
/**
|
||||
@@ -358,6 +360,9 @@ export function WorkspaceBrowser({
|
||||
}: WorkspaceBrowserProps) {
|
||||
const workspaces = useWorkspaces(state => state.items)
|
||||
const archivedSessionIds = useWorkspaces(state => state.archivedSessionIds)
|
||||
// Live occupancy of this surface's directory-flow hole (the same source the
|
||||
// flow reads): a composition without a picking affordance can add nothing.
|
||||
const directoryFlowAvailable = useDirectoryFlow(occupied => occupied)
|
||||
const groupBy = useStore(s => s.groupBy)
|
||||
// The query outlives the tree and the input (both wide-only) so collapsing
|
||||
// does not silently drop an in-progress filter.
|
||||
@@ -541,21 +546,26 @@ export function WorkspaceBrowser({
|
||||
</span>
|
||||
)}
|
||||
{wide && <GroupByMenu groupBy={groupBy} onPick={(mode) => { actions.setGroupBy(mode) }} t={t} />}
|
||||
<Tooltip label={t('workspace.new')} disabled={wide}>
|
||||
<button
|
||||
ref={wsPlusRef}
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
aria-label={t('create.confirm')}
|
||||
onClick={() => {
|
||||
setWsPickerOpen(v => !v)
|
||||
}}
|
||||
>
|
||||
<IconProjectAddOutline16 size={wide ? 16 : 18} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/* Picker menu + create dialogs (same package — direct composition). */}
|
||||
<WorkspaceCreateFlow
|
||||
{/* Adding is the button's one action, so a composition with no
|
||||
picking affordance has nothing to offer here: the region hides the
|
||||
button rather than leaving a dead one in the header. */}
|
||||
{directoryFlowAvailable && (
|
||||
<Tooltip label={t('workspace.add')} disabled={wide}>
|
||||
<button
|
||||
ref={wsPlusRef}
|
||||
type="button"
|
||||
className={css.iconButton}
|
||||
aria-label={t('workspace.add')}
|
||||
onClick={() => {
|
||||
setWsPickerOpen(v => !v)
|
||||
}}
|
||||
>
|
||||
<IconProjectAddOutline16 size={wide ? 16 : 18} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* Add flow + its error dialog (same package — direct composition). */}
|
||||
<WorkspacePickFlow
|
||||
t={t}
|
||||
open={wsPickerOpen}
|
||||
anchorRef={wsPlusRef}
|
||||
@@ -563,7 +573,7 @@ export function WorkspaceBrowser({
|
||||
createWorkspace={createWorkspace}
|
||||
useDirectoryFlow={useDirectoryFlow}
|
||||
renderDirectoryFlow={owner => renderSlot('sidebar.workspaces.directoryFlow', owner)}
|
||||
createOnly
|
||||
addOnly
|
||||
side="right"
|
||||
onPick={(workspaceId) => {
|
||||
setWsPickerOpen(false)
|
||||
|
||||
@@ -1,35 +1,10 @@
|
||||
/* Modal form styles mirror the empty state's path/create modals (same figma
|
||||
* dialog family: field h44, r22, hairline border, pad 14/7) so the two
|
||||
* entries stay visually identical. */
|
||||
.modalInput {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding: 7px 14px;
|
||||
border: 1px solid var(--dsw-alias-border-l2);
|
||||
border-radius: 22px;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
color: var(--dsw-alias-label-primary);
|
||||
}
|
||||
|
||||
.modalInput::placeholder {
|
||||
color: var(--dsw-alias-label-caption);
|
||||
}
|
||||
|
||||
.modalInput:disabled {
|
||||
color: var(--dsw-alias-label-dimmed);
|
||||
}
|
||||
|
||||
/* The adoption error dialog's footer and message styles; the dialog itself is
|
||||
* the shared Modal (same figma dialog family as the browser's own dialogs). */
|
||||
.modalAction {
|
||||
min-width: 72px;
|
||||
}
|
||||
|
||||
.modalError,
|
||||
.modalStatus,
|
||||
.menuStatus {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
@@ -40,7 +15,6 @@
|
||||
color: var(--dsw-alias-state-error-primary);
|
||||
}
|
||||
|
||||
.modalStatus,
|
||||
.menuStatus {
|
||||
color: var(--dsw-alias-label-secondary);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/**
|
||||
* Workspace pick/create flow. WorkspaceCreateFlow is the reusable core
|
||||
* (menu + path/create dialogs) consumed directly by WorkspaceBrowser (same
|
||||
* package) and wrapped by WorkspacePicker for the conversation empty-state
|
||||
* slot registration. Directory picking itself lives in the composed flow
|
||||
* package's slot occupant (see the contract module doc): this core only
|
||||
* opens the flow, adopts the picked path, and owns the error surface.
|
||||
* Workspace pick/add flow. WorkspacePickFlow is the reusable core (menu +
|
||||
* path error dialog) consumed directly by WorkspaceBrowser (same package) and
|
||||
* wrapped by WorkspacePicker for the conversation empty-state slot
|
||||
* registration. Directory picking itself lives in the composed flow package's
|
||||
* slot occupant (see the contract module doc): this core only opens the flow,
|
||||
* adopts the picked path, and owns the error surface. Adding a workspace has
|
||||
* exactly one route — pick a host directory, new or existing — because the
|
||||
* occupant's own create-folder affordance already covers creating one.
|
||||
*/
|
||||
import type { ReactNode, RefObject } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
Button, IconFolderClose16, IconPlusOutline16, Menu, Modal, type MenuEntry,
|
||||
} from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
@@ -19,13 +21,10 @@ import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import type { DirectoryFlowOwnerProps, WorkspacePickerProps } from './contract/slots.ts'
|
||||
import css from './WorkspacePicker.module.css'
|
||||
|
||||
const OPEN_LOCAL_FOLDER = '::open-local-folder'
|
||||
const CREATE_NEW = '::create-new'
|
||||
|
||||
type ModalKind = 'create' | 'folder-error' | null
|
||||
const ADD_WORKSPACE = '::add-workspace'
|
||||
|
||||
/** Core flow props: the owner supplies popover control and pick semantics. */
|
||||
export interface WorkspaceCreateFlowProps {
|
||||
export interface WorkspacePickFlowProps {
|
||||
/** The standard locale seat, forwarded by whichever slot entry hosts the flow. */
|
||||
t: WorkspacePickerProps['t']
|
||||
/** Popover visibility (anchor button toggle state, owner-local). */
|
||||
@@ -34,9 +33,9 @@ export interface WorkspaceCreateFlowProps {
|
||||
anchorRef?: RefObject<HTMLElement | null> | undefined
|
||||
/** Selector hook over the workspace list (framework standard hook). */
|
||||
useWorkspaces: <S>(selector: (state: WorkspaceListState) => S) => S
|
||||
/** Create or adopt a real Host Workspace. */
|
||||
createWorkspace: (input: { name: string } | { path: string }) => Promise<WorkspaceView>
|
||||
/** Bound occupancy selector hook for this surface's directory-flow hole (empty hides the local-folder entry). */
|
||||
/** Adopt a picked host directory as a real Workspace. */
|
||||
createWorkspace: (input: { path: string }) => Promise<WorkspaceView>
|
||||
/** Bound occupancy selector hook for this surface's directory-flow hole (empty leaves the surface with no add action). */
|
||||
useDirectoryFlow: SnapshotSelectorHook<boolean>
|
||||
/** Render this surface's directory-flow hole with the owner conversation (the entry's narrowed renderSlot). */
|
||||
renderDirectoryFlow: (owner: DirectoryFlowOwnerProps) => ReactNode
|
||||
@@ -44,8 +43,8 @@ export interface WorkspaceCreateFlowProps {
|
||||
onPick: (workspaceId: WorkspaceId) => void
|
||||
/** Close the popover (outside click / Escape / post-pick). */
|
||||
onClose: () => void
|
||||
/** Only show create actions (open folder / create new), hide existing workspaces. */
|
||||
createOnly?: boolean
|
||||
/** Only offer the add action, hide existing workspaces. */
|
||||
addOnly?: boolean
|
||||
/** Menu opening direction relative to the anchor. */
|
||||
side?: 'bottom' | 'top' | 'right'
|
||||
/** Currently active workspace (trailing check in the picker list). */
|
||||
@@ -53,11 +52,11 @@ export interface WorkspaceCreateFlowProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the pick menu plus the two create dialogs.
|
||||
* Render the pick menu plus the adoption error dialog.
|
||||
* @param props - owner-controlled flow props.
|
||||
* @returns menu + dialog elements.
|
||||
*/
|
||||
export function WorkspaceCreateFlow({
|
||||
export function WorkspacePickFlow({
|
||||
t,
|
||||
open,
|
||||
anchorRef,
|
||||
@@ -67,32 +66,26 @@ export function WorkspaceCreateFlow({
|
||||
renderDirectoryFlow,
|
||||
onPick,
|
||||
onClose,
|
||||
createOnly = false,
|
||||
addOnly = false,
|
||||
side = 'bottom',
|
||||
selectedId,
|
||||
}: WorkspaceCreateFlowProps) {
|
||||
}: WorkspacePickFlowProps) {
|
||||
const workspaceSnapshot = useWorkspaces(state => state)
|
||||
const workspaces = workspaceSnapshot.items
|
||||
const getAnchorRect = useCallback(
|
||||
() => anchorRef?.current?.getBoundingClientRect() ?? null,
|
||||
[anchorRef],
|
||||
)
|
||||
const [modalKind, setModalKind] = useState<ModalKind>(null)
|
||||
const [workspaceName, setWorkspaceName] = useState('')
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [errorOpen, setErrorOpen] = useState(false)
|
||||
const [modalError, setModalError] = useState<string | null>(null)
|
||||
const [flowOpen, setFlowOpen] = useState(false)
|
||||
const [pickingFolder, setPickingFolder] = useState(false)
|
||||
const [folderConflict, setFolderConflict] = useState(false)
|
||||
const composingRef = useRef(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
|
||||
// selection or creation.
|
||||
// selection or adoption.
|
||||
const flowBusy = flowOpen || pickingFolder
|
||||
const normalizedWorkspaceName = workspaceName.trim()
|
||||
const duplicateWorkspaceName = !creating && normalizedWorkspaceName !== ''
|
||||
&& workspaces.some(workspace => workspace.title === normalizedWorkspaceName)
|
||||
|
||||
// The occupied hole gates the picking affordance: with no composed flow the
|
||||
// entry simply is not there (the seam's documented no-flow default). The
|
||||
@@ -107,27 +100,27 @@ export function WorkspaceCreateFlow({
|
||||
useEffect(() => {
|
||||
if (flowOpen && !flowAvailable) setFlowOpen(false)
|
||||
}, [flowOpen, flowAvailable])
|
||||
const createEntries: MenuEntry[] = [
|
||||
...(flowAvailable
|
||||
? [{ id: OPEN_LOCAL_FOLDER, label: t('menu.openFolder'), icon: <IconFolderClose16 size={16} />, disabled: flowBusy }]
|
||||
: []),
|
||||
{ id: CREATE_NEW, label: t('menu.createWorkspace'), icon: <IconPlusOutline16 size={16} />, disabled: flowBusy },
|
||||
]
|
||||
// With workspaces listed, the create actions pin below the scroll region
|
||||
// (divider + always visible); otherwise they ARE the menu.
|
||||
const pinCreate = !createOnly && workspaces.length > 0
|
||||
const items: MenuEntry[] = pinCreate
|
||||
const addEntries: MenuEntry[] = flowAvailable
|
||||
? [{ id: ADD_WORKSPACE, label: t('menu.addWorkspace'), icon: <IconPlusOutline16 size={16} />, disabled: flowBusy }]
|
||||
: []
|
||||
// With workspaces listed, the add action pins below the scroll region
|
||||
// (divider + always visible); otherwise it IS the menu.
|
||||
const pinAdd = !addOnly && workspaces.length > 0
|
||||
const items: MenuEntry[] = pinAdd
|
||||
? workspaces.map(workspace => ({
|
||||
id: workspace.workspaceId,
|
||||
label: workspace.title,
|
||||
icon: <IconFolderClose16 size={16} />,
|
||||
disabled: flowBusy,
|
||||
}))
|
||||
: createEntries
|
||||
: addEntries
|
||||
// Nothing listed and nothing to add with (a composition that mounts this
|
||||
// package without any directory-picker): an empty popover would claim a
|
||||
// choice that does not exist, so the anchor gesture shows nothing at all.
|
||||
const menuIsEmpty = items.length === 0
|
||||
|
||||
const closeModal = (): void => {
|
||||
if (creating) return
|
||||
setModalKind(null)
|
||||
setErrorOpen(false)
|
||||
setModalError(null)
|
||||
}
|
||||
|
||||
@@ -143,16 +136,32 @@ export function WorkspaceCreateFlow({
|
||||
)
|
||||
setModalError(reason instanceof Error ? reason.message : String(reason))
|
||||
setFlowOpen(false)
|
||||
setModalKind('folder-error')
|
||||
setErrorOpen(true)
|
||||
})
|
||||
|
||||
const openLocalFolder = (): void => {
|
||||
const openDirectoryFlow = useCallback((): void => {
|
||||
onClose()
|
||||
setModalKind(null)
|
||||
setErrorOpen(false)
|
||||
setModalError(null)
|
||||
setFolderConflict(false)
|
||||
setFlowOpen(true)
|
||||
}
|
||||
}, [onClose])
|
||||
|
||||
// A menu exists to disambiguate between targets. With no workspaces listed
|
||||
// and the add action the only entry left, the anchor gesture IS that action:
|
||||
// a one-row popover would cost a click and offer nothing to choose between.
|
||||
// The owner's open request is consumed the same way selecting the entry
|
||||
// would consume it (close the popover, raise the flow). An empty list is
|
||||
// only final once the baseline lands — until then the menu stays up with its
|
||||
// loading status instead of jumping into a flow the arriving list would have
|
||||
// made unnecessary; the add-only surface lists nothing and never waits.
|
||||
const listSettled = addOnly || workspaceSnapshot.phase === 'ready'
|
||||
const addIsTheOnlyEntry = !pinAdd && listSettled && addEntries.length === 1
|
||||
// `flowBusy` gates this exactly as it disables the equivalent menu entry: a
|
||||
// pick still being adopted owns the surface until it settles.
|
||||
useEffect(() => {
|
||||
if (open && addIsTheOnlyEntry && !flowBusy) openDirectoryFlow()
|
||||
}, [open, addIsTheOnlyEntry, flowBusy, openDirectoryFlow])
|
||||
|
||||
/** Owner side of the flow conversation: adopt keeps the flow open (busy) until the Host answers. */
|
||||
const flowOwner: DirectoryFlowOwnerProps = {
|
||||
@@ -167,53 +176,25 @@ export function WorkspaceCreateFlow({
|
||||
setFlowOpen(false)
|
||||
setFolderConflict(false)
|
||||
setModalError(message)
|
||||
setModalKind('folder-error')
|
||||
setErrorOpen(true)
|
||||
},
|
||||
}
|
||||
|
||||
const handleSelect = (id: string): void => {
|
||||
if (id === OPEN_LOCAL_FOLDER) {
|
||||
openLocalFolder()
|
||||
return
|
||||
}
|
||||
if (id === CREATE_NEW) {
|
||||
onClose()
|
||||
setWorkspaceName('')
|
||||
setModalError(null)
|
||||
setModalKind('create')
|
||||
if (id === ADD_WORKSPACE) {
|
||||
openDirectoryFlow()
|
||||
return
|
||||
}
|
||||
onPick(id as WorkspaceId)
|
||||
}
|
||||
|
||||
const create = (input: { name: string } | { path: string }): void => {
|
||||
if (creating) return
|
||||
setCreating(true)
|
||||
setModalError(null)
|
||||
void createWorkspace(input).then((workspace) => {
|
||||
setCreating(false)
|
||||
setModalKind(null)
|
||||
onPick(workspace.workspaceId)
|
||||
}).catch((reason: unknown) => {
|
||||
const message = reason instanceof Error ? reason.message : String(reason)
|
||||
setModalError(`Workspace creation failed: ${message}`)
|
||||
setCreating(false)
|
||||
})
|
||||
}
|
||||
|
||||
const confirmCreate = (): void => {
|
||||
if (normalizedWorkspaceName !== '' && !duplicateWorkspaceName) {
|
||||
create({ name: normalizedWorkspaceName })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu
|
||||
open={open}
|
||||
open={open && !addIsTheOnlyEntry && !menuIsEmpty}
|
||||
anchor={null}
|
||||
items={items}
|
||||
{...pinCreate ? { footer: createEntries } : {}}
|
||||
{...pinAdd ? { footer: addEntries } : {}}
|
||||
selectedId={selectedId}
|
||||
onSelect={handleSelect}
|
||||
onClose={onClose}
|
||||
@@ -221,10 +202,10 @@ export function WorkspaceCreateFlow({
|
||||
portal
|
||||
getAnchorRect={getAnchorRect}
|
||||
/>
|
||||
{open && workspaceSnapshot.phase === 'pending' && <div className={css.menuStatus} role="status">{t('picker.loading')}</div>}
|
||||
{open && !addIsTheOnlyEntry && !menuIsEmpty && workspaceSnapshot.phase === 'pending' && <div className={css.menuStatus} role="status">{t('picker.loading')}</div>}
|
||||
{renderDirectoryFlow(flowOwner)}
|
||||
<Modal
|
||||
open={modalKind === 'folder-error'}
|
||||
open={errorOpen}
|
||||
onClose={closeModal}
|
||||
closeLabel={t('close')}
|
||||
title={folderConflict ? t('conflict.title') : t('folderError.title')}
|
||||
@@ -233,7 +214,7 @@ export function WorkspaceCreateFlow({
|
||||
<Button variant="outline" className={css.modalAction} onClick={closeModal}>{t('cancel')}</Button>
|
||||
{/* Retrying needs an occupant to serve the flow; without one the
|
||||
* button would open a flow nobody can answer or cancel. */}
|
||||
<Button variant="primary" className={css.modalAction} disabled={!flowAvailable} onClick={openLocalFolder}>{t('folderError.retry')}</Button>
|
||||
<Button variant="primary" className={css.modalAction} disabled={!flowAvailable} onClick={openDirectoryFlow}>{t('folderError.retry')}</Button>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
@@ -243,49 +224,6 @@ export function WorkspaceCreateFlow({
|
||||
: modalError}
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
open={modalKind === 'create'}
|
||||
onClose={closeModal}
|
||||
closeLabel={t('close')}
|
||||
title={t('menu.createWorkspace')}
|
||||
description={t('create.desc')}
|
||||
footer={(
|
||||
<>
|
||||
<Button variant="outline" className={css.modalAction} disabled={creating} onClick={closeModal}>{t('cancel')}</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
className={css.modalAction}
|
||||
disabled={creating || normalizedWorkspaceName === '' || duplicateWorkspaceName}
|
||||
onClick={confirmCreate}
|
||||
>
|
||||
{t('create.confirm')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<input
|
||||
className={css.modalInput}
|
||||
value={workspaceName}
|
||||
placeholder={t('field.workspaceName')}
|
||||
aria-label={t('create.name.aria')}
|
||||
autoFocus
|
||||
disabled={creating}
|
||||
onChange={(event) => { setWorkspaceName(event.target.value); setModalError(null) }}
|
||||
onCompositionStart={() => { composingRef.current = true }}
|
||||
onCompositionEnd={() => { composingRef.current = false }}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !composingRef.current) {
|
||||
event.preventDefault()
|
||||
confirmCreate()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{creating && <div className={css.modalStatus} role="status">{t('create.pending')}</div>}
|
||||
{duplicateWorkspaceName && (
|
||||
<div className={css.modalError} role="alert">{t('conflict.named', { name: normalizedWorkspaceName })}</div>
|
||||
)}
|
||||
{modalError !== null && <div className={css.modalError} role="alert">{modalError}</div>}
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -309,7 +247,7 @@ export function WorkspacePicker({
|
||||
t,
|
||||
}: WorkspacePickerProps) {
|
||||
return (
|
||||
<WorkspaceCreateFlow
|
||||
<WorkspacePickFlow
|
||||
t={t}
|
||||
open={open}
|
||||
anchorRef={anchorRef}
|
||||
|
||||
@@ -5,16 +5,19 @@
|
||||
* the whole browsing region (section header, search, grouped/flat session
|
||||
* list, workspace dialogs). It registers this package's viewing store and
|
||||
* consumes the shell's two-fact owner share (wide / expandSidebar).
|
||||
* - WorkspacePicker fills the conversation empty-state hole (menu +
|
||||
* create dialogs shared with the browser).
|
||||
* - WorkspacePicker fills the conversation empty-state hole (menu + error
|
||||
* dialog shared with the browser).
|
||||
*
|
||||
* Each registration also declares one **directory-flow hole** (`single`
|
||||
* kind): the slot a composed picker package's client half fills with its
|
||||
* picking interaction — a renderless native-chooser driver or an in-app
|
||||
* browsing dialog. ui-workspace owns the trigger (the "Open local folder…"
|
||||
* menu entry, shown only while the hole is occupied) and the adoption
|
||||
* 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.
|
||||
* 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.
|
||||
* Two holes exist because the two menu surfaces are independent slot entries
|
||||
* and a hole has exactly one declaring entry — they carry the same owner
|
||||
* contract and the same occupant.
|
||||
@@ -65,7 +68,7 @@ export type DirectoryFlowSlotName =
|
||||
* Directory-picking share both trigger surfaces consume. Occupancy rides the
|
||||
* inject face's reserved `hooks` compartment: the renderer binds the source
|
||||
* into the `useDirectoryFlow` selector hook, so an empty hole hides the
|
||||
* "Open local folder…" entry reactively and the surface withdraws an open
|
||||
* "Add workspace…" entry reactively and the surface withdraws an open
|
||||
* flow whose occupant unloaded mid-interaction (nobody is left to cancel).
|
||||
*/
|
||||
export type DirectoryPickingInjected = {
|
||||
@@ -125,8 +128,8 @@ export type WorkspaceBrowserInjected = DirectoryPickingInjected & {
|
||||
* the Host response/changed frame; failures leave the order unchanged.
|
||||
*/
|
||||
insertSessionBefore: (workspaceId: WorkspaceId, sessionId: SessionId, beforeSessionId?: SessionId) => Promise<void>
|
||||
/** Explicitly create or adopt a real Workspace before targeting a Session. */
|
||||
createWorkspace: (input: { name: string } | { path: string }) => Promise<WorkspaceView>
|
||||
/** Adopt a picked host directory as a real Workspace before targeting a Session. */
|
||||
createWorkspace: (input: { path: string }) => Promise<WorkspaceView>
|
||||
}
|
||||
|
||||
/** Full browser props: shell owner share + viewing store + injected actions + the locale seat. */
|
||||
@@ -144,8 +147,8 @@ export type WorkspaceBrowserProps =
|
||||
* supplies the implicit index signature required by the registry.
|
||||
*/
|
||||
export type WorkspacePickerInjected = DirectoryPickingInjected & {
|
||||
/** Explicitly create or adopt a real Workspace before targeting a Session. */
|
||||
createWorkspace: (input: { name: string } | { path: string }) => Promise<WorkspaceView>
|
||||
/** Adopt a picked host directory as a real Workspace before targeting a Session. */
|
||||
createWorkspace: (input: { path: string }) => Promise<WorkspaceView>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* `workspace` namespace dictionaries: the browsing region (section header,
|
||||
* search, tree rows, dialogs) and the pick/create flow. Runtime failure
|
||||
* search, tree rows, dialogs) and the pick/add flow. Runtime failure
|
||||
* messages (wire error strings) pass through untranslated by policy.
|
||||
*/
|
||||
|
||||
@@ -15,7 +15,7 @@ export const zh = {
|
||||
'groupBy.flat': '单列表',
|
||||
'empty.none': '暂无会话',
|
||||
'empty.noMatches': '无匹配结果',
|
||||
'workspace.new': '新建工作区',
|
||||
'workspace.add': '添加工作区',
|
||||
'search.sessions.aria': '搜索会话',
|
||||
'search.placeholder': '搜索名称、关键词…',
|
||||
'search.clear': '清除搜索',
|
||||
@@ -24,18 +24,13 @@ export const zh = {
|
||||
'search.unavailable': '内容搜索暂不可用,仅显示名称匹配。',
|
||||
'search.noMatches': '无匹配会话',
|
||||
'search.hasMore': '仅显示前 {n} 条结果,请缩小搜索范围。',
|
||||
'menu.openFolder': '打开本地文件夹…',
|
||||
'menu.createWorkspace': '新建工作区',
|
||||
'menu.addWorkspace': '添加工作区…',
|
||||
'picker.loading': '正在加载工作区…',
|
||||
'conflict.title': '已存在同名工作区',
|
||||
'conflict.hint': '请选择其他名称的文件夹。',
|
||||
'conflict.named': '已存在名为“{name}”的工作区。',
|
||||
'folderError.title': '无法打开文件夹',
|
||||
'folderError.retry': '重新选择',
|
||||
'create.confirm': '创建工作区',
|
||||
'create.desc': '该名称将同时用于工作区及其新文件夹。',
|
||||
'create.name.aria': '新工作区名称',
|
||||
'create.pending': '正在创建工作区…',
|
||||
'rename': '重命名',
|
||||
'rename.workspace.title': '重命名工作区',
|
||||
'rename.session.title': '重命名会话',
|
||||
@@ -79,7 +74,7 @@ export const en = {
|
||||
'groupBy.flat': 'In one list',
|
||||
'empty.none': 'No sessions yet',
|
||||
'empty.noMatches': 'No matches',
|
||||
'workspace.new': 'New Workspace',
|
||||
'workspace.add': 'Add workspace',
|
||||
'search.sessions.aria': 'Search sessions',
|
||||
'search.placeholder': 'Search name, keywords...',
|
||||
'search.clear': 'Clear search',
|
||||
@@ -88,18 +83,13 @@ export const en = {
|
||||
'search.unavailable': 'Content search is temporarily unavailable. Showing name matches.',
|
||||
'search.noMatches': 'No matching sessions',
|
||||
'search.hasMore': 'Showing the first {n} results. Narrow your search.',
|
||||
'menu.openFolder': 'Open local folder…',
|
||||
'menu.createWorkspace': 'Create a new workspace',
|
||||
'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': 'Couldn’t open folder',
|
||||
'folderError.retry': 'Choose again',
|
||||
'create.confirm': 'Create workspace',
|
||||
'create.desc': 'The name is used for both the workspace and its new folder.',
|
||||
'create.name.aria': 'New workspace name',
|
||||
'create.pending': 'Creating workspace…',
|
||||
'rename': 'Rename',
|
||||
'rename.workspace.title': 'Rename workspace',
|
||||
'rename.session.title': 'Rename session',
|
||||
|
||||
Reference in New Issue
Block a user