mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
fix: tighten repository launch context bar
The launch controls live directly under the composer, so they need to read as a compact context rail rather than a second card. The styling now uses a lighter surface, shorter controls, bounded flex sizing, and explicit hover titles for long repository and branch names. Git repo status is passed into the picker so a freshly opened Git project does not briefly look like a plain folder. Constraint: Keep the existing session/composer layout untouched; only refine the context bar under the input. Rejected: Redesign the empty session layout | user explicitly scoped this to the lower context bar. Confidence: high Scope-risk: narrow Directive: Do not grow this rail vertically without browser-checking long repo and branch names. Tested: cd desktop && bun run test -- --run src/components/shared/DirectoryPicker.test.tsx src/components/chat/ChatInput.test.tsx src/pages/EmptySession.test.tsx Tested: bun run check:desktop Tested: agent-browser long repo and long branch screenshots at /tmp/cc-haha-repo-controls-polished-git-long-branch-selected.png and /tmp/cc-haha-repo-controls-polished-mobile-long.png Not-tested: Native Tauri packaged window visual check
This commit is contained in:
parent
09209ecefb
commit
3606531f7e
@ -70,10 +70,42 @@ describe('DirectoryPicker', () => {
|
||||
|
||||
const trigger = screen.getByRole('button')
|
||||
expect(trigger).toHaveTextContent('project')
|
||||
expect(trigger.className).toContain('rounded-lg')
|
||||
expect(trigger.className).toContain('rounded-[7px]')
|
||||
expect(trigger.className).not.toContain('rounded-full')
|
||||
})
|
||||
|
||||
it('constrains long workbar project names without hiding the full path from hover users', () => {
|
||||
const longProjectName = 'project-with-a-very-long-directory-name-that-should-not-stretch-the-launch-bar'
|
||||
const longPath = `/workspace/${longProjectName}`
|
||||
|
||||
render(
|
||||
<DirectoryPicker
|
||||
value={longPath}
|
||||
onChange={vi.fn()}
|
||||
variant="workbar"
|
||||
/>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button')
|
||||
const label = screen.getByText(longProjectName)
|
||||
expect(trigger).toHaveAttribute('title', longPath)
|
||||
expect(trigger.className).toContain('w-full')
|
||||
expect(label.className).toContain('truncate')
|
||||
})
|
||||
|
||||
it('can show a Git icon for workbar projects before the recent-project cache is loaded', () => {
|
||||
render(
|
||||
<DirectoryPicker
|
||||
value="/workspace/project"
|
||||
onChange={vi.fn()}
|
||||
variant="workbar"
|
||||
isGitProject
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button').querySelector('svg')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders browse entries without nesting interactive buttons', async () => {
|
||||
vi.mocked(sessionsApi.getRecentProjects).mockResolvedValue({ projects: [] })
|
||||
vi.mocked(filesystemApi.browse).mockResolvedValue({
|
||||
|
||||
@ -8,6 +8,7 @@ type Props = {
|
||||
value: string
|
||||
onChange: (path: string) => void
|
||||
variant?: 'chip' | 'workbar'
|
||||
isGitProject?: boolean
|
||||
}
|
||||
|
||||
type DirEntry = { name: string; path: string; isDirectory: boolean }
|
||||
@ -29,7 +30,7 @@ function projectNameFromPath(filePath: string) {
|
||||
return displayRoot.split('/').filter(Boolean).pop() || filePath
|
||||
}
|
||||
|
||||
export function DirectoryPicker({ value, onChange, variant = 'chip' }: Props) {
|
||||
export function DirectoryPicker({ value, onChange, variant = 'chip', isGitProject = false }: Props) {
|
||||
const t = useTranslation()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [mode, setMode] = useState<'recent' | 'browse'>('recent')
|
||||
@ -146,42 +147,46 @@ export function DirectoryPicker({ value, onChange, variant = 'chip' }: Props) {
|
||||
// Find selected project info
|
||||
const selectedProject = projects.find((p) => p.realPath === value)
|
||||
const isWorkbar = variant === 'workbar'
|
||||
const selectedLabel = selectedProject?.repoName || selectedProject?.projectName || projectNameFromPath(value)
|
||||
const showGitIcon = selectedProject?.isGit || isGitProject
|
||||
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 h-9 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:border-[var(--color-border)] hover:bg-[var(--color-surface-container-lowest)] 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 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:border-[var(--color-border)] hover:bg-[var(--color-surface-container-lowest)] 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 (
|
||||
<div ref={ref} className="relative">
|
||||
<div ref={ref} className={isWorkbar ? 'relative min-w-0 flex-[1_1_220px] max-w-[360px]' : 'relative'}>
|
||||
{/* Trigger — shows selected project chip or placeholder */}
|
||||
{value ? (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
onClick={() => { setIsOpen(!isOpen); setMode('recent') }}
|
||||
className={triggerClassName}
|
||||
title={value}
|
||||
>
|
||||
{selectedProject?.isGit ? (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" className="text-[var(--color-text-secondary)]">
|
||||
{showGitIcon ? (
|
||||
<svg width="15" height="15" viewBox="0 0 16 16" fill="currentColor" className="shrink-0 text-[var(--color-text-secondary)]">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
|
||||
</svg>
|
||||
) : (
|
||||
<span className={`material-symbols-outlined ${isWorkbar ? 'text-[18px]' : 'text-[14px]'} text-[var(--color-text-secondary)]`}>folder</span>
|
||||
<span className={`material-symbols-outlined shrink-0 ${isWorkbar ? 'text-[17px]' : 'text-[14px]'} text-[var(--color-text-secondary)]`}>folder</span>
|
||||
)}
|
||||
<span className="min-w-0 truncate text-[var(--color-text-primary)]">
|
||||
{selectedProject?.repoName || selectedProject?.projectName || projectNameFromPath(value)}
|
||||
<span className="min-w-0 flex-1 truncate text-[var(--color-text-primary)]">
|
||||
{selectedLabel}
|
||||
</span>
|
||||
<span className={`${isWorkbar ? 'text-[16px]' : 'text-[12px]'} material-symbols-outlined text-[var(--color-text-tertiary)]`}>expand_more</span>
|
||||
<span className={`${isWorkbar ? 'text-[15px]' : 'text-[12px]'} material-symbols-outlined shrink-0 text-[var(--color-text-tertiary)]`}>expand_more</span>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
onClick={() => { setIsOpen(!isOpen); setMode('recent') }}
|
||||
className={emptyTriggerClassName}
|
||||
title={t('dirPicker.selectProject')}
|
||||
>
|
||||
<span className={`material-symbols-outlined ${isWorkbar ? 'text-[18px]' : 'text-[14px]'}`}>folder_open</span>
|
||||
{t('dirPicker.selectProject')}
|
||||
<span className={`material-symbols-outlined shrink-0 ${isWorkbar ? 'text-[17px]' : 'text-[14px]'}`}>folder_open</span>
|
||||
<span className="min-w-0 truncate">{t('dirPicker.selectProject')}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
@ -256,15 +256,16 @@ 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'
|
||||
const worktreeLabel = useWorktree ? t('repoLaunch.worktreeIsolated') : t('repoLaunch.worktreeCurrent')
|
||||
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:border-[var(--color-border)] 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-[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" />
|
||||
<div className="flex min-h-[48px] min-w-0 flex-wrap items-center gap-x-2 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)]">
|
||||
<DirectoryPicker value={workDir} onChange={onWorkDirChange} variant="workbar" isGitProject={isGitReady} />
|
||||
|
||||
{loading && workDir && (
|
||||
<div className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-2 text-sm text-[var(--color-text-secondary)]">
|
||||
<div className="inline-flex h-9 items-center gap-1.5 rounded-[7px] px-2.5 text-[13px] text-[var(--color-text-secondary)]">
|
||||
<Loader2 size={14} className="shrink-0 animate-spin" />
|
||||
<span>{t('common.loading')}</span>
|
||||
</div>
|
||||
@ -272,7 +273,7 @@ export function RepositoryLaunchControls({
|
||||
|
||||
{isGitReady && (
|
||||
<>
|
||||
<span className="h-5 w-px bg-[var(--color-border-separator)]" aria-hidden="true" />
|
||||
<span className="hidden h-5 w-px shrink-0 bg-[var(--color-border-separator)] sm:block" aria-hidden="true" />
|
||||
<button
|
||||
ref={branchButtonRef}
|
||||
type="button"
|
||||
@ -280,14 +281,15 @@ export function RepositoryLaunchControls({
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={branchMenuOpen}
|
||||
aria-label={`${t('repoLaunch.selectBranch')}: ${selectedBranch?.name || t('repoLaunch.noBranch')}`}
|
||||
title={selectedBranch?.name || t('repoLaunch.noBranch')}
|
||||
onClick={() => {
|
||||
setBranchMenuOpen((prev) => !prev)
|
||||
setBranchFilter('')
|
||||
}}
|
||||
className={workbarButtonClassName}
|
||||
className={`${workbarButtonClassName} flex-[0_1_260px]`}
|
||||
>
|
||||
<GitBranch size={18} className="shrink-0" />
|
||||
<span className="min-w-0 max-w-[220px] truncate text-[var(--color-text-primary)]">
|
||||
<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)]">
|
||||
{selectedBranch?.name || t('repoLaunch.noBranch')}
|
||||
</span>
|
||||
<ChevronDown size={16} className="shrink-0 text-[var(--color-text-tertiary)]" />
|
||||
@ -298,16 +300,17 @@ export function RepositoryLaunchControls({
|
||||
disabled={disabled || requiresIsolation}
|
||||
aria-pressed={useWorktree}
|
||||
aria-label={t('repoLaunch.toggleWorktree')}
|
||||
title={worktreeLabel}
|
||||
onClick={() => onUseWorktreeChange(!useWorktree)}
|
||||
className={`${workbarButtonClassName} ${
|
||||
useWorktree
|
||||
? 'bg-[var(--color-primary-fixed)] text-[var(--color-text-primary)]'
|
||||
? 'border-[var(--color-border-focus)] bg-[var(--color-surface-container-lowest)] text-[var(--color-text-primary)] shadow-[inset_0_0_0_1px_var(--color-border-focus)]'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<GitFork size={18} className="shrink-0" />
|
||||
<GitFork size={17} className="shrink-0 text-[var(--color-text-tertiary)]" />
|
||||
<span className="min-w-0 truncate">
|
||||
{useWorktree ? t('repoLaunch.worktreeIsolated') : t('repoLaunch.worktreeCurrent')}
|
||||
{worktreeLabel}
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user