diff --git a/src/renderer/src/components/NewWorkspaceComposerCard.tsx b/src/renderer/src/components/NewWorkspaceComposerCard.tsx
index 6c8ba28f1..05b6fd7a5 100644
--- a/src/renderer/src/components/NewWorkspaceComposerCard.tsx
+++ b/src/renderer/src/components/NewWorkspaceComposerCard.tsx
@@ -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({
{createError ? (
-
- {createError}
+
+ {createError.help ? (
+
+
{createError.title}
+
{createError.message}
+
{createError.help}
+
+ ) : (
+ createError.message
+ )}
) : null}
diff --git a/src/renderer/src/components/editor/CombinedDiffViewer.tsx b/src/renderer/src/components/editor/CombinedDiffViewer.tsx
index b444d713d..ac5a3587d 100644
--- a/src/renderer/src/components/editor/CombinedDiffViewer.tsx
+++ b/src/renderer/src/components/editor/CombinedDiffViewer.tsx
@@ -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
diff --git a/src/renderer/src/components/editor/DiffSectionItem.tsx b/src/renderer/src/components/editor/DiffSectionItem.tsx
index f7cb804a8..e2b12eb65 100644
--- a/src/renderer/src/components/editor/DiffSectionItem.tsx
+++ b/src/renderer/src/components/editor/DiffSectionItem.tsx
@@ -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,
diff --git a/src/renderer/src/components/editor/diff-section-types.ts b/src/renderer/src/components/editor/diff-section-types.ts
new file mode 100644
index 000000000..d74266c0b
--- /dev/null
+++ b/src/renderer/src/components/editor/diff-section-types.ts
@@ -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
+}
diff --git a/src/renderer/src/hooks/useComposerState.ts b/src/renderer/src/hooks/useComposerState.ts
index 3ff303c7b..a401775b6 100644
--- a/src/renderer/src/hooks/useComposerState.ts
+++ b/src/renderer/src/hooks/useComposerState.ts
@@ -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
(null)
+ const [createError, setCreateError] = useState(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)
}
diff --git a/src/renderer/src/lib/workspace-create-error-format.test.ts b/src/renderer/src/lib/workspace-create-error-format.test.ts
new file mode 100644
index 000000000..4f8fc7a59
--- /dev/null
+++ b/src/renderer/src/lib/workspace-create-error-format.test.ts
@@ -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')
+ })
+})
diff --git a/src/renderer/src/lib/workspace-create-error-format.ts b/src/renderer/src/lib/workspace-create-error-format.ts
new file mode 100644
index 000000000..98c427d45
--- /dev/null
+++ b/src/renderer/src/lib/workspace-create-error-format.ts
@@ -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
+}