fix: restore empty session launcher layout

The repository launch controls should extend the existing empty-session composer, not replace the session layout. This restores the previous mascot, title, composer sizing, textarea, and run button while keeping the project, branch, and worktree controls attached directly beneath the input.

Constraint: User requested the original session layout stay intact except for adding repository controls under the chat input
Rejected: Keep the Codex-style centered launcher rewrite | it changed too much of the existing session surface
Confidence: high
Scope-risk: narrow
Directive: Future changes to repository launch controls should treat EmptySession layout as fixed unless explicitly requested otherwise
Tested: cd desktop && bun run test -- --run src/components/shared/DirectoryPicker.test.tsx src/pages/EmptySession.test.tsx src/__tests__/pages.test.tsx
Tested: cd desktop && bun run lint
Tested: bun run check:desktop
Tested: agent-browser screenshot /tmp/cc-haha-layout-restored-selected.png on localhost:4790
Not-tested: root bun run verify not rerun for this narrow layout correction
This commit is contained in:
程序员阿江(Relakkes) 2026-05-07 17:56:33 +08:00
parent f3dc2399c2
commit 0f220f4af5
5 changed files with 46 additions and 40 deletions

View File

@ -159,7 +159,7 @@ describe('Content-only pages render without errors', () => {
expect(screen.queryByText('/internal-only')).not.toBeInTheDocument()
})
it('EmptySession renders the launch composer', async () => {
it('EmptySession renders mascot and composer', async () => {
let container!: HTMLElement
await act(async () => {
container = render(<EmptySession />).container
@ -167,8 +167,8 @@ describe('Content-only pages render without errors', () => {
await Promise.resolve()
})
expect(container.querySelector('textarea')).toBeInTheDocument()
expect(container.innerHTML).toContain('What should we build?')
expect(container.innerHTML).toContain('Ask Claude anything')
expect(container.innerHTML).toContain('New session')
expect(container.innerHTML).toContain('Ask 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(<ActiveSession />)
// With empty messages, the hero is shown
expect(container.innerHTML).toContain('What should we build?')
expect(container.innerHTML).toContain('New session')
// ChatInput has a textarea
const textarea = container.querySelector('textarea')
expect(textarea).toBeInTheDocument()
expect(textarea).toHaveAttribute('placeholder', 'Ask Claude anything. Type @ to mention files or tools')
expect(textarea).toHaveAttribute('placeholder', 'Ask anything...')
expect(textarea).toHaveAttribute('rows', '2')
expect(container.innerHTML).not.toContain('Preview')
// Cleanup

View File

@ -260,7 +260,7 @@ export function RepositoryLaunchControls({
return (
<div ref={rootRef} className="flex min-w-0 flex-col gap-2">
<div className="flex min-h-[58px] min-w-0 flex-wrap items-center gap-x-3 gap-y-1 rounded-b-[28px] bg-[var(--color-surface-container)] px-5 py-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.65)]">
<div className="flex min-h-[58px] min-w-0 flex-wrap items-center gap-x-3 gap-y-1 rounded-b-xl bg-[var(--color-surface-container)] px-5 py-3 shadow-[inset_0_1px_0_rgba(255,255,255,0.65)]">
<DirectoryPicker value={workDir} onChange={onWorkDirChange} variant="workbar" />
{loading && workDir && (

View File

@ -679,9 +679,9 @@ export const en = {
'settings.general.webSearchSave': 'Save',
// ─── Empty Session ──────────────────────────────────────
'empty.title': 'What should we build?',
'empty.title': 'New session',
'empty.subtitle': 'Start a fresh coding session. Claude is ready to help you build, debug, and architect your project.',
'empty.placeholder': 'Ask Claude anything. Type @ to mention files or tools',
'empty.placeholder': 'Ask anything...',
'empty.addFiles': 'Add files or photos',
'empty.slashCommands': 'Slash commands',
'empty.failedToCreate': 'Failed to create session',

View File

@ -681,9 +681,9 @@ export const zh: Record<TranslationKey, string> = {
'settings.general.webSearchSave': '保存',
// ─── Empty Session ──────────────────────────────────────
'empty.title': '我们该构建什么?',
'empty.title': '新建会话',
'empty.subtitle': '开始一个新的编码会话。Claude 已准备好帮你构建、调试和架构你的项目。',
'empty.placeholder': '向 Claude 询问任何事。输入 @ 使用文件或工具',
'empty.placeholder': '随便问点什么...',
'empty.addFiles': '添加文件或图片',
'empty.slashCommands': '斜杠命令',
'empty.failedToCreate': '创建会话失败',

View File

@ -533,17 +533,25 @@ export function EmptySession() {
return (
<div className="relative flex flex-1 flex-col overflow-hidden bg-[var(--color-surface)]">
<div className="flex min-h-full flex-1 items-center justify-center px-8 py-12">
<div className="flex w-full max-w-[1120px] -translate-y-6 flex-col items-center gap-12">
<h1 className="text-center text-[44px] font-semibold leading-tight text-[var(--color-text-primary)]" style={{ fontFamily: 'var(--font-headline)' }}>
<div className="flex flex-1 flex-col items-center justify-center p-8 pb-32">
<div className="flex max-w-md flex-col items-center text-center">
<img src="/app-icon.png" alt="Claude Code Haha" className="mb-6 h-24 w-24" />
<h1 className="mb-2 text-3xl font-extrabold tracking-tight text-[var(--color-text-primary)]" style={{ fontFamily: 'var(--font-headline)' }}>
{t('empty.title')}
</h1>
<div className="flex w-full max-w-[1040px] flex-col">
<div
className="relative flex min-h-[194px] flex-col gap-4 rounded-t-[28px] border border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] px-6 pb-4 pt-5 shadow-[0_22px_70px_rgba(15,23,42,0.10),0_5px_18px_rgba(15,23,42,0.06)]"
onDragOver={(event) => event.preventDefault()}
onDrop={handleDrop}
>
<p className="mx-auto max-w-xs text-[var(--color-text-secondary)]" style={{ fontFamily: 'var(--font-body)' }}>
{t('empty.subtitle')}
</p>
</div>
</div>
<div className="absolute bottom-4 left-0 right-0 flex justify-center px-8">
<div className="flex w-full max-w-3xl flex-col">
<div
className="glass-panel relative flex flex-col gap-3 rounded-t-xl rounded-b-none p-4"
onDragOver={(event) => event.preventDefault()}
onDrop={handleDrop}
>
{fileSearchOpen && (
<FileSearchMenu
ref={fileSearchRef}
@ -639,22 +647,22 @@ export function EmptySession() {
onChange={(event) => handleInputChange(event.target.value, event.target.selectionStart ?? event.target.value.length)}
onKeyDown={handleKeyDown}
onPaste={handlePaste}
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)]"
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)]"
style={{ fontFamily: 'var(--font-body)' }}
placeholder={t('empty.placeholder')}
rows={3}
rows={2}
/>
</div>
<div className="flex items-center justify-between border-t border-[var(--color-border-separator)] pt-4">
<div className="flex items-center justify-between border-t border-[var(--color-border-separator)] pt-3">
<div className="flex items-center gap-2">
<div ref={plusMenuRef} className="relative">
<button
onClick={() => setPlusMenuOpen((prev) => !prev)}
aria-label="Open composer tools"
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"
className="rounded-lg p-1.5 text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)]"
>
<span className="material-symbols-outlined text-[28px]">add</span>
<span className="material-symbols-outlined text-[18px]">add</span>
</button>
{plusMenuOpen && (
@ -693,29 +701,27 @@ export function EmptySession() {
/>
<ModelSelector runtimeKey={DRAFT_RUNTIME_SELECTION_KEY} disabled={isSubmitting} />
<button
aria-label={t('common.run')}
onClick={handleSubmit}
disabled={!canSubmit}
className="flex h-14 w-14 items-center justify-center rounded-full bg-[var(--color-text-secondary)] text-[var(--color-surface-container-lowest)] shadow-[0_10px_24px_rgba(15,23,42,0.18)] transition-all hover:bg-[var(--color-text-primary)] disabled:bg-[var(--color-text-tertiary)] disabled:opacity-45"
className="flex w-[112px] items-center justify-center gap-1 rounded-lg bg-[image:var(--gradient-btn-primary)] px-3 py-1.5 text-xs font-semibold text-[var(--color-btn-primary-fg)] shadow-[var(--shadow-button-primary)] transition-all hover:brightness-105 disabled:opacity-30"
>
<span className="sr-only">{t('common.run')}</span>
<span className="material-symbols-outlined text-[28px]">arrow_upward</span>
{t('common.run')}
<span className="material-symbols-outlined text-[14px]">arrow_forward</span>
</button>
</div>
</div>
</div>
<RepositoryLaunchControls
workDir={workDir}
onWorkDirChange={handleWorkDirChange}
branch={selectedBranch}
onBranchChange={setSelectedBranch}
useWorktree={useWorktree}
onUseWorktreeChange={setUseWorktree}
onLaunchReadyChange={setRepositoryLaunchReady}
disabled={isSubmitting}
/>
</div>
<RepositoryLaunchControls
workDir={workDir}
onWorkDirChange={handleWorkDirChange}
branch={selectedBranch}
onBranchChange={setSelectedBranch}
useWorktree={useWorktree}
onUseWorktreeChange={setUseWorktree}
onLaunchReadyChange={setRepositoryLaunchReady}
disabled={isSubmitting}
/>
</div>
</div>