diff --git a/src/renderer/src/components/NewWorkspaceComposerCard.start-from.test.tsx b/src/renderer/src/components/NewWorkspaceComposerCard.start-from.test.tsx index 5b186fc9378..15edfd8680e 100644 --- a/src/renderer/src/components/NewWorkspaceComposerCard.start-from.test.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerCard.start-from.test.tsx @@ -55,12 +55,18 @@ vi.mock('@/components/new-workspace/ProjectCombobox', () => ({ vi.mock('@/components/repo/CreateFromPicker', () => ({ CreateFromPicker: ({ value, - onValueChange + onValueChange, + readOnly }: { value: string onValueChange: (next: string) => void + readOnly?: boolean }) => ( -
+
@@ -174,6 +180,14 @@ describe('NewWorkspaceComposerCard start from', () => { expect(picks).toEqual(['release/1.2']) }) + it('renders the base ref as read-only while the project needs a connection', () => { + container = renderCard({ selectedRepoRequiresConnection: true }) + + expect( + container.querySelector('[data-testid="base-ref-picker"]')?.getAttribute('data-readonly') + ).toBe('true') + }) + it('reports the project default as no base at all', () => { const picks: (string | undefined)[] = [] container = renderCard({ diff --git a/src/renderer/src/components/NewWorkspaceComposerCard.test.tsx b/src/renderer/src/components/NewWorkspaceComposerCard.test.tsx index 8206354f916..bc3e79a4171 100644 --- a/src/renderer/src/components/NewWorkspaceComposerCard.test.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerCard.test.tsx @@ -377,7 +377,7 @@ describe('NewWorkspaceComposerCard folder task source mode', () => { '[data-contextual-tour-target="workspace-creation-name"]' ) expect(projectSection?.textContent).not.toContain('Task Source') - expect(nameSection?.textContent).toContain("Name or 'Create From'") + expect(nameSection?.textContent).toContain('Create From') expect( current.container .querySelector('[aria-label="workspace name"]') diff --git a/src/renderer/src/components/new-workspace/ComposerBaseRefPicker.tsx b/src/renderer/src/components/new-workspace/ComposerBaseRefPicker.tsx index e97f980ee87..3d54ded9a39 100644 --- a/src/renderer/src/components/new-workspace/ComposerBaseRefPicker.tsx +++ b/src/renderer/src/components/new-workspace/ComposerBaseRefPicker.tsx @@ -1,12 +1,15 @@ import React from 'react' import { CreateFromPicker } from '@/components/repo/CreateFromPicker' +import { useAppStore } from '@/store' import { useRepoMap, useWorktreesForRepo } from '@/store/selectors' +import { getRepoExecutionHostId } from '../../../../shared/execution-host' type ComposerBaseRefPickerProps = { repoId: string baseBranch: string | undefined onBaseBranchChange: (value: string | undefined) => void resetHint: string | null | undefined + readOnly?: boolean } /** @@ -19,12 +22,15 @@ export function ComposerBaseRefPicker({ repoId, baseBranch, onBaseBranchChange, - resetHint + resetHint, + readOnly = false }: ComposerBaseRefPickerProps): React.JSX.Element { const repoMap = useRepoMap() + const repo = repoMap.get(repoId) const repoWorktrees = useWorktreesForRepo(repoId) + const updateRepo = useAppStore((state) => state.updateRepo) return ( -
+
onBaseBranchChange(nextBaseBranch || undefined)} + onSetDefault={ + readOnly + ? undefined + : async (nextBaseBranch) => { + await updateRepo( + repoId, + { worktreeBaseRef: nextBaseBranch }, + repo ? { hostId: getRepoExecutionHostId(repo) } : undefined + ) + } + } /> {resetHint ?

{resetHint}

: null}
diff --git a/src/renderer/src/components/new-workspace/NewWorkspaceComposerNameSection.tsx b/src/renderer/src/components/new-workspace/NewWorkspaceComposerNameSection.tsx index f05ef9d06a1..8bc581b095e 100644 --- a/src/renderer/src/components/new-workspace/NewWorkspaceComposerNameSection.tsx +++ b/src/renderer/src/components/new-workspace/NewWorkspaceComposerNameSection.tsx @@ -33,6 +33,7 @@ type NewWorkspaceComposerNameSectionProps = Pick< | 'allowSmartNameAddProject' | 'smartNameRepoSwitchTarget' | 'onSmartNameModeChange' + | 'smartNameMode' | 'forkPushWarning' | 'canReuseSelectedBranch' | 'reuseSelectedBranch' @@ -69,6 +70,7 @@ export function NewWorkspaceComposerNameSection({ allowSmartNameAddProject = true, smartNameRepoSwitchTarget = 'project', onSmartNameModeChange, + smartNameMode, onNamePlainEnter, forkPushWarning, canReuseSelectedBranch, @@ -83,21 +85,33 @@ export function NewWorkspaceComposerNameSection({ shouldShowComposerBaseRefPicker({ selectedRepoIsGit, branchesEnabled, + smartNameMode: smartNameMode ?? 'smart', smartNameSelectionKind: smartNameSelection?.kind ?? null }) return (
- +
+ + {showBaseRefPicker && onBaseBranchChange ? ( + + ) : null} +
{forkPushWarning}

) : null} - {showBaseRefPicker && onBaseBranchChange ? ( - - ) : null}
{ it('offers a base ref when no source is selected yet', () => { - expect(shouldShowComposerBaseRefPicker({ ...gitRepo, smartNameSelectionKind: null })).toBe(true) + expect( + shouldShowComposerBaseRefPicker({ + ...gitRepo, + smartNameMode: 'smart', + smartNameSelectionKind: null + }) + ).toBe(true) }) it.each(['github-issue', 'gitlab-issue', 'linear', 'jira'] as const)( 'offers a base ref for a %s source', (smartNameSelectionKind) => { - expect(shouldShowComposerBaseRefPicker({ ...gitRepo, smartNameSelectionKind })).toBe(true) + expect( + shouldShowComposerBaseRefPicker({ + ...gitRepo, + smartNameMode: 'smart', + smartNameSelectionKind + }) + ).toBe(true) } ) it.each(['github-pr', 'gitlab-mr', 'branch'] as const)( 'hides the base ref for a %s source that carries its own base', (smartNameSelectionKind) => { - expect(shouldShowComposerBaseRefPicker({ ...gitRepo, smartNameSelectionKind })).toBe(false) + expect( + shouldShowComposerBaseRefPicker({ + ...gitRepo, + smartNameMode: 'smart', + smartNameSelectionKind + }) + ).toBe(false) } ) @@ -27,6 +45,7 @@ describe('shouldShowComposerBaseRefPicker', () => { shouldShowComposerBaseRefPicker({ selectedRepoIsGit: false, branchesEnabled: true, + smartNameMode: 'smart', smartNameSelectionKind: 'jira' }) ).toBe(false) @@ -37,8 +56,19 @@ describe('shouldShowComposerBaseRefPicker', () => { shouldShowComposerBaseRefPicker({ selectedRepoIsGit: true, branchesEnabled: false, + smartNameMode: 'smart', smartNameSelectionKind: 'jira' }) ).toBe(false) }) + + it('hides the base ref while the Branch tab is active before selection', () => { + expect( + shouldShowComposerBaseRefPicker({ + ...gitRepo, + smartNameMode: 'branches', + smartNameSelectionKind: null + }) + ).toBe(false) + }) }) diff --git a/src/renderer/src/components/new-workspace/base-ref-picker-visibility.ts b/src/renderer/src/components/new-workspace/base-ref-picker-visibility.ts index e3c985a8142..5a86e831021 100644 --- a/src/renderer/src/components/new-workspace/base-ref-picker-visibility.ts +++ b/src/renderer/src/components/new-workspace/base-ref-picker-visibility.ts @@ -1,4 +1,5 @@ import type { SmartWorkspaceNameSelection } from '@/components/new-workspace/SmartWorkspaceNameField' +import type { SmartNameMode } from './smart-workspace-source-results' /** * Whether the composer offers a base ref for the worktree it is about to create. @@ -10,11 +11,15 @@ import type { SmartWorkspaceNameSelection } from '@/components/new-workspace/Sma export function shouldShowComposerBaseRefPicker(args: { selectedRepoIsGit: boolean branchesEnabled: boolean + smartNameMode: SmartNameMode smartNameSelectionKind: SmartWorkspaceNameSelection['kind'] | null }): boolean { if (!args.selectedRepoIsGit || !args.branchesEnabled) { return false } + if (args.smartNameMode === 'branches') { + return false + } // Why: these already carry a base — a PR/MR pins its own head, and a branch pick IS the base, // where overriding would silently turn a checkout of it into a new branch off something else. return ( diff --git a/src/renderer/src/components/new-workspace/new-workspace-composer-card-props.ts b/src/renderer/src/components/new-workspace/new-workspace-composer-card-props.ts index aebfdb12204..b59a9f72fa5 100644 --- a/src/renderer/src/components/new-workspace/new-workspace-composer-card-props.ts +++ b/src/renderer/src/components/new-workspace/new-workspace-composer-card-props.ts @@ -74,6 +74,7 @@ export type NewWorkspaceComposerCardProps = { onSmartGitLabItemSelect: (item: GitLabWorkItem) => void onSmartBranchSelect: (refName: string, localBranchName: string) => void onSmartNameModeChange?: (mode: SmartNameMode) => void + smartNameMode?: SmartNameMode onSmartLinearIssueSelect: (issue: LinearIssue) => void onSmartJiraIssueSelect?: (issue: JiraIssue, sourceContext: TaskSourceContext) => void onOpenJiraSettings?: () => void diff --git a/src/renderer/src/components/repo/CreateFromPicker.test.tsx b/src/renderer/src/components/repo/CreateFromPicker.test.tsx index 38c13cbb282..9216645fbc6 100644 --- a/src/renderer/src/components/repo/CreateFromPicker.test.tsx +++ b/src/renderer/src/components/repo/CreateFromPicker.test.tsx @@ -5,6 +5,7 @@ import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { Repo } from '../../../../shared/repo-types' import { CreateFromPicker } from './CreateFromPicker' +import { TooltipProvider } from '@/components/ui/tooltip' import { getRuntimeRepoBaseRefDefault, searchRuntimeRepoBaseRefs @@ -74,13 +75,15 @@ function makeRepo(overrides: Partial): Repo { async function renderPicker(repo: Repo): Promise { await act(async () => { root.render( - + + + ) }) } @@ -109,7 +112,8 @@ describe('CreateFromPicker host routing', () => { expect(getRuntimeRepoBaseRefDefault).toHaveBeenCalledWith( { activeRuntimeEnvironmentId: 'owner-runtime' }, - repo.id + repo.id, + 'runtime:owner-runtime' ) }) @@ -131,7 +135,8 @@ describe('CreateFromPicker host routing', () => { { activeRuntimeEnvironmentId: null }, repo.id, '', - 30 + 30, + 'local' ) }) @@ -143,7 +148,8 @@ describe('CreateFromPicker host routing', () => { expect(getRuntimeRepoBaseRefDefault).toHaveBeenCalledWith( { activeRuntimeEnvironmentId: null }, - repo.id + repo.id, + 'local' ) expect(searchRuntimeRepoBaseRefs).not.toHaveBeenCalled() }) diff --git a/src/renderer/src/components/repo/CreateFromPicker.tsx b/src/renderer/src/components/repo/CreateFromPicker.tsx index 3652e955ff2..3609e625f5c 100644 --- a/src/renderer/src/components/repo/CreateFromPicker.tsx +++ b/src/renderer/src/components/repo/CreateFromPicker.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Check, ChevronsUpDown } from 'lucide-react' +import { Check, ChevronsUpDown, GitBranch, Star } from 'lucide-react' import { Button } from '@/components/ui/button' import { Command, @@ -8,10 +8,17 @@ import { CommandItem, CommandList } from '@/components/ui/command' +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger +} from '@/components/ui/context-menu' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { cn } from '@/lib/utils' import type { Repo } from '../../../../shared/repo-types' import type { Worktree } from '../../../../shared/worktree/types' +import { getRepoExecutionHostId } from '../../../../shared/execution-host' import { useAppStore } from '@/store' import { getRuntimeEnvironmentIdForRepo } from '@/lib/repo-runtime-owner' import { @@ -20,6 +27,7 @@ import { } from '@/runtime/runtime-repo-client' import { isRuntimeRepoRefSearchQueryWithinLimit } from '@/runtime/runtime-repo-search-bounds' import { translate } from '@/i18n/i18n' +import { FilePathCursorTooltip } from '@/components/file-path-cursor-tooltip' const DEFAULT_VALUE = '__project_default__' @@ -33,14 +41,20 @@ export function CreateFromPicker({ worktrees, value, triggerClassName, - onValueChange + compact = false, + readOnly = false, + onValueChange, + onSetDefault }: { repoId: string repoMap: Map worktrees: Worktree[] value: string triggerClassName?: string + compact?: boolean + readOnly?: boolean onValueChange: (baseBranch: string) => void + onSetDefault?: (baseBranch: string) => void | Promise }): React.JSX.Element { // Per-repo evidence, not the ambient active-runtime setting; the base-ref helpers // just take a settings-shaped object, so it is synthesized at each call below. @@ -48,6 +62,7 @@ export function CreateFromPicker({ getRuntimeEnvironmentIdForRepo(state, repoId) ) const repo = repoMap.get(repoId) + const repoHostId = repo ? getRepoExecutionHostId(repo) : undefined const [open, setOpen] = React.useState(false) const inputRef = React.useRef(null) const focusFrameRef = React.useRef(null) @@ -57,8 +72,13 @@ export function CreateFromPicker({ const [isSearching, setIsSearching] = React.useState(false) const effectiveDefault = repo?.worktreeBaseRef ?? defaultBaseRef const selectedValue = value || DEFAULT_VALUE + const projectDefaultLabel = translate( + 'auto.components.automations.CreateFromPicker.ef6d762538', + 'Project default' + ) const selectedLabel = - value || (effectiveDefault ? `${effectiveDefault} (default)` : 'Project default') + value || (effectiveDefault ? `${effectiveDefault} (default)` : projectDefaultLabel) + const compactLabel = value || effectiveDefault || projectDefaultLabel const branchOptions = React.useMemo(() => { const options = new Set() if (effectiveDefault) { @@ -76,6 +96,34 @@ export function CreateFromPicker({ return Array.from(options).sort((left, right) => left.localeCompare(right)) }, [effectiveDefault, searchResults, worktrees]) + const renderBranchContextMenu = React.useCallback( + (branch: string, row: React.ReactNode): React.ReactNode => { + if (!onSetDefault || !branch) { + return row + } + const isDefault = branch === effectiveDefault + return ( + + {row} + + { + void onSetDefault(branch) + }} + > + + {isDefault + ? translate('auto.components.agent.AgentCombobox.1b0d6965fa', 'Current default') + : translate('auto.components.agent.AgentCombobox.9c6b59fe58', 'Set as default')} + + + + ) + }, + [effectiveDefault, onSetDefault] + ) + const cancelFocusFrame = React.useCallback((): void => { if (focusFrameRef.current !== null) { cancelAnimationFrame(focusFrameRef.current) @@ -119,7 +167,8 @@ export function CreateFromPicker({ setDefaultBaseRef(null) void getRuntimeRepoBaseRefDefault( { activeRuntimeEnvironmentId: repoRuntimeEnvironmentId }, - repoId + repoId, + repoHostId ) .then((result) => { if (!stale) { @@ -134,7 +183,7 @@ export function CreateFromPicker({ return () => { stale = true } - }, [repoRuntimeEnvironmentId, repoId]) + }, [repoHostId, repoRuntimeEnvironmentId, repoId]) React.useEffect(() => { if (!isRuntimeRepoRefSearchQueryWithinLimit(query)) { @@ -158,7 +207,8 @@ export function CreateFromPicker({ { activeRuntimeEnvironmentId: repoRuntimeEnvironmentId }, repoId, trimmedQuery, - 30 + 30, + repoHostId ) .then((results) => { if (!stale) { @@ -181,108 +231,154 @@ export function CreateFromPicker({ stale = true window.clearTimeout(timer) } - }, [repoRuntimeEnvironmentId, open, query, repoId]) + }, [repoHostId, repoRuntimeEnvironmentId, open, query, repoId]) + + const trigger = compact ? ( + + ) : ( + + ) return (
- - - - - { - event.preventDefault() - focusSearchInput() - }} - > - - - - - {isSearching - ? translate( - 'auto.components.automations.CreateFromPicker.9ce96621f4', - 'Searching branches...' - ) - : translate( - 'auto.components.automations.CreateFromPicker.79512f22a7', - 'No branches found.' - )} - - { - onValueChange('') - setOpen(false) - }} - > - - - {effectiveDefault + /> + + + {isSearching ? translate( - 'auto.components.automations.CreateFromPicker.e53d306056', - '{{value0}} (default)', - { value0: effectiveDefault } + 'auto.components.automations.CreateFromPicker.9ce96621f4', + 'Searching branches...' ) : translate( - 'auto.components.automations.CreateFromPicker.ef6d762538', - 'Project default' + 'auto.components.automations.CreateFromPicker.79512f22a7', + 'No branches found.' )} - - - {branchOptions - .filter((branch) => branch !== effectiveDefault) - .map((branch) => ( + + {renderBranchContextMenu( + effectiveDefault ?? '', { - onValueChange(branch) + onValueChange('') setOpen(false) }} > - {branch} + + + {effectiveDefault + ? translate( + 'auto.components.automations.CreateFromPicker.e53d306056', + '{{value0}} (default)', + { value0: effectiveDefault } + ) + : translate( + 'auto.components.automations.CreateFromPicker.ef6d762538', + 'Project default' + )} + + - ))} - - - - + )} + {branchOptions + .filter((branch) => branch !== effectiveDefault) + .map((branch) => + renderBranchContextMenu( + branch, + { + onValueChange(branch) + setOpen(false) + }} + > + + + {branch} + + + ) + )} + + + + + )}
) } diff --git a/src/renderer/src/hooks/composer-state/composer-card-contract.ts b/src/renderer/src/hooks/composer-state/composer-card-contract.ts index 1dffa161c32..5be3c98bf8b 100644 --- a/src/renderer/src/hooks/composer-state/composer-card-contract.ts +++ b/src/renderer/src/hooks/composer-state/composer-card-contract.ts @@ -18,6 +18,7 @@ export type ComposerCardSourceProps = Pick< | 'selectedRepoExecutionHostId' | 'selectedRepoProjectId' | 'smartNameSelection' + | 'smartNameMode' | 'reuseSelectedBranch' | 'createMultiple' | 'agentPrompt' diff --git a/src/renderer/src/hooks/composer-state/composer-card-props.ts b/src/renderer/src/hooks/composer-state/composer-card-props.ts index 0e126e3af75..308dbbb65db 100644 --- a/src/renderer/src/hooks/composer-state/composer-card-props.ts +++ b/src/renderer/src/hooks/composer-state/composer-card-props.ts @@ -111,6 +111,7 @@ export function buildComposerCardProps(state: ComposerModel) { shouldWaitForSetupCheck, workspaceSeedName, smartNameJiraSourceContext, + smartNameMode, smartNameSelection, sparsePresets, sparseSelectedPresetId, @@ -174,6 +175,7 @@ export function buildComposerCardProps(state: ComposerModel) { onOpenJiraSettings: handleOpenJiraSettings, smartNameGitHubSourceContext: selectedRepoGitHubSourceContext, smartNameJiraSourceContext, + smartNameMode, smartNameSelection, onClearSmartNameSelection: handleClearSmartNameSelection, canReuseSelectedBranch: diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 23f03820b50..031c33895dd 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -1601,7 +1601,7 @@ "326a578923": "orca.yaml + local", "2132b670da": "both", "0e587e31fb": "yaml", - "ac3748dcda": "Name or 'Create From'", + "ac3748dcda": "Create From", "f660aa1454": "Connecting", "7711ad5122": "Local setup command", "e5db1b0419": "Combined setup command", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 8e73672554c..4ace0d0908c 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -1312,7 +1312,7 @@ "326a578923": "orca.yaml + local", "2132b670da": "ambos", "0e587e31fb": "yaml", - "ac3748dcda": "Nombre o 'Crear desde'", + "ac3748dcda": "Crear desde", "f660aa1454": "Conectando", "7711ad5122": "Comando de configuración local", "e5db1b0419": "Comando de configuración combinado", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index c9ec69f8e46..4f1ce964033 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -1312,7 +1312,7 @@ "326a578923": "orca.yaml + ローカル", "2132b670da": "両方", "0e587e31fb": "yaml", - "ac3748dcda": "名前または「作成元」", + "ac3748dcda": "作成元", "f660aa1454": "接続中", "7711ad5122": "ローカルセットアップコマンド", "e5db1b0419": "組み合わせセットアップコマンド", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index bf60596e599..65691bdc12b 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -1317,7 +1317,7 @@ "326a578923": "orca.yaml + 로컬", "2132b670da": "둘 다", "0e587e31fb": "yaml", - "ac3748dcda": "이름 또는 '다음에서 만들기'", + "ac3748dcda": "다음에서 만들기", "f660aa1454": "연결 중", "7711ad5122": "로컬 설정 명령", "e5db1b0419": "결합된 설정 명령", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 3f2a1e2d1ed..645ac873b5e 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -1315,7 +1315,7 @@ "326a578923": "orca.yaml + 本地", "2132b670da": "两者", "0e587e31fb": "yaml", - "ac3748dcda": "名称或创建来源", + "ac3748dcda": "创建来源", "f660aa1454": "连接中", "7711ad5122": "本地设置命令", "e5db1b0419": "组合设置命令",