fix: keep empty-session launch controls visually attached

The active empty-session composer used the same launch controls as the new-session surface but rendered them outside the glass panel, creating an inconsistent gap between the prompt box and project/branch/worktree selectors. Embed the desktop hero launch row in the composer panel and keep the existing mobile and post-message context-chip paths unchanged.

Constraint: Preserve the existing new-session composer layout and shared RepositoryLaunchControls behavior.
Rejected: Adjust outer margins only | would keep the active empty-session DOM structurally different from the already-correct new-session composer.
Confidence: high
Scope-risk: narrow
Tested: cd desktop && bun run test -- src/components/chat/ChatInput.test.tsx src/pages/EmptySession.test.tsx
Tested: cd desktop && bun run lint
Tested: git diff --check
Tested: agent-browser DOM check confirmed the launch row is inside chat-input-panel with 1px bottom gap
Not-tested: Full bun run verify gate
This commit is contained in:
程序员阿江(Relakkes) 2026-05-14 23:02:15 +08:00
parent c39a8331c7
commit a9bd96f8b0
2 changed files with 26 additions and 2 deletions

View File

@ -320,9 +320,16 @@ describe('ChatInput file mentions', () => {
render(<ChatInput variant="hero" />)
const panel = screen.getByTestId('chat-input-panel')
expect(panel).toHaveClass('rounded-xl')
expect(panel).not.toHaveClass('rounded-b-none')
expect(await screen.findByRole('button', { name: /Select branch: main/ })).toBeInTheDocument()
expect(screen.getByText('Current worktree')).toBeInTheDocument()
expect(screen.queryByText('Select a project...')).not.toBeInTheDocument()
const branchButton = screen.getByRole('button', { name: /Select branch: main/ })
expect(panel).toContainElement(branchButton.parentElement)
expect(branchButton.parentElement).toHaveClass('bg-transparent')
})
it('uses the persisted message count to keep reopened sessions in context mode while history loads', async () => {

View File

@ -151,6 +151,7 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
const useCompactControls = compact || isMobileComposer
const iconOnlyAction = compact || isMobileComposer
const activeLaunchWorkDir = showLaunchControls ? (launchWorkDir || resolvedWorkDir || '') : (resolvedWorkDir || '')
const embedLaunchControlsInHero = isHeroComposer && !useCompactControls && showLaunchControls
const pendingSlashUiAction = !isMemberSession && input.trim().startsWith('/')
? resolveSlashUiAction(input.trim().slice(1))
: null
@ -802,7 +803,7 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
<div
data-testid="chat-input-panel"
className={isHeroComposer
? 'glass-panel relative flex flex-col gap-3 rounded-t-xl rounded-b-none p-4 transition-colors'
? `glass-panel relative flex flex-col gap-3 ${embedLaunchControlsInHero ? 'rounded-xl' : 'rounded-t-xl rounded-b-none'} p-4 transition-colors`
: compact
? `glass-panel relative p-3 transition-colors ${isMobileComposer ? 'rounded-2xl shadow-[0_-12px_36px_rgba(54,35,28,0.12)]' : 'rounded-xl'}`
: `glass-panel relative transition-colors ${isMobileComposer ? 'rounded-2xl p-3 shadow-[0_-12px_36px_rgba(54,35,28,0.12)]' : 'rounded-xl p-4'}`}
@ -1049,11 +1050,27 @@ export function ChatInput({ variant = 'default', compact = false }: ChatInputPro
</button>
</div>
</div>
{embedLaunchControlsInHero && (
<div className="-mx-4 -mb-4 mt-3">
<RepositoryLaunchControls
workDir={activeLaunchWorkDir}
onWorkDirChange={handleLaunchWorkDirChange}
branch={launchBranch}
onBranchChange={setLaunchBranch}
useWorktree={launchUseWorktree}
onUseWorktreeChange={setLaunchUseWorktree}
onLaunchReadyChange={setLaunchReady}
disabled={isActive || launchTransitioning}
placement="composer"
/>
</div>
)}
</div>
<input ref={fileInputRef} type="file" multiple className="hidden" onChange={handleFileSelect} />
{!isMemberSession && (
{!isMemberSession && !embedLaunchControlsInHero && (
<div className={useCompactControls ? 'mt-2 flex min-w-0 px-1' : 'mt-3 px-1'}>
{messageCount > 0 ? (
<ProjectContextChip