fix: keep launch context rail on one line

Long branch names should be clipped inside the branch selector instead of forcing the repository context rail onto a second row. The rail now stays nowrap, the branch selector owns truncation, and the worktree mode selector keeps its readable fixed footprint.

Constraint: Preserve the existing composer and repository context layout shape.
Rejected: Let the rail wrap on long branch names | it creates the broken two-line state shown in the desktop composer.
Confidence: high
Scope-risk: narrow
Directive: Long repository or branch labels must truncate inside their own controls rather than changing the rail height.
Tested: cd desktop && bun run test -- --run src/pages/EmptySession.test.tsx src/components/chat/ChatInput.test.tsx src/components/shared/DirectoryPicker.test.tsx
Tested: bun run check:desktop
Tested: agent-browser selected a long branch on http://localhost:4790 and verified rail height 53px, branch width 260px, no horizontal overflow; screenshot /tmp/cc-haha-launch-rail-long-branch-nowrap.png
Not-tested: Native Tauri packaged window visual check
This commit is contained in:
程序员阿江(Relakkes) 2026-05-07 19:59:19 +08:00
parent 5675539ef0
commit 5c738db3af
2 changed files with 40 additions and 4 deletions

View File

@ -298,11 +298,11 @@ export function RepositoryLaunchControls({
}, [isLaunchReady, onLaunchReadyChange])
const worktreeLabel = useWorktree ? t('repoLaunch.worktreeIsolated') : t('repoLaunch.worktreeCurrent')
const workbarButtonClassName = 'group inline-flex h-9 max-w-full min-w-0 items-center gap-1.5 rounded-[7px] border border-transparent px-2.5 text-[13px] font-medium leading-none text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-container-lowest)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35 disabled:cursor-not-allowed disabled:opacity-50'
const workbarButtonClassName = 'group inline-flex h-9 min-w-0 items-center gap-1.5 rounded-[7px] border border-transparent px-2.5 text-[13px] font-medium leading-none text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-container-lowest)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35 disabled:cursor-not-allowed disabled:opacity-50'
return (
<div ref={rootRef} className="flex min-w-0 flex-col gap-2">
<div className="flex min-h-[48px] min-w-0 flex-wrap items-center justify-start gap-x-1.5 gap-y-1 rounded-b-xl border-t border-[var(--color-border-separator)] bg-[var(--color-surface-container-low)] px-4 py-2 shadow-[inset_0_1px_0_rgba(255,255,255,0.45)]">
<div className="flex min-h-[48px] min-w-0 flex-nowrap items-center justify-start gap-x-1.5 gap-y-1 overflow-hidden rounded-b-xl border-t border-[var(--color-border-separator)] bg-[var(--color-surface-container-low)] px-4 py-2 shadow-[inset_0_1px_0_rgba(255,255,255,0.45)]">
<DirectoryPicker value={workDir} onChange={onWorkDirChange} variant="workbar" isGitProject={isGitReady} />
{loading && workDir && (
@ -328,7 +328,7 @@ export function RepositoryLaunchControls({
setWorktreeMenuOpen(false)
setBranchFilter('')
}}
className={`${workbarButtonClassName} max-w-[260px]`}
className={`${workbarButtonClassName} max-w-[260px] shrink`}
>
<GitBranch size={17} className="shrink-0 text-[var(--color-text-tertiary)] group-hover:text-[var(--color-text-secondary)]" />
<span className="min-w-0 flex-1 truncate text-[var(--color-text-primary)]">
@ -350,7 +350,7 @@ export function RepositoryLaunchControls({
setWorktreeMenuOpen((prev) => !prev)
setBranchMenuOpen(false)
}}
className={`${workbarButtonClassName} ${
className={`${workbarButtonClassName} shrink-0 ${
useWorktree
? 'bg-[var(--color-surface-container-lowest)] text-[var(--color-text-primary)]'
: ''

View File

@ -304,6 +304,42 @@ describe('EmptySession', () => {
})
})
it('keeps the repository launch context on one row and truncates long branch names', async () => {
const longBranch = 'feature/super-long-branch-name-for-repository-launch-controls-e2e'
mocks.getRepositoryContext.mockResolvedValueOnce(okRepositoryContext({
currentBranch: longBranch,
defaultBranch: 'main',
branches: [{
name: longBranch,
current: true,
local: true,
remote: false,
checkedOut: true,
worktreePath: '/workspace/project',
}],
worktrees: [{
path: '/workspace/project',
branch: longBranch,
current: true,
}],
}))
render(<EmptySession />)
fireEvent.change(screen.getByRole('textbox'), {
target: { value: 'draft question', selectionStart: 14 },
})
fireEvent.click(screen.getByRole('button', { name: 'Pick project' }))
const branchButton = await screen.findByRole('button', { name: new RegExp(`Select branch: ${longBranch}`) })
const branchClasses = branchButton.className.split(/\s+/)
expect(branchButton.parentElement?.className).toContain('flex-nowrap')
expect(branchButton.parentElement?.className).not.toContain('flex-wrap')
expect(branchClasses).toContain('max-w-[260px]')
expect(branchClasses).not.toContain('max-w-full')
expect(branchButton.querySelector('span')?.className).toContain('truncate')
})
it('defaults to isolated worktree when the fallback branch is checked out elsewhere', async () => {
mocks.getRepositoryContext.mockResolvedValueOnce(okRepositoryContext({
currentBranch: null,