mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
fix: polish desktop launch repository controls
The empty-session launch surface needed to behave like a real coding-agent entry point: project, branch, and worktree choices are now visible together, and branch choices that cannot safely run in-place force an isolated worktree before session creation. Constraint: Desktop launch flows must preserve repository safety when a target branch is dirty or checked out elsewhere Rejected: Keep branch and worktree controls as detached chips | the selection state was easy to miss and visually inconsistent with the launcher Confidence: high Scope-risk: moderate Directive: Do not remove the automatic worktree requirement without rechecking dirty and checked-out branch launches through the desktop UI Tested: cd desktop && bun run test -- --run src/components/shared/DirectoryPicker.test.tsx src/pages/EmptySession.test.tsx Tested: cd desktop && bun run lint Tested: bun run check:desktop Tested: browser-driven desktop smoke on ports 4789/4790 Not-tested: bun run verify still has the pre-existing agent-utils coverage ratchet failure outside desktop
This commit is contained in:
parent
8b07c16f45
commit
f3dc2399c2
@ -159,7 +159,7 @@ describe('Content-only pages render without errors', () => {
|
||||
expect(screen.queryByText('/internal-only')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('EmptySession renders mascot and composer', async () => {
|
||||
it('EmptySession renders the launch 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('New session')
|
||||
expect(container.innerHTML).toContain('Ask anything')
|
||||
expect(container.innerHTML).toContain('What should we build?')
|
||||
expect(container.innerHTML).toContain('Ask Claude 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('New session')
|
||||
expect(container.innerHTML).toContain('What should we build?')
|
||||
// ChatInput has a textarea
|
||||
const textarea = container.querySelector('textarea')
|
||||
expect(textarea).toBeInTheDocument()
|
||||
expect(textarea).toHaveAttribute('placeholder', 'Ask anything...')
|
||||
expect(textarea).toHaveAttribute('placeholder', 'Ask Claude anything. Type @ to mention files or tools')
|
||||
expect(textarea).toHaveAttribute('rows', '2')
|
||||
expect(container.innerHTML).not.toContain('Preview')
|
||||
// Cleanup
|
||||
|
||||
@ -16,6 +16,7 @@ vi.mock('../../api/filesystem', () => ({
|
||||
|
||||
import { DirectoryPicker } from './DirectoryPicker'
|
||||
import { sessionsApi } from '../../api/sessions'
|
||||
import { filesystemApi } from '../../api/filesystem'
|
||||
|
||||
describe('DirectoryPicker', () => {
|
||||
it('uses the source repository name as the fallback label for desktop worktree paths', () => {
|
||||
@ -57,4 +58,39 @@ describe('DirectoryPicker', () => {
|
||||
expect(trigger).toHaveTextContent('NanmiCoder/OpenCutSkill')
|
||||
expect(trigger).not.toHaveTextContent('main')
|
||||
})
|
||||
|
||||
it('supports the flat workbar trigger variant without changing the selected label', () => {
|
||||
render(
|
||||
<DirectoryPicker
|
||||
value="/workspace/project"
|
||||
onChange={vi.fn()}
|
||||
variant="workbar"
|
||||
/>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button')
|
||||
expect(trigger).toHaveTextContent('project')
|
||||
expect(trigger.className).toContain('rounded-lg')
|
||||
expect(trigger.className).not.toContain('rounded-full')
|
||||
})
|
||||
|
||||
it('renders browse entries without nesting interactive buttons', async () => {
|
||||
vi.mocked(sessionsApi.getRecentProjects).mockResolvedValue({ projects: [] })
|
||||
vi.mocked(filesystemApi.browse).mockResolvedValue({
|
||||
currentPath: '/workspace',
|
||||
parentPath: '/Users/nanmi',
|
||||
entries: [{ name: 'project', path: '/workspace/project', isDirectory: true }],
|
||||
})
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
render(<DirectoryPicker value="" onChange={vi.fn()} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /选择项目|Select a project/ }))
|
||||
fireEvent.click(await screen.findByText(/选择其他文件夹|Choose a different folder/))
|
||||
|
||||
expect(await screen.findByRole('button', { name: /project/ })).toBeInTheDocument()
|
||||
expect(errorSpy).not.toHaveBeenCalledWith(expect.stringContaining('validateDOMNesting'))
|
||||
|
||||
errorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
@ -7,6 +7,7 @@ import { useTranslation } from '../../i18n'
|
||||
type Props = {
|
||||
value: string
|
||||
onChange: (path: string) => void
|
||||
variant?: 'chip' | 'workbar'
|
||||
}
|
||||
|
||||
type DirEntry = { name: string; path: string; isDirectory: boolean }
|
||||
@ -28,7 +29,7 @@ function projectNameFromPath(filePath: string) {
|
||||
return displayRoot.split('/').filter(Boolean).pop() || filePath
|
||||
}
|
||||
|
||||
export function DirectoryPicker({ value, onChange }: Props) {
|
||||
export function DirectoryPicker({ value, onChange, variant = 'chip' }: Props) {
|
||||
const t = useTranslation()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [mode, setMode] = useState<'recent' | 'browse'>('recent')
|
||||
@ -144,6 +145,13 @@ export function DirectoryPicker({ value, onChange }: Props) {
|
||||
|
||||
// Find selected project info
|
||||
const selectedProject = projects.find((p) => p.realPath === value)
|
||||
const isWorkbar = variant === 'workbar'
|
||||
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 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 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">
|
||||
@ -152,27 +160,27 @@ export function DirectoryPicker({ value, onChange }: Props) {
|
||||
<button
|
||||
ref={triggerRef}
|
||||
onClick={() => { setIsOpen(!isOpen); setMode('recent') }}
|
||||
className="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)]"
|
||||
className={triggerClassName}
|
||||
>
|
||||
{selectedProject?.isGit ? (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" className="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 text-[14px] text-[var(--color-text-secondary)]">folder</span>
|
||||
<span className={`material-symbols-outlined ${isWorkbar ? 'text-[18px]' : 'text-[14px]'} text-[var(--color-text-secondary)]`}>folder</span>
|
||||
)}
|
||||
<span className="font-medium text-[var(--color-text-primary)]">
|
||||
<span className="min-w-0 truncate text-[var(--color-text-primary)]">
|
||||
{selectedProject?.repoName || selectedProject?.projectName || projectNameFromPath(value)}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-[12px] text-[var(--color-text-tertiary)]">expand_more</span>
|
||||
<span className={`${isWorkbar ? 'text-[16px]' : 'text-[12px]'} material-symbols-outlined text-[var(--color-text-tertiary)]`}>expand_more</span>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
onClick={() => { setIsOpen(!isOpen); setMode('recent') }}
|
||||
className="flex items-center gap-2 text-xs text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] transition-colors"
|
||||
className={emptyTriggerClassName}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">folder_open</span>
|
||||
<span className={`material-symbols-outlined ${isWorkbar ? 'text-[18px]' : 'text-[14px]'}`}>folder_open</span>
|
||||
{t('dirPicker.selectProject')}
|
||||
</button>
|
||||
)}
|
||||
@ -283,16 +291,22 @@ export function DirectoryPicker({ value, onChange }: Props) {
|
||||
{browseEntries.length === 0 ? (
|
||||
<div className="px-3 py-4 text-center text-xs text-[var(--color-text-tertiary)]">{t('dirPicker.noSubdirs')}</div>
|
||||
) : browseEntries.map((entry) => (
|
||||
<button
|
||||
<div
|
||||
key={entry.path}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-[var(--color-surface-hover)]"
|
||||
className="flex w-full items-center gap-2 px-3 py-2 hover:bg-[var(--color-surface-hover)]"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px] text-[var(--color-text-tertiary)]" onClick={() => loadBrowseDir(entry.path)}>folder</span>
|
||||
<span className="text-xs text-[var(--color-text-primary)] flex-1" onClick={() => loadBrowseDir(entry.path)}>{entry.name}</span>
|
||||
<button onClick={() => handleSelect(entry.path)} className="px-2 py-0.5 text-[10px] font-semibold text-[var(--color-brand)] hover:bg-[var(--color-primary-fixed)] rounded transition-colors">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => loadBrowseDir(entry.path)}
|
||||
className="flex min-w-0 flex-1 items-center gap-2 text-left"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px] text-[var(--color-text-tertiary)]">folder</span>
|
||||
<span className="min-w-0 flex-1 truncate text-xs text-[var(--color-text-primary)]">{entry.name}</span>
|
||||
</button>
|
||||
<button type="button" onClick={() => handleSelect(entry.path)} className="rounded px-2 py-0.5 text-[10px] font-semibold text-[var(--color-brand)] transition-colors hover:bg-[var(--color-primary-fixed)]">
|
||||
{t('common.select')}
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -199,6 +199,18 @@ export function RepositoryLaunchControls({
|
||||
return null
|
||||
}, [context, selectedBranch, t, useWorktree])
|
||||
|
||||
const requiresIsolation = useMemo(() => {
|
||||
if (context?.state !== 'ok' || !selectedBranch) return false
|
||||
if (selectedBranch.name === context.currentBranch) return false
|
||||
return context.dirty || selectedBranch.checkedOut
|
||||
}, [context, selectedBranch])
|
||||
|
||||
useEffect(() => {
|
||||
if (requiresIsolation && !useWorktree) {
|
||||
onUseWorktreeChange(true)
|
||||
}
|
||||
}, [onUseWorktreeChange, requiresIsolation, useWorktree])
|
||||
|
||||
const selectBranch = (candidate: RepositoryBranchInfo) => {
|
||||
onBranchChange(candidate.name)
|
||||
setBranchMenuOpen(false)
|
||||
@ -244,13 +256,15 @@ 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'
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className="flex min-w-0 flex-col gap-2">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<DirectoryPicker value={workDir} onChange={onWorkDirChange} />
|
||||
<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)]">
|
||||
<DirectoryPicker value={workDir} onChange={onWorkDirChange} variant="workbar" />
|
||||
|
||||
{loading && workDir && (
|
||||
<div className="inline-flex items-center gap-1.5 rounded-full border border-[var(--color-border)] bg-[var(--color-surface-container-lowest)] px-3 py-1.5 text-xs text-[var(--color-text-secondary)]">
|
||||
<div className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-2 text-sm text-[var(--color-text-secondary)]">
|
||||
<Loader2 size={14} className="shrink-0 animate-spin" />
|
||||
<span>{t('common.loading')}</span>
|
||||
</div>
|
||||
@ -258,40 +272,41 @@ export function RepositoryLaunchControls({
|
||||
|
||||
{isGitReady && (
|
||||
<>
|
||||
<span className="h-5 w-px bg-[var(--color-border-separator)]" aria-hidden="true" />
|
||||
<button
|
||||
ref={branchButtonRef}
|
||||
type="button"
|
||||
disabled={disabled || loading || context.branches.length === 0}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={branchMenuOpen}
|
||||
aria-label={t('repoLaunch.selectBranch')}
|
||||
aria-label={`${t('repoLaunch.selectBranch')}: ${selectedBranch?.name || t('repoLaunch.noBranch')}`}
|
||||
onClick={() => {
|
||||
setBranchMenuOpen((prev) => !prev)
|
||||
setBranchFilter('')
|
||||
}}
|
||||
className="inline-flex max-w-full items-center gap-2 rounded-full border border-[var(--color-border)] bg-[var(--color-surface-container-low)] px-3 py-1.5 text-xs 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"
|
||||
className={workbarButtonClassName}
|
||||
>
|
||||
<GitBranch size={15} className="shrink-0" />
|
||||
<span className="truncate font-medium text-[var(--color-text-primary)]">
|
||||
<GitBranch size={18} className="shrink-0" />
|
||||
<span className="min-w-0 max-w-[220px] truncate text-[var(--color-text-primary)]">
|
||||
{selectedBranch?.name || t('repoLaunch.noBranch')}
|
||||
</span>
|
||||
<ChevronDown size={14} className="shrink-0 text-[var(--color-text-tertiary)]" />
|
||||
<ChevronDown size={16} className="shrink-0 text-[var(--color-text-tertiary)]" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
disabled={disabled || requiresIsolation}
|
||||
aria-pressed={useWorktree}
|
||||
aria-label={t('repoLaunch.toggleWorktree')}
|
||||
onClick={() => onUseWorktreeChange(!useWorktree)}
|
||||
className={`inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35 disabled:cursor-not-allowed disabled:opacity-50 ${
|
||||
className={`${workbarButtonClassName} ${
|
||||
useWorktree
|
||||
? 'border-[var(--color-brand)]/40 bg-[var(--color-primary-fixed)] text-[var(--color-text-primary)]'
|
||||
: 'border-[var(--color-border)] bg-[var(--color-surface-container-low)] text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)]'
|
||||
? 'bg-[var(--color-primary-fixed)] text-[var(--color-text-primary)]'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<GitFork size={15} className="shrink-0" />
|
||||
<span className="font-medium">
|
||||
<GitFork size={18} className="shrink-0" />
|
||||
<span className="min-w-0 truncate">
|
||||
{useWorktree ? t('repoLaunch.worktreeIsolated') : t('repoLaunch.worktreeCurrent')}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@ -679,9 +679,9 @@ export const en = {
|
||||
'settings.general.webSearchSave': 'Save',
|
||||
|
||||
// ─── Empty Session ──────────────────────────────────────
|
||||
'empty.title': 'New session',
|
||||
'empty.title': 'What should we build?',
|
||||
'empty.subtitle': 'Start a fresh coding session. Claude is ready to help you build, debug, and architect your project.',
|
||||
'empty.placeholder': 'Ask anything...',
|
||||
'empty.placeholder': 'Ask Claude anything. Type @ to mention files or tools',
|
||||
'empty.addFiles': 'Add files or photos',
|
||||
'empty.slashCommands': 'Slash commands',
|
||||
'empty.failedToCreate': 'Failed to create session',
|
||||
|
||||
@ -681,9 +681,9 @@ export const zh: Record<TranslationKey, string> = {
|
||||
'settings.general.webSearchSave': '保存',
|
||||
|
||||
// ─── Empty Session ──────────────────────────────────────
|
||||
'empty.title': '新建会话',
|
||||
'empty.title': '我们该构建什么?',
|
||||
'empty.subtitle': '开始一个新的编码会话。Claude 已准备好帮你构建、调试和架构你的项目。',
|
||||
'empty.placeholder': '随便问点什么...',
|
||||
'empty.placeholder': '向 Claude 询问任何事。输入 @ 使用文件或工具',
|
||||
'empty.addFiles': '添加文件或图片',
|
||||
'empty.slashCommands': '斜杠命令',
|
||||
'empty.failedToCreate': '创建会话失败',
|
||||
|
||||
@ -303,4 +303,54 @@ describe('EmptySession', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('defaults to isolated worktree when the fallback branch is checked out elsewhere', async () => {
|
||||
mocks.getRepositoryContext.mockResolvedValueOnce(okRepositoryContext({
|
||||
currentBranch: null,
|
||||
defaultBranch: 'main',
|
||||
branches: [
|
||||
{
|
||||
name: 'main',
|
||||
current: false,
|
||||
local: true,
|
||||
remote: false,
|
||||
checkedOut: true,
|
||||
worktreePath: '/workspace/project',
|
||||
},
|
||||
{
|
||||
name: 'feature/a',
|
||||
current: false,
|
||||
local: true,
|
||||
remote: false,
|
||||
checkedOut: false,
|
||||
},
|
||||
],
|
||||
worktrees: [{
|
||||
path: '/workspace/project/.codex/worktrees/detached/project',
|
||||
branch: null,
|
||||
current: true,
|
||||
}],
|
||||
}))
|
||||
|
||||
render(<EmptySession />)
|
||||
|
||||
fireEvent.change(screen.getByRole('textbox'), {
|
||||
target: { value: 'draft question', selectionStart: 14 },
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Pick project' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('main')).toBeInTheDocument()
|
||||
expect(screen.getByText('Isolated worktree')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Run/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.createSession).toHaveBeenCalledWith({
|
||||
workDir: '/workspace/project',
|
||||
repository: { branch: 'main', worktree: true },
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -533,25 +533,17 @@ export function EmptySession() {
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-1 flex-col overflow-hidden bg-[var(--color-surface)]">
|
||||
<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)' }}>
|
||||
<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)' }}>
|
||||
{t('empty.title')}
|
||||
</h1>
|
||||
<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 gap-2">
|
||||
<div
|
||||
className="glass-panel relative flex flex-col gap-3 rounded-xl p-4"
|
||||
onDragOver={(event) => event.preventDefault()}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<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}
|
||||
>
|
||||
{fileSearchOpen && (
|
||||
<FileSearchMenu
|
||||
ref={fileSearchRef}
|
||||
@ -647,22 +639,22 @@ export function EmptySession() {
|
||||
onChange={(event) => handleInputChange(event.target.value, event.target.selectionStart ?? event.target.value.length)}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
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)]"
|
||||
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)]"
|
||||
style={{ fontFamily: 'var(--font-body)' }}
|
||||
placeholder={t('empty.placeholder')}
|
||||
rows={2}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border-t border-[var(--color-border-separator)] pt-3">
|
||||
<div className="flex items-center justify-between border-t border-[var(--color-border-separator)] pt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div ref={plusMenuRef} className="relative">
|
||||
<button
|
||||
onClick={() => setPlusMenuOpen((prev) => !prev)}
|
||||
aria-label="Open composer tools"
|
||||
className="rounded-lg p-1.5 text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)]"
|
||||
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"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">add</span>
|
||||
<span className="material-symbols-outlined text-[28px]">add</span>
|
||||
</button>
|
||||
|
||||
{plusMenuOpen && (
|
||||
@ -701,27 +693,29 @@ export function EmptySession() {
|
||||
/>
|
||||
<ModelSelector runtimeKey={DRAFT_RUNTIME_SELECTION_KEY} disabled={isSubmitting} />
|
||||
<button
|
||||
aria-label={t('common.run')}
|
||||
onClick={handleSubmit}
|
||||
disabled={!canSubmit}
|
||||
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"
|
||||
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"
|
||||
>
|
||||
{t('common.run')}
|
||||
<span className="material-symbols-outlined text-[14px]">arrow_forward</span>
|
||||
<span className="sr-only">{t('common.run')}</span>
|
||||
<span className="material-symbols-outlined text-[28px]">arrow_upward</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RepositoryLaunchControls
|
||||
workDir={workDir}
|
||||
onWorkDirChange={handleWorkDirChange}
|
||||
branch={selectedBranch}
|
||||
onBranchChange={setSelectedBranch}
|
||||
useWorktree={useWorktree}
|
||||
onUseWorktreeChange={setUseWorktree}
|
||||
onLaunchReadyChange={setRepositoryLaunchReady}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<RepositoryLaunchControls
|
||||
workDir={workDir}
|
||||
onWorkDirChange={handleWorkDirChange}
|
||||
branch={selectedBranch}
|
||||
onBranchChange={setSelectedBranch}
|
||||
useWorktree={useWorktree}
|
||||
onUseWorktreeChange={setUseWorktree}
|
||||
onLaunchReadyChange={setRepositoryLaunchReady}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user