diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css index 0bfd772f60..11f3502325 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css @@ -142,6 +142,8 @@ align-self: center; /* figma 75:8208: 12 between hero chrome / workspace row / card. */ gap: 12px; + /* Foot inside the centered box floats the stack a bit above true center. */ + padding-bottom: 32px; width: min(776px, calc(100% - 48px)); z-index: 1; } diff --git a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx index 2c97e5ce0e..382a42cb40 100644 --- a/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx @@ -36,27 +36,42 @@ export function ConversationRoot({ workspace => workspace.workspaceId === pendingWorkspaceId, ) + // Clear the pending pick once the session lands in it, or when the picked + // workspace disappears from a ready list (deleted from the sidebar). useEffect(() => { - if (pendingWorkspaceId !== undefined - && sessionWorkspace?.workspaceId === pendingWorkspaceId) { + if (pendingWorkspaceId === undefined) return + if (sessionWorkspace?.workspaceId === pendingWorkspaceId + || (workspaces.phase === 'ready' && pendingWorkspace === undefined)) { setPendingWorkspaceId(undefined) } - }, [pendingWorkspaceId, sessionWorkspace?.workspaceId]) + }, [pendingWorkspaceId, sessionWorkspace?.workspaceId, workspaces.phase, pendingWorkspace]) const hero = sessionId === undefined || (composerPhase === 'blank' && (openState === 'open' || openState === 'loading')) const zone: InputZone | undefined = session === undefined || inputState === undefined ? undefined : { session, input: inputState } + // Flow optimization — worth a close PR review for code/boundary issues. + // The chip is a selector; label resolution walks the flow top-down: + // 1. a just-picked workspace (pending) → its title; + // 2. cold start, no session yet → placeholder ("Choose workspace"); + // 3. the blank session's workspace is in the list → its title; + // 4. list still loading → cwd folder name bridges so the title does not + // flash on refresh (empty cwd → placeholder); + // 5. list ready but no owning workspace (deleted from the sidebar) → + // placeholder, never the deleted folder's name via cwd. + const chipTitle = pendingWorkspace?.title + ?? (sessionId === undefined + ? undefined + : sessionWorkspace?.title + ?? (workspaces.phase === 'ready' || cwd === undefined || cwd === '' + ? undefined + : workspaceLabel(cwd))) + const heroWorkspaceRow = (
{ setPickerOpen(open => !open) }} /> diff --git a/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx b/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx index c6122ecbcc..ca64fd7f44 100644 --- a/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/EmptyHero.tsx @@ -7,20 +7,18 @@ import { useId } from 'react' import type { ReactNode, RefObject } from 'react' import { - FishLogo, IconChevronDownOutline14, IconFolderOpen16, + FishLogo, IconChevronDownOutline14, IconFolderClose16, IconFolderOpen16, } from '@deepseek-ai/dsh-client-ui-primitives' import { workspaceTitleOf } from '@deepseek-ai/dsh-client-runtime/client' import css from './HeroShell.module.css' /** - * Basename label for the workspace chip / menu rows (the shared derivation); - * empty → the design's "New Workspace" placeholder copy; separator-only - * paths echo the raw cwd. - * @param cwd - workspace directory path ('' for none). + * Basename label for the workspace chip (the shared derivation); + * separator-only paths echo the raw cwd. + * @param cwd - workspace directory path (non-empty). * @returns chip label. */ export function workspaceLabel(cwd: string): string { - if (cwd === '') return 'New Workspace' const base = workspaceTitleOf(cwd) return base !== '' ? base : cwd } @@ -28,15 +26,17 @@ export function workspaceLabel(cwd: string): string { /** * The workspace chip (folder + label + chevron), always interactive: before * the first message the workspace stays switchable — picking another one - * moves the New Session flow to that workspace's blank session. - * @param props.label - chip label (see {@link workspaceLabel}). + * moves the New Session flow to that workspace's blank session. Without a + * label the chip renders its placeholder state: closed folder + the + * "Choose workspace" call to action. + * @param props.label - chip label (see {@link workspaceLabel}); omitted → placeholder. * @param props.menuOpen - menu expansion echo. * @param props.onClick - menu toggle. * @returns the chip button element. */ export function WorkspaceChip({ buttonRef, label, menuOpen = false, onClick }: { buttonRef?: RefObject - label: string + label?: string | undefined menuOpen?: boolean onClick?: () => void }) { @@ -50,8 +50,10 @@ export function WorkspaceChip({ buttonRef, label, menuOpen = false, onClick }: { aria-expanded={menuOpen} onClick={onClick} > - - {label} + {label === undefined + ? + : } + {label ?? 'Choose workspace'} ) diff --git a/packages/client/ui-sidebar/src/client/SidebarRoot.module.css b/packages/client/ui-sidebar/src/client/SidebarRoot.module.css index fe73435df8..ebb47467af 100644 --- a/packages/client/ui-sidebar/src/client/SidebarRoot.module.css +++ b/packages/client/ui-sidebar/src/client/SidebarRoot.module.css @@ -79,13 +79,19 @@ /* Brand group (figma I133:7632): the full wordmark rides the text ink (figma-flows ruling: main-screen instance is black; blue is brand - emphasis only). */ + emphasis only). A button only in behavior (New Session shortcut): the + pointer cursor is the sole affordance — no hover chrome on the mark. */ .brand { flex: 1; min-width: 0; display: inline-flex; align-items: center; overflow: hidden; + padding: 0; + border: none; + background: transparent; + color: inherit; + cursor: pointer; } .iconButton { diff --git a/packages/client/ui-sidebar/src/client/SidebarRoot.tsx b/packages/client/ui-sidebar/src/client/SidebarRoot.tsx index f5f810cbbe..30d705c780 100644 --- a/packages/client/ui-sidebar/src/client/SidebarRoot.tsx +++ b/packages/client/ui-sidebar/src/client/SidebarRoot.tsx @@ -61,10 +61,17 @@ export function SidebarRoot({ style={wide ? { width: collapsed ? lastWideWidth.current : width } : undefined} >
+ {/* Expanded, the wordmark doubles as a New Session shortcut; the + collapsed rail's logo is the expand toggle below instead. */} {wide && ( - + )} {/* Rail resting state is the whale mark; hovering swaps in the panel icon (the expand affordance, figma sidebar-hover flow). */} diff --git a/packages/client/ui-sidebar/tests/sidebar-root.spec.tsx b/packages/client/ui-sidebar/tests/sidebar-root.spec.tsx index 458ed0bda4..3c8086e4ce 100644 --- a/packages/client/ui-sidebar/tests/sidebar-root.spec.tsx +++ b/packages/client/ui-sidebar/tests/sidebar-root.spec.tsx @@ -54,10 +54,13 @@ function mountShell({ collapsed = false, width = 300 }: { collapsed?: boolean; w } describe('SidebarRoot shell', () => { - it('routes New Session and the column toggle', () => { + it('routes New Session (capsule + wordmark) and the column toggle', () => { const b = mountShell() - fireEvent.click(screen.getByRole('button', { name: 'New session' })) - expect(b.startSession).toHaveBeenCalledWith() + // Expanded, both the wordmark and the capsule start a session. + const starters = screen.getAllByRole('button', { name: 'New session' }) + expect(starters).toHaveLength(2) + for (const button of starters) fireEvent.click(button) + expect(b.startSession).toHaveBeenCalledTimes(2) fireEvent.click(screen.getByRole('button', { name: 'Collapse sidebar' })) expect(b.toggleSidebar).toHaveBeenCalledOnce() })