Merge remote-tracking branch 'origin/doc/host-client-group-readmes' into feat/directory-picker

# Conflicts:
#	packages/client/connection/src/client/fixture.ts
#	packages/client/connection/tests/fake-api.ts
#	packages/client/runtime/src/client/workspaces/service.ts
#	packages/client/runtime/tests/fake-api.ts
#	packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx
#	packages/client/ui-workspace/src/client/WorkspacePicker.tsx
#	packages/client/ui-workspace/tests/workspace-picker.spec.tsx
#	packages/host/apiproxy/README.i18n.yaml
#	packages/host/apiproxy/src/api-proxy.ts
#	packages/host/apiproxy/src/api/host.schema.ts
#	packages/host/apiproxy/src/api/host.ts
#	packages/host/apiproxy/src/api/rpc-map.ts
#	packages/host/apiproxy/src/fetch/client.ts
#	packages/host/apiproxy/src/fetch/handler.ts
#	packages/host/apiproxy/tests/api-proxy-workspace.spec.ts
#	packages/host/apiproxy/tests/client-handler.spec.ts
#	packages/host/apiproxy/tests/fetch-carrier.spec.ts
This commit is contained in:
creatixchu
2026-07-28 21:21:21 +08:00
750 changed files with 15381 additions and 5577 deletions
@@ -129,6 +129,7 @@
reads the shell's class names): the two icon controls stack as 36x36
circles matching the shell's rail rhythm. */
.rail .sectionHeader {
gap: 0;
padding-left: 0;
margin-bottom: 12px;
}
@@ -207,6 +208,13 @@
min-height: 0;
overflow-y: auto;
padding-bottom: 12px;
/* Row trailing content (the relative time, and the hover action buttons
that replace it) sits flush against the row's 8px right padding, so an
overlaid scrollbar covers it. Reserving the gutter keeps the bar beside
the rows instead of on top of them; `stable` holds the reservation when
the list is short enough not to scroll, so expanding a group does not
shift every row left. */
scrollbar-gutter: stable;
}
/* One workspace section: header row + expanded session run. Rows inside
@@ -266,6 +266,7 @@ export function WorkspaceBrowser({
// states; the menu anchors on this button).
const [wsPickerOpen, setWsPickerOpen] = useState(false)
const wsPlusRef = useRef<HTMLButtonElement>(null)
const composingRef = useRef(false)
// Rail search = expand + land in the search box: the flag arms before the
// expand request; once the shell flips wide the input mounts and takes focus.
@@ -359,7 +360,6 @@ export function WorkspaceBrowser({
className={css.iconButton}
aria-label="Create workspace"
onClick={() => {
if (!wide) expandSidebar()
setWsPickerOpen(v => !v)
}}
>
@@ -374,6 +374,8 @@ export function WorkspaceBrowser({
createWorkspace={createWorkspace}
pickDirectory={pickDirectory}
directoryPickerKind={directoryPickerKind}
createOnly
side="right"
onPick={(workspaceId) => {
setWsPickerOpen(false)
startSession(workspaceId)
@@ -461,9 +463,12 @@ export function WorkspaceBrowser({
aria-label="Workspace name"
autoFocus
disabled={renaming}
onFocus={(e) => { e.target.select() }}
onChange={(e) => { setRenameDraft(e.target.value); setRenameError(null) }}
onCompositionStart={() => { composingRef.current = true }}
onCompositionEnd={() => { composingRef.current = false }}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (e.key === 'Enter' && !composingRef.current) {
e.preventDefault()
confirmRename()
}
@@ -5,7 +5,7 @@
* slot registration.
*/
import type { RefObject } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import {
Button, IconFolderClose16, IconPlusOutline16, Menu, Modal, type MenuEntry,
} from '@deepseek-ai/dsh-client-ui-primitives'
@@ -39,6 +39,12 @@ 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
/** Menu opening direction relative to the anchor. */
side?: 'bottom' | 'top' | 'right'
/** Currently active workspace (trailing check in the picker list). */
selectedId?: WorkspaceId | undefined
}
/**
@@ -55,6 +61,9 @@ export function WorkspaceCreateFlow({
directoryPickerKind,
onPick,
onClose,
createOnly = false,
side = 'bottom',
selectedId,
}: WorkspaceCreateFlowProps) {
const workspaceSnapshot = useWorkspaces(state => state)
const workspaces = workspaceSnapshot.items
@@ -68,6 +77,7 @@ export function WorkspaceCreateFlow({
const [modalError, setModalError] = useState<string | null>(null)
const [pickingFolder, setPickingFolder] = useState(false)
const [folderConflict, setFolderConflict] = useState(false)
const composingRef = useRef(false)
const normalizedWorkspaceName = workspaceName.trim()
const duplicateWorkspaceName = !creating && normalizedWorkspaceName !== ''
&& workspaces.some(workspace => workspace.title === normalizedWorkspaceName)
@@ -100,19 +110,23 @@ export function WorkspaceCreateFlow({
return () => { stale = true }
}, [open, directoryPickerKind])
const items: MenuEntry[] = [
...workspaces.map(workspace => ({
id: workspace.workspaceId,
label: workspace.title,
icon: <IconFolderClose16 size={16} />,
disabled: pickingFolder,
})),
...(workspaces.length > 0 ? [{ type: 'separator' as const, id: 'sep-create' }] : []),
const createEntries: MenuEntry[] = [
...(nativePicker
? [{ 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 },
]
// 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
? workspaces.map(workspace => ({
id: workspace.workspaceId,
label: workspace.title,
icon: <IconFolderClose16 size={16} />,
disabled: pickingFolder,
}))
: createEntries
const closeModal = (): void => {
if (creating) return
@@ -147,7 +161,7 @@ export function WorkspaceCreateFlow({
}
if (id === CREATE_NEW) {
onClose()
setWorkspaceName('workspace')
setWorkspaceName('')
setModalError(null)
setModalKind('create')
return
@@ -182,8 +196,11 @@ export function WorkspaceCreateFlow({
open={open}
anchor={null}
items={items}
{...pinCreate ? { footer: createEntries } : {}}
selectedId={selectedId}
onSelect={handleSelect}
onClose={onClose}
side={side}
portal
getAnchorRect={getAnchorRect}
/>
@@ -227,12 +244,15 @@ export function WorkspaceCreateFlow({
<input
className={css.modalInput}
value={workspaceName}
placeholder="Workspace name"
aria-label="New workspace name"
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') {
if (event.key === 'Enter' && !composingRef.current) {
event.preventDefault()
confirmCreate()
}
@@ -258,6 +278,7 @@ export function WorkspacePicker({
open,
anchorRef,
useWorkspaces,
selectedId,
onPick,
onClose,
createWorkspace,
@@ -272,6 +293,7 @@ export function WorkspacePicker({
createWorkspace={createWorkspace}
pickDirectory={pickDirectory}
directoryPickerKind={directoryPickerKind}
selectedId={selectedId}
onPick={onPick}
onClose={onClose}
/>