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()
})