Guide workspace creation for empty repositories (#1926)
* Guide empty repository workspace creation * fix: address CI failures
This commit is contained in:
parent
be76195159
commit
82f99ee6ef
|
|
@ -28,6 +28,7 @@ import SparseCheckoutPresetSelect from '@/components/sparse/SparseCheckoutPreset
|
|||
import SmartWorkspaceNameField, {
|
||||
type SmartWorkspaceNameSelection
|
||||
} from '@/components/new-workspace/SmartWorkspaceNameField'
|
||||
import type { WorkspaceCreateErrorDisplay } from '@/lib/workspace-create-error-format'
|
||||
|
||||
const isMac = typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac')
|
||||
|
||||
|
|
@ -65,7 +66,7 @@ type NewWorkspaceComposerCardProps = {
|
|||
onSetupDecisionChange: (value: 'run' | 'skip') => void
|
||||
shouldWaitForSetupCheck: boolean
|
||||
resolvedSetupDecision: 'run' | 'skip' | null
|
||||
createError: string | null
|
||||
createError: WorkspaceCreateErrorDisplay | null
|
||||
canUseSparseCheckout: boolean
|
||||
sparsePresets: SparsePreset[]
|
||||
sparseSelectedPresetId: string | null
|
||||
|
|
@ -549,8 +550,19 @@ export default function NewWorkspaceComposerCard({
|
|||
</div>
|
||||
|
||||
{createError ? (
|
||||
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||
{createError}
|
||||
<div
|
||||
role="alert"
|
||||
className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive"
|
||||
>
|
||||
{createError.help ? (
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium">{createError.title}</p>
|
||||
<p>{createError.message}</p>
|
||||
<p className="text-destructive/85">{createError.help}</p>
|
||||
</div>
|
||||
) : (
|
||||
createError.message
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,23 +16,10 @@ import { getRuntimeGitBranchDiff, getRuntimeGitDiff } from '@/runtime/runtime-gi
|
|||
import '@/lib/monaco-setup'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import type { OpenFile } from '@/store/slices/editor'
|
||||
import type { GitBranchChangeEntry, GitDiffResult, GitStatusEntry } from '../../../../shared/types'
|
||||
import type { GitBranchChangeEntry, GitDiffResult } from '../../../../shared/types'
|
||||
import { DiffSectionItem } from './DiffSectionItem'
|
||||
import { getCombinedUncommittedEntries } from './combined-diff-entries'
|
||||
|
||||
type DiffSection = {
|
||||
key: string
|
||||
path: string
|
||||
status: string
|
||||
area?: GitStatusEntry['area']
|
||||
oldPath?: string
|
||||
originalContent: string
|
||||
modifiedContent: string
|
||||
collapsed: boolean
|
||||
loading: boolean
|
||||
dirty: boolean
|
||||
diffResult: GitDiffResult | null
|
||||
}
|
||||
import type { DiffSection } from './diff-section-types'
|
||||
|
||||
type CachedCombinedDiffViewState = {
|
||||
entrySignature: string
|
||||
|
|
|
|||
|
|
@ -23,25 +23,12 @@ import { DiffCommentPopover } from '../diff-comments/DiffCommentPopover'
|
|||
import { getDiffCommentPopoverTop } from '../diff-comments/diff-comment-popover-position'
|
||||
import { applyDiffEditorLineNumberOptions } from './diff-editor-line-number-options'
|
||||
import { computeLineStats } from './diff-line-stats'
|
||||
import type { DiffComment, GitDiffResult } from '../../../../shared/types'
|
||||
import type { DiffSection } from './diff-section-types'
|
||||
import type { DiffComment } from '../../../../shared/types'
|
||||
import { isDiffComment } from '@/lib/diff-comment-compat'
|
||||
|
||||
const ImageDiffViewer = lazy(() => import('./ImageDiffViewer'))
|
||||
|
||||
type DiffSection = {
|
||||
key: string
|
||||
path: string
|
||||
status: string
|
||||
area?: 'staged' | 'unstaged' | 'untracked'
|
||||
oldPath?: string
|
||||
originalContent: string
|
||||
modifiedContent: string
|
||||
collapsed: boolean
|
||||
loading: boolean
|
||||
dirty: boolean
|
||||
diffResult: GitDiffResult | null
|
||||
}
|
||||
|
||||
export function DiffSectionItem({
|
||||
section,
|
||||
index,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
import type { GitDiffResult, GitStatusEntry } from '../../../../shared/types'
|
||||
|
||||
export type DiffSection = {
|
||||
key: string
|
||||
path: string
|
||||
status: string
|
||||
area?: GitStatusEntry['area']
|
||||
oldPath?: string
|
||||
originalContent: string
|
||||
modifiedContent: string
|
||||
collapsed: boolean
|
||||
loading: boolean
|
||||
dirty: boolean
|
||||
diffResult: GitDiffResult | null
|
||||
}
|
||||
|
|
@ -61,6 +61,11 @@ import {
|
|||
readRuntimeIssueCommand,
|
||||
type HookCheckResult
|
||||
} from '@/runtime/runtime-hooks-client'
|
||||
import {
|
||||
formatWorkspaceCreateError,
|
||||
getWorkspaceCreateErrorToastMessage,
|
||||
type WorkspaceCreateErrorDisplay
|
||||
} from '@/lib/workspace-create-error-format'
|
||||
|
||||
export type UseComposerStateOptions = {
|
||||
initialRepoId?: string
|
||||
|
|
@ -167,7 +172,7 @@ export type ComposerCardProps = {
|
|||
onSetupDecisionChange: (value: 'run' | 'skip') => void
|
||||
shouldWaitForSetupCheck: boolean
|
||||
resolvedSetupDecision: 'run' | 'skip' | null
|
||||
createError: string | null
|
||||
createError: WorkspaceCreateErrorDisplay | null
|
||||
canUseSparseCheckout: boolean
|
||||
/** Saved presets for the currently-selected repo. Empty array when no
|
||||
* presets exist or when the repo is remote. */
|
||||
|
|
@ -375,7 +380,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
|||
const [hasLoadedIssueCommand, setHasLoadedIssueCommand] = useState(false)
|
||||
const [setupDecision, setSetupDecision] = useState<'run' | 'skip' | null>(null)
|
||||
const [creating, setCreating] = useState(false)
|
||||
const [createError, setCreateError] = useState<string | null>(null)
|
||||
const [createError, setCreateError] = useState<WorkspaceCreateErrorDisplay | null>(null)
|
||||
const [advancedOpen, setAdvancedOpen] = useState(
|
||||
persistDraft ? Boolean((newWorkspaceDraft?.note ?? '').trim()) : false
|
||||
)
|
||||
|
|
@ -1664,9 +1669,9 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
|||
}
|
||||
onCreated?.()
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create worktree.'
|
||||
setCreateError(message)
|
||||
toast.error(message)
|
||||
const formattedError = formatWorkspaceCreateError(error)
|
||||
setCreateError(formattedError)
|
||||
toast.error(getWorkspaceCreateErrorToastMessage(formattedError))
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
|
|
@ -1896,9 +1901,9 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
|||
}
|
||||
onCreated?.()
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create worktree.'
|
||||
setCreateError(message)
|
||||
toast.error(message)
|
||||
const formattedError = formatWorkspaceCreateError(error)
|
||||
setCreateError(formattedError)
|
||||
toast.error(getWorkspaceCreateErrorToastMessage(formattedError))
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
formatWorkspaceCreateError,
|
||||
getWorkspaceCreateErrorToastMessage
|
||||
} from './workspace-create-error-format'
|
||||
|
||||
describe('formatWorkspaceCreateError', () => {
|
||||
it('returns guidance for missing default base ref failures', () => {
|
||||
const error = new Error(
|
||||
'Could not resolve a default base ref for this repo. Pick a base branch explicitly and try again.'
|
||||
)
|
||||
|
||||
const formatted = formatWorkspaceCreateError(error)
|
||||
|
||||
expect(formatted).toEqual({
|
||||
title: 'No base branch found',
|
||||
message: 'Orca could not resolve a usable base ref for this workspace.',
|
||||
help: 'Create an initial commit (for example on main), or select an existing branch in Create From, then try again.'
|
||||
})
|
||||
expect(getWorkspaceCreateErrorToastMessage(formatted)).toBe('No base branch found')
|
||||
})
|
||||
|
||||
it('matches missing base ref failures case-insensitively', () => {
|
||||
const formatted = formatWorkspaceCreateError(
|
||||
new Error('COULD NOT RESOLVE A DEFAULT BASE REF from remote provider')
|
||||
)
|
||||
|
||||
expect(formatted.title).toBe('No base branch found')
|
||||
expect(formatted.help).toBeDefined()
|
||||
})
|
||||
|
||||
it('passes unknown errors through unchanged', () => {
|
||||
const formatted = formatWorkspaceCreateError(new Error('fatal: not a git repository'))
|
||||
|
||||
expect(formatted).toEqual({
|
||||
title: 'fatal: not a git repository',
|
||||
message: 'fatal: not a git repository'
|
||||
})
|
||||
expect(getWorkspaceCreateErrorToastMessage(formatted)).toBe('fatal: not a git repository')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
export type WorkspaceCreateErrorDisplay = {
|
||||
title: string
|
||||
message: string
|
||||
help?: string
|
||||
}
|
||||
|
||||
const MISSING_BASE_REF_ANCHOR = 'could not resolve a default base ref'
|
||||
|
||||
export function formatWorkspaceCreateError(error: unknown): WorkspaceCreateErrorDisplay {
|
||||
const message = error instanceof Error ? error.message : 'Failed to create worktree.'
|
||||
|
||||
if (message.toLowerCase().includes(MISSING_BASE_REF_ANCHOR)) {
|
||||
return {
|
||||
title: 'No base branch found',
|
||||
message: 'Orca could not resolve a usable base ref for this workspace.',
|
||||
help: 'Create an initial commit (for example on main), or select an existing branch in Create From, then try again.'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: message,
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
export function getWorkspaceCreateErrorToastMessage(error: WorkspaceCreateErrorDisplay): string {
|
||||
return error.help ? error.title : error.message
|
||||
}
|
||||
Loading…
Reference in New Issue