diff --git a/desktop/src/__tests__/pages.test.tsx b/desktop/src/__tests__/pages.test.tsx
index 24e38544..b6833951 100644
--- a/desktop/src/__tests__/pages.test.tsx
+++ b/desktop/src/__tests__/pages.test.tsx
@@ -159,7 +159,7 @@ describe('Content-only pages render without errors', () => {
expect(screen.queryByText('/internal-only')).not.toBeInTheDocument()
})
- it('EmptySession renders mascot and composer', async () => {
+ it('EmptySession renders the launch composer', async () => {
let container!: HTMLElement
await act(async () => {
container = render().container
@@ -167,8 +167,8 @@ describe('Content-only pages render without errors', () => {
await Promise.resolve()
})
expect(container.querySelector('textarea')).toBeInTheDocument()
- expect(container.innerHTML).toContain('New session')
- expect(container.innerHTML).toContain('Ask anything')
+ expect(container.innerHTML).toContain('What should we build?')
+ expect(container.innerHTML).toContain('Ask Claude anything')
})
it('EmptySession shows draft context usage before a session is created', async () => {
@@ -235,11 +235,11 @@ describe('Content-only pages render without errors', () => {
})
const { container } = render()
// With empty messages, the hero is shown
- expect(container.innerHTML).toContain('New session')
+ expect(container.innerHTML).toContain('What should we build?')
// ChatInput has a textarea
const textarea = container.querySelector('textarea')
expect(textarea).toBeInTheDocument()
- expect(textarea).toHaveAttribute('placeholder', 'Ask anything...')
+ expect(textarea).toHaveAttribute('placeholder', 'Ask Claude anything. Type @ to mention files or tools')
expect(textarea).toHaveAttribute('rows', '2')
expect(container.innerHTML).not.toContain('Preview')
// Cleanup
diff --git a/desktop/src/components/shared/DirectoryPicker.test.tsx b/desktop/src/components/shared/DirectoryPicker.test.tsx
index fa638391..eeb86578 100644
--- a/desktop/src/components/shared/DirectoryPicker.test.tsx
+++ b/desktop/src/components/shared/DirectoryPicker.test.tsx
@@ -16,6 +16,7 @@ vi.mock('../../api/filesystem', () => ({
import { DirectoryPicker } from './DirectoryPicker'
import { sessionsApi } from '../../api/sessions'
+import { filesystemApi } from '../../api/filesystem'
describe('DirectoryPicker', () => {
it('uses the source repository name as the fallback label for desktop worktree paths', () => {
@@ -57,4 +58,39 @@ describe('DirectoryPicker', () => {
expect(trigger).toHaveTextContent('NanmiCoder/OpenCutSkill')
expect(trigger).not.toHaveTextContent('main')
})
+
+ it('supports the flat workbar trigger variant without changing the selected label', () => {
+ render(
+ ,
+ )
+
+ const trigger = screen.getByRole('button')
+ expect(trigger).toHaveTextContent('project')
+ expect(trigger.className).toContain('rounded-lg')
+ expect(trigger.className).not.toContain('rounded-full')
+ })
+
+ it('renders browse entries without nesting interactive buttons', async () => {
+ vi.mocked(sessionsApi.getRecentProjects).mockResolvedValue({ projects: [] })
+ vi.mocked(filesystemApi.browse).mockResolvedValue({
+ currentPath: '/workspace',
+ parentPath: '/Users/nanmi',
+ entries: [{ name: 'project', path: '/workspace/project', isDirectory: true }],
+ })
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
+
+ render()
+
+ fireEvent.click(screen.getByRole('button', { name: /选择项目|Select a project/ }))
+ fireEvent.click(await screen.findByText(/选择其他文件夹|Choose a different folder/))
+
+ expect(await screen.findByRole('button', { name: /project/ })).toBeInTheDocument()
+ expect(errorSpy).not.toHaveBeenCalledWith(expect.stringContaining('validateDOMNesting'))
+
+ errorSpy.mockRestore()
+ })
})
diff --git a/desktop/src/components/shared/DirectoryPicker.tsx b/desktop/src/components/shared/DirectoryPicker.tsx
index 1b68bc81..e4b15fdf 100644
--- a/desktop/src/components/shared/DirectoryPicker.tsx
+++ b/desktop/src/components/shared/DirectoryPicker.tsx
@@ -7,6 +7,7 @@ import { useTranslation } from '../../i18n'
type Props = {
value: string
onChange: (path: string) => void
+ variant?: 'chip' | 'workbar'
}
type DirEntry = { name: string; path: string; isDirectory: boolean }
@@ -28,7 +29,7 @@ function projectNameFromPath(filePath: string) {
return displayRoot.split('/').filter(Boolean).pop() || filePath
}
-export function DirectoryPicker({ value, onChange }: Props) {
+export function DirectoryPicker({ value, onChange, variant = 'chip' }: Props) {
const t = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const [mode, setMode] = useState<'recent' | 'browse'>('recent')
@@ -144,6 +145,13 @@ export function DirectoryPicker({ value, onChange }: Props) {
// Find selected project info
const selectedProject = projects.find((p) => p.realPath === value)
+ const isWorkbar = variant === 'workbar'
+ const triggerClassName = isWorkbar
+ ? 'flex min-w-0 max-w-full items-center gap-2 rounded-lg px-2.5 py-2 text-sm font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35'
+ : 'flex items-center gap-2 px-3 py-1.5 bg-[var(--color-surface-container-low)] hover:bg-[var(--color-surface-hover)] rounded-full text-xs transition-colors border border-[var(--color-border)]'
+ const emptyTriggerClassName = isWorkbar
+ ? 'flex min-w-0 items-center gap-2 rounded-lg px-2.5 py-2 text-sm font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35'
+ : 'flex items-center gap-2 text-xs text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors'
return (
@@ -152,27 +160,27 @@ export function DirectoryPicker({ value, onChange }: Props) {
) : (
)}
@@ -283,16 +291,22 @@ export function DirectoryPicker({ value, onChange }: Props) {
{browseEntries.length === 0 ? (
{t('dirPicker.noSubdirs')}
) : browseEntries.map((entry) => (
-
))}
>
)}
diff --git a/desktop/src/components/shared/RepositoryLaunchControls.tsx b/desktop/src/components/shared/RepositoryLaunchControls.tsx
index 45fb8285..05c1c27c 100644
--- a/desktop/src/components/shared/RepositoryLaunchControls.tsx
+++ b/desktop/src/components/shared/RepositoryLaunchControls.tsx
@@ -199,6 +199,18 @@ export function RepositoryLaunchControls({
return null
}, [context, selectedBranch, t, useWorktree])
+ const requiresIsolation = useMemo(() => {
+ if (context?.state !== 'ok' || !selectedBranch) return false
+ if (selectedBranch.name === context.currentBranch) return false
+ return context.dirty || selectedBranch.checkedOut
+ }, [context, selectedBranch])
+
+ useEffect(() => {
+ if (requiresIsolation && !useWorktree) {
+ onUseWorktreeChange(true)
+ }
+ }, [onUseWorktreeChange, requiresIsolation, useWorktree])
+
const selectBranch = (candidate: RepositoryBranchInfo) => {
onBranchChange(candidate.name)
setBranchMenuOpen(false)
@@ -244,13 +256,15 @@ export function RepositoryLaunchControls({
onLaunchReadyChange?.(isLaunchReady)
}, [isLaunchReady, onLaunchReadyChange])
+ const workbarButtonClassName = 'inline-flex min-w-0 items-center gap-2 rounded-lg px-2.5 py-2 text-sm font-medium text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35 disabled:cursor-not-allowed disabled:opacity-50'
+
return (
-
-
+
+
{loading && workDir && (
-
+
{t('common.loading')}
@@ -258,40 +272,41 @@ export function RepositoryLaunchControls({
{isGitReady && (
<>
+
{
setBranchMenuOpen((prev) => !prev)
setBranchFilter('')
}}
- className="inline-flex max-w-full items-center gap-2 rounded-full border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-3 py-1.5 text-xs text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35 disabled:cursor-not-allowed disabled:opacity-50"
+ className={workbarButtonClassName}
>
-
-
+
+
{selectedBranch?.name || t('repoLaunch.noBranch')}
-
+
onUseWorktreeChange(!useWorktree)}
- className={`inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35 disabled:cursor-not-allowed disabled:opacity-50 ${
+ className={`${workbarButtonClassName} ${
useWorktree
- ? 'border-[var(--color-brand)]/40 bg-[var(--color-primary-fixed)] text-[var(--color-text-primary)]'
- : 'border-[var(--color-border)] bg-[var(--color-surface-container-low)] text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)]'
+ ? 'bg-[var(--color-primary-fixed)] text-[var(--color-text-primary)]'
+ : ''
}`}
>
-
-
+
+
{useWorktree ? t('repoLaunch.worktreeIsolated') : t('repoLaunch.worktreeCurrent')}
diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts
index 57d030e5..7257650a 100644
--- a/desktop/src/i18n/locales/en.ts
+++ b/desktop/src/i18n/locales/en.ts
@@ -679,9 +679,9 @@ export const en = {
'settings.general.webSearchSave': 'Save',
// ─── Empty Session ──────────────────────────────────────
- 'empty.title': 'New session',
+ 'empty.title': 'What should we build?',
'empty.subtitle': 'Start a fresh coding session. Claude is ready to help you build, debug, and architect your project.',
- 'empty.placeholder': 'Ask anything...',
+ 'empty.placeholder': 'Ask Claude anything. Type @ to mention files or tools',
'empty.addFiles': 'Add files or photos',
'empty.slashCommands': 'Slash commands',
'empty.failedToCreate': 'Failed to create session',
diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts
index 90f03b90..e0299fab 100644
--- a/desktop/src/i18n/locales/zh.ts
+++ b/desktop/src/i18n/locales/zh.ts
@@ -681,9 +681,9 @@ export const zh: Record
= {
'settings.general.webSearchSave': '保存',
// ─── Empty Session ──────────────────────────────────────
- 'empty.title': '新建会话',
+ 'empty.title': '我们该构建什么?',
'empty.subtitle': '开始一个新的编码会话。Claude 已准备好帮你构建、调试和架构你的项目。',
- 'empty.placeholder': '随便问点什么...',
+ 'empty.placeholder': '向 Claude 询问任何事。输入 @ 使用文件或工具',
'empty.addFiles': '添加文件或图片',
'empty.slashCommands': '斜杠命令',
'empty.failedToCreate': '创建会话失败',
diff --git a/desktop/src/pages/EmptySession.test.tsx b/desktop/src/pages/EmptySession.test.tsx
index 2c515703..0f35cfda 100644
--- a/desktop/src/pages/EmptySession.test.tsx
+++ b/desktop/src/pages/EmptySession.test.tsx
@@ -303,4 +303,54 @@ describe('EmptySession', () => {
})
})
})
+
+ it('defaults to isolated worktree when the fallback branch is checked out elsewhere', async () => {
+ mocks.getRepositoryContext.mockResolvedValueOnce(okRepositoryContext({
+ currentBranch: null,
+ defaultBranch: 'main',
+ branches: [
+ {
+ name: 'main',
+ current: false,
+ local: true,
+ remote: false,
+ checkedOut: true,
+ worktreePath: '/workspace/project',
+ },
+ {
+ name: 'feature/a',
+ current: false,
+ local: true,
+ remote: false,
+ checkedOut: false,
+ },
+ ],
+ worktrees: [{
+ path: '/workspace/project/.codex/worktrees/detached/project',
+ branch: null,
+ current: true,
+ }],
+ }))
+
+ render()
+
+ fireEvent.change(screen.getByRole('textbox'), {
+ target: { value: 'draft question', selectionStart: 14 },
+ })
+ fireEvent.click(screen.getByRole('button', { name: 'Pick project' }))
+
+ await waitFor(() => {
+ expect(screen.getByText('main')).toBeInTheDocument()
+ expect(screen.getByText('Isolated worktree')).toBeInTheDocument()
+ })
+
+ fireEvent.click(screen.getByRole('button', { name: /Run/i }))
+
+ await waitFor(() => {
+ expect(mocks.createSession).toHaveBeenCalledWith({
+ workDir: '/workspace/project',
+ repository: { branch: 'main', worktree: true },
+ })
+ })
+ })
})
diff --git a/desktop/src/pages/EmptySession.tsx b/desktop/src/pages/EmptySession.tsx
index 19f100a4..a41651c2 100644
--- a/desktop/src/pages/EmptySession.tsx
+++ b/desktop/src/pages/EmptySession.tsx
@@ -533,25 +533,17 @@ export function EmptySession() {
return (
-
-
-

-
+
+
+
{t('empty.title')}
-
- {t('empty.subtitle')}
-
-
-
-
-
-
-
event.preventDefault()}
- onDrop={handleDrop}
- >
+
+
event.preventDefault()}
+ onDrop={handleDrop}
+ >
{fileSearchOpen && (
handleInputChange(event.target.value, event.target.selectionStart ?? event.target.value.length)}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
- className="flex-1 resize-none border-none bg-transparent py-2 leading-relaxed text-[var(--color-text-primary)] outline-none placeholder:text-[var(--color-text-tertiary)]"
+ className="min-h-[92px] flex-1 resize-none border-none bg-transparent py-4 text-[22px] leading-relaxed text-[var(--color-text-primary)] outline-none placeholder:text-[var(--color-text-tertiary)]"
style={{ fontFamily: 'var(--font-body)' }}
placeholder={t('empty.placeholder')}
- rows={2}
+ rows={3}
/>
-
+
setPlusMenuOpen((prev) => !prev)}
aria-label="Open composer tools"
- className="rounded-lg p-1.5 text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)]"
+ className="rounded-full p-2 text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35"
>
- add
+ add
{plusMenuOpen && (
@@ -701,27 +693,29 @@ export function EmptySession() {
/>
- {t('common.run')}
- arrow_forward
+ {t('common.run')}
+ arrow_upward
-
+
-
+
+