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 +}