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

View File

@ -260,7 +260,7 @@ export function RepositoryLaunchControls({
return ( return (
<div ref={rootRef} className="flex min-w-0 flex-col gap-2"> <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" /> <DirectoryPicker value={workDir} onChange={onWorkDirChange} variant="workbar" />
{loading && workDir && ( {loading && workDir && (

View File

@ -679,9 +679,9 @@ export const en = {
'settings.general.webSearchSave': 'Save', 'settings.general.webSearchSave': 'Save',
// ─── Empty Session ────────────────────────────────────── // ─── 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.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.addFiles': 'Add files or photos',
'empty.slashCommands': 'Slash commands', 'empty.slashCommands': 'Slash commands',
'empty.failedToCreate': 'Failed to create session', 'empty.failedToCreate': 'Failed to create session',

View File

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

View File

@ -533,17 +533,25 @@ export function EmptySession() {
return ( return (
<div className="relative flex flex-1 flex-col overflow-hidden bg-[var(--color-surface)]"> <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 flex-1 flex-col items-center justify-center p-8 pb-32">
<div className="flex w-full max-w-[1120px] -translate-y-6 flex-col items-center gap-12"> <div className="flex max-w-md flex-col items-center text-center">
<h1 className="text-center text-[44px] font-semibold leading-tight text-[var(--color-text-primary)]" style={{ fontFamily: 'var(--font-headline)' }}> <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')} {t('empty.title')}
</h1> </h1>
<div className="flex w-full max-w-[1040px] flex-col"> <p className="mx-auto max-w-xs text-[var(--color-text-secondary)]" style={{ fontFamily: 'var(--font-body)' }}>
<div {t('empty.subtitle')}
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)]" </p>
onDragOver={(event) => event.preventDefault()} </div>
onDrop={handleDrop} </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 && ( {fileSearchOpen && (
<FileSearchMenu <FileSearchMenu
ref={fileSearchRef} ref={fileSearchRef}
@ -639,22 +647,22 @@ export function EmptySession() {
onChange={(event) => handleInputChange(event.target.value, event.target.selectionStart ?? event.target.value.length)} onChange={(event) => handleInputChange(event.target.value, event.target.selectionStart ?? event.target.value.length)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onPaste={handlePaste} 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)' }} style={{ fontFamily: 'var(--font-body)' }}
placeholder={t('empty.placeholder')} placeholder={t('empty.placeholder')}
rows={3} rows={2}
/> />
</div> </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 className="flex items-center gap-2">
<div ref={plusMenuRef} className="relative"> <div ref={plusMenuRef} className="relative">
<button <button
onClick={() => setPlusMenuOpen((prev) => !prev)} onClick={() => setPlusMenuOpen((prev) => !prev)}
aria-label="Open composer tools" 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> </button>
{plusMenuOpen && ( {plusMenuOpen && (
@ -693,29 +701,27 @@ export function EmptySession() {
/> />
<ModelSelector runtimeKey={DRAFT_RUNTIME_SELECTION_KEY} disabled={isSubmitting} /> <ModelSelector runtimeKey={DRAFT_RUNTIME_SELECTION_KEY} disabled={isSubmitting} />
<button <button
aria-label={t('common.run')}
onClick={handleSubmit} onClick={handleSubmit}
disabled={!canSubmit} 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> {t('common.run')}
<span className="material-symbols-outlined text-[28px]">arrow_upward</span> <span className="material-symbols-outlined text-[14px]">arrow_forward</span>
</button> </button>
</div> </div>
</div> </div>
</div>
<RepositoryLaunchControls
workDir={workDir}
onWorkDirChange={handleWorkDirChange}
branch={selectedBranch}
onBranchChange={setSelectedBranch}
useWorktree={useWorktree}
onUseWorktreeChange={setUseWorktree}
onLaunchReadyChange={setRepositoryLaunchReady}
disabled={isSubmitting}
/>
</div> </div>
<RepositoryLaunchControls
workDir={workDir}
onWorkDirChange={handleWorkDirChange}
branch={selectedBranch}
onBranchChange={setSelectedBranch}
useWorktree={useWorktree}
onUseWorktreeChange={setUseWorktree}
onLaunchReadyChange={setRepositoryLaunchReady}
disabled={isSubmitting}
/>
</div> </div>
</div> </div>