fix: avoid treating normal folders as repository warnings

Non-Git folders are valid launch targets for desktop sessions, so the composer should not render them as an inline repository warning. The branch and worktree controls remain hidden outside Git repositories while server-side repository validation still protects actual branch-launch requests.

Constraint: Non-Git directories must remain usable as ordinary session workdirs.
Rejected: Keep the inline not-Git warning | it reads like an error even though the user can continue normally.
Confidence: high
Scope-risk: narrow
Directive: Keep dirty-worktree and checked-out-branch warnings visible because those still require a launch-mode decision.
Tested: cd desktop && bun run test -- --run src/pages/EmptySession.test.tsx src/components/chat/ChatInput.test.tsx
Tested: bun run check:desktop
Tested: git diff --check -- desktop/src/components/shared/RepositoryLaunchControls.tsx desktop/src/pages/EmptySession.test.tsx desktop/src/i18n/locales/en.ts desktop/src/i18n/locales/zh.ts
Not-tested: Native Tauri packaged window visual check
This commit is contained in:
程序员阿江(Relakkes) 2026-05-08 12:41:34 +08:00
parent eadbd4be14
commit 592ace93d0
4 changed files with 43 additions and 6 deletions

View File

@ -37,7 +37,7 @@ const VIEWPORT_GUTTER = 12
function stateMessage(context: RepositoryContextResult | null, error: string | null) {
if (error) return error
if (!context) return null
if (context.state === 'not_git_repo') return 'not_git'
if (context.state === 'not_git_repo') return null
if (context.state === 'missing_workdir') return 'missing'
if (context.state === 'error') return context.error || 'error'
return null
@ -370,9 +370,7 @@ export function RepositoryLaunchControls({
<div className="flex items-center gap-2 px-1 text-[11px] text-[var(--color-text-tertiary)]">
<AlertCircle size={13} className="shrink-0" />
<span>
{message === 'not_git'
? t('repoLaunch.notGit')
: message === 'missing'
{message === 'missing'
? t('repoLaunch.missingWorkdir')
: message}
</span>

View File

@ -706,7 +706,6 @@ export const en = {
'repoLaunch.worktreeCurrent': 'Current worktree',
'repoLaunch.worktreeIsolated': 'Isolated worktree',
'repoLaunch.selectWorktree': 'Select worktree mode',
'repoLaunch.notGit': 'Current project is not a Git repository.',
'repoLaunch.missingWorkdir': 'Working directory is missing.',
'repoLaunch.dirtyWarning': 'Uncommitted changes detected. Direct switching will be blocked; enable isolated worktree to continue without touching this folder.',
'repoLaunch.checkedOutWarning': 'This branch is already checked out elsewhere. Enable isolated worktree or choose another branch.',

View File

@ -708,7 +708,6 @@ export const zh: Record<TranslationKey, string> = {
'repoLaunch.worktreeCurrent': '当前工作树',
'repoLaunch.worktreeIsolated': '独立工作树',
'repoLaunch.selectWorktree': '选择工作树模式',
'repoLaunch.notGit': '当前项目不是 Git 仓库。',
'repoLaunch.missingWorkdir': '工作目录不存在。',
'repoLaunch.dirtyWarning': '检测到未提交变更,直接切换会被阻止。开启独立工作树即可继续,且不会改动当前目录。',
'repoLaunch.checkedOutWarning': '这个分支已在其他工作树中检出。请开启独立工作树或选择其他分支。',

View File

@ -103,6 +103,20 @@ function okRepositoryContext(overrides: Partial<RepositoryContextResult> = {}):
}
}
function notGitRepositoryContext(): RepositoryContextResult {
return {
state: 'not_git_repo',
workDir: '/workspace/project',
repoRoot: null,
repoName: null,
currentBranch: null,
defaultBranch: null,
dirty: false,
branches: [],
worktrees: [],
}
}
describe('EmptySession', () => {
const initialSessionState = useSessionStore.getInitialState()
const initialChatState = useChatStore.getInitialState()
@ -199,6 +213,33 @@ describe('EmptySession', () => {
expect(mocks.wsConnect).toHaveBeenCalledWith('draft-session')
})
it('starts in a selected non-Git project without showing a repository warning', async () => {
mocks.getRepositoryContext.mockResolvedValueOnce(notGitRepositoryContext())
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.getByRole('button', { name: /Run/i })).not.toBeDisabled()
})
expect(screen.queryByText('Current project is not a Git repository.')).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: /Select branch:/ })).not.toBeInTheDocument()
expect(screen.queryByText('Current worktree')).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: /Run/i }))
await waitFor(() => {
expect(mocks.createSession).toHaveBeenCalledWith({
workDir: '/workspace/project',
})
})
})
it('shows an actionable repository error when direct branch switching is blocked', async () => {
mocks.createSession.mockRejectedValueOnce(new ApiError(400, {
error: 'REPOSITORY_DIRTY_WORKTREE',