mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
This reverts commit a7b9ea5995.
Co-authored-by: Vincent <47273853+Tkotm76@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import React, { act } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import NewWorkspaceComposerCard from './NewWorkspaceComposerCard'
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: Object.assign(
|
||||
(selector: (state: unknown) => unknown) =>
|
||||
selector({
|
||||
closeModal: vi.fn(),
|
||||
openModal: vi.fn(),
|
||||
openSettingsPage: vi.fn(),
|
||||
openSettingsTarget: vi.fn(),
|
||||
setRuntimeEnvironmentStatus: vi.fn(),
|
||||
activeModal: 'new-workspace-composer',
|
||||
settings: { defaultTuiAgent: null, disabledTuiAgents: [] },
|
||||
updateSettings: vi.fn(),
|
||||
projects: [],
|
||||
repos: [],
|
||||
worktreesByRepo: {}
|
||||
}),
|
||||
{ getState: () => ({}) }
|
||||
)
|
||||
}))
|
||||
|
||||
vi.mock('@/components/contextual-tours/use-contextual-tour', () => ({
|
||||
useContextualTour: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/tooltip', () => ({
|
||||
Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
TooltipContent: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>
|
||||
}))
|
||||
|
||||
vi.mock('@/components/agent/AgentCombobox', () => ({
|
||||
default: () => <button type="button">Agent picker</button>
|
||||
}))
|
||||
|
||||
vi.mock('@/components/sidebar/AddRemoteHostDialog', () => ({
|
||||
AddRemoteHostDialog: () => null
|
||||
}))
|
||||
|
||||
vi.mock('@/components/new-workspace/SmartWorkspaceNameField', () => ({
|
||||
default: () => <input aria-label="workspace name" />
|
||||
}))
|
||||
|
||||
vi.mock('@/components/new-workspace/ProjectCombobox', () => ({
|
||||
default: () => <div data-testid="project-combobox" />
|
||||
}))
|
||||
|
||||
// Why: the picker owns its own test; here it only has to report its value and emit picks.
|
||||
vi.mock('@/components/repo/CreateFromPicker', () => ({
|
||||
CreateFromPicker: ({
|
||||
value,
|
||||
onValueChange
|
||||
}: {
|
||||
value: string
|
||||
onValueChange: (next: string) => void
|
||||
}) => (
|
||||
<div data-testid="base-ref-picker" data-value={value}>
|
||||
<button type="button" onClick={() => onValueChange('release/1.2')}>
|
||||
Pick release
|
||||
</button>
|
||||
<button type="button" onClick={() => onValueChange('')}>
|
||||
Pick project default
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
|
||||
function renderCard(
|
||||
overrides: Partial<React.ComponentProps<typeof NewWorkspaceComposerCard>> = {}
|
||||
): HTMLDivElement {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
act(() => {
|
||||
createRoot(container).render(
|
||||
<NewWorkspaceComposerCard
|
||||
quickAgent={null}
|
||||
onQuickAgentChange={() => {}}
|
||||
eligibleRepos={[]}
|
||||
repoId="repo-a"
|
||||
selectedRepoIsGit
|
||||
onRepoChange={() => {}}
|
||||
onProjectChange={() => {}}
|
||||
primaryActionLabel="Create workspace"
|
||||
name=""
|
||||
onNameValueChange={() => {}}
|
||||
branchNameOverride={undefined}
|
||||
onBranchNameOverrideChange={() => {}}
|
||||
onSmartGitHubItemSelect={() => {}}
|
||||
onSmartGitLabItemSelect={() => {}}
|
||||
onSmartBranchSelect={() => {}}
|
||||
onSmartLinearIssueSelect={() => {}}
|
||||
smartNameSelection={{ kind: 'jira', label: 'ERP-1491' }}
|
||||
onClearSmartNameSelection={() => {}}
|
||||
canReuseSelectedBranch={false}
|
||||
reuseSelectedBranch={false}
|
||||
onReuseSelectedBranchChange={() => {}}
|
||||
forkPushWarning={null}
|
||||
detectedAgentIds={null}
|
||||
onOpenAgentSettings={() => {}}
|
||||
advancedOpen={false}
|
||||
onToggleAdvanced={() => {}}
|
||||
parentWorktreeId={null}
|
||||
onParentWorktreeIdChange={() => {}}
|
||||
createDisabled={false}
|
||||
projectError={null}
|
||||
creating={false}
|
||||
onCreate={() => {}}
|
||||
note=""
|
||||
onNoteChange={() => {}}
|
||||
setupConfig={null}
|
||||
requiresExplicitSetupChoice={false}
|
||||
setupDecision={null}
|
||||
onSetupDecisionChange={() => {}}
|
||||
setupAgentStartupPolicy="start-immediately"
|
||||
onSetupAgentStartupPolicyChange={() => {}}
|
||||
shouldWaitForSetupCheck={false}
|
||||
resolvedSetupDecision={null}
|
||||
createError={null}
|
||||
selectedRepoConnectionId={null}
|
||||
selectedRepoSshStatus={null}
|
||||
selectedRepoRequiresConnection={false}
|
||||
selectedRepoConnectInProgress={false}
|
||||
onConnectSelectedRepo={async () => {}}
|
||||
canUseSparseCheckout={false}
|
||||
sparsePresets={[]}
|
||||
sparseSelectedPresetId={null}
|
||||
onSparseSelectPreset={() => {}}
|
||||
branchesEnabled
|
||||
setupControlsEnabled={false}
|
||||
sparseControlsEnabled={false}
|
||||
baseBranch={undefined}
|
||||
onBaseBranchChange={() => {}}
|
||||
startFromResetHint={null}
|
||||
{...overrides}
|
||||
/>
|
||||
)
|
||||
})
|
||||
return container
|
||||
}
|
||||
|
||||
function clickButton(container: HTMLDivElement, label: string): void {
|
||||
const button = [...container.querySelectorAll<HTMLButtonElement>('button')].find(
|
||||
(candidate) => candidate.textContent === label
|
||||
)
|
||||
act(() => button?.click())
|
||||
}
|
||||
|
||||
describe('NewWorkspaceComposerCard start from', () => {
|
||||
let container: HTMLDivElement | null = null
|
||||
|
||||
afterEach(() => {
|
||||
container?.remove()
|
||||
container = null
|
||||
})
|
||||
|
||||
it('offers a base ref while a Jira issue names the workspace', () => {
|
||||
container = renderCard()
|
||||
|
||||
expect(container.querySelector('[data-testid="base-ref-picker"]')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('reports the picked ref to the composer', () => {
|
||||
const picks: (string | undefined)[] = []
|
||||
container = renderCard({ onBaseBranchChange: (next) => picks.push(next) })
|
||||
|
||||
clickButton(container, 'Pick release')
|
||||
|
||||
expect(picks).toEqual(['release/1.2'])
|
||||
})
|
||||
|
||||
it('reports the project default as no base at all', () => {
|
||||
const picks: (string | undefined)[] = []
|
||||
container = renderCard({
|
||||
baseBranch: 'release/1.2',
|
||||
onBaseBranchChange: (next) => picks.push(next)
|
||||
})
|
||||
|
||||
clickButton(container, 'Pick project default')
|
||||
|
||||
expect(picks).toEqual([undefined])
|
||||
})
|
||||
|
||||
// Why: picking a base clears reuse, so offering one here would silently turn a checkout of
|
||||
// the picked branch into a new branch off something else.
|
||||
it('omits the base ref for a branch source, which already is the base', () => {
|
||||
container = renderCard({
|
||||
smartNameSelection: { kind: 'branch', label: 'feature/export-v2' },
|
||||
baseBranch: 'feature/export-v2'
|
||||
})
|
||||
|
||||
expect(container.querySelector('[data-testid="base-ref-picker"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('offers the base ref while a plain typed name owns the field', () => {
|
||||
container = renderCard({ smartNameSelection: null, name: 'my-own-name' })
|
||||
|
||||
expect(container.querySelector('[data-testid="base-ref-picker"]')).toBeTruthy()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['github-pr', { kind: 'github-pr' as const, label: '#42 Fix' }],
|
||||
['gitlab-mr', { kind: 'gitlab-mr' as const, label: '!42 Fix' }]
|
||||
])(
|
||||
'omits the base ref for a %s source that carries its own base',
|
||||
(_label, smartNameSelection) => {
|
||||
container = renderCard({ smartNameSelection })
|
||||
|
||||
expect(container.querySelector('[data-testid="base-ref-picker"]')).toBeNull()
|
||||
}
|
||||
)
|
||||
|
||||
it('omits the base ref when branches are disabled', () => {
|
||||
container = renderCard({ branchesEnabled: false })
|
||||
|
||||
expect(container.querySelector('[data-testid="base-ref-picker"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('surfaces the reset hint left by a project switch', () => {
|
||||
container = renderCard({ startFromResetHint: 'was origin/main' })
|
||||
|
||||
expect(container.textContent).toContain('was origin/main')
|
||||
})
|
||||
})
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Info } from 'lucide-react'
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { CreateFromPicker } from '@/components/repo/CreateFromPicker'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { AutomationWorkspaceMode } from '../../../../shared/automations-types'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
import { AUTOMATION_EDITOR_SECTION_LABEL_CLASS, Field } from './automation-page-parts'
|
||||
import { CreateFromPicker } from './CreateFromPicker'
|
||||
import { WorkspaceCombobox } from './WorkspaceCombobox'
|
||||
import type { AutomationDraft } from './AutomationEditorDialog'
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import { CreateFromPicker } from '@/components/repo/CreateFromPicker'
|
||||
import { useRepoMap, useWorktreesForRepo } from '@/store/selectors'
|
||||
|
||||
type ComposerBaseRefPickerProps = {
|
||||
repoId: string
|
||||
baseBranch: string | undefined
|
||||
onBaseBranchChange: (value: string | undefined) => void
|
||||
resetHint: string | null | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Base ref control for the New Workspace composer.
|
||||
*
|
||||
* Owns its own store reads so the name section stays presentational and the
|
||||
* worktree subscription only exists while the picker is actually on screen.
|
||||
*/
|
||||
export function ComposerBaseRefPicker({
|
||||
repoId,
|
||||
baseBranch,
|
||||
onBaseBranchChange,
|
||||
resetHint
|
||||
}: ComposerBaseRefPickerProps): React.JSX.Element {
|
||||
const repoMap = useRepoMap()
|
||||
const repoWorktrees = useWorktreesForRepo(repoId)
|
||||
return (
|
||||
<div className="space-y-1 pt-1">
|
||||
<CreateFromPicker
|
||||
// Why: branch search state is repo-scoped, so a project switch must drop it before the next paint.
|
||||
key={repoId}
|
||||
repoId={repoId}
|
||||
repoMap={repoMap}
|
||||
worktrees={repoWorktrees}
|
||||
value={baseBranch ?? ''}
|
||||
onValueChange={(nextBaseBranch) => onBaseBranchChange(nextBaseBranch || undefined)}
|
||||
/>
|
||||
{resetHint ? <p className="text-[11px] text-muted-foreground">{resetHint}</p> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ComposerBaseRefPicker
|
||||
@@ -3,6 +3,8 @@ import { AlertTriangle, Check } from 'lucide-react'
|
||||
import SmartWorkspaceNameField from '@/components/new-workspace/SmartWorkspaceNameField'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { shouldShowComposerBaseRefPicker } from './base-ref-picker-visibility'
|
||||
import { ComposerBaseRefPicker } from './ComposerBaseRefPicker'
|
||||
import type { NewWorkspaceComposerCardProps } from './new-workspace-composer-card-props'
|
||||
|
||||
type NewWorkspaceComposerNameSectionProps = Pick<
|
||||
@@ -35,6 +37,9 @@ type NewWorkspaceComposerNameSectionProps = Pick<
|
||||
| 'canReuseSelectedBranch'
|
||||
| 'reuseSelectedBranch'
|
||||
| 'onReuseSelectedBranchChange'
|
||||
| 'baseBranch'
|
||||
| 'onBaseBranchChange'
|
||||
| 'startFromResetHint'
|
||||
> & {
|
||||
onNamePlainEnter: () => void
|
||||
}
|
||||
@@ -68,8 +73,18 @@ export function NewWorkspaceComposerNameSection({
|
||||
forkPushWarning,
|
||||
canReuseSelectedBranch,
|
||||
reuseSelectedBranch,
|
||||
onReuseSelectedBranchChange
|
||||
onReuseSelectedBranchChange,
|
||||
baseBranch,
|
||||
onBaseBranchChange,
|
||||
startFromResetHint
|
||||
}: NewWorkspaceComposerNameSectionProps): React.JSX.Element {
|
||||
const showBaseRefPicker =
|
||||
Boolean(onBaseBranchChange) &&
|
||||
shouldShowComposerBaseRefPicker({
|
||||
selectedRepoIsGit,
|
||||
branchesEnabled,
|
||||
smartNameSelectionKind: smartNameSelection?.kind ?? null
|
||||
})
|
||||
return (
|
||||
<div className="min-w-0 space-y-1" data-contextual-tour-target="workspace-creation-name">
|
||||
<label className="block min-w-0 truncate text-xs font-medium text-muted-foreground">
|
||||
@@ -120,6 +135,14 @@ export function NewWorkspaceComposerNameSection({
|
||||
<span>{forkPushWarning}</span>
|
||||
</p>
|
||||
) : null}
|
||||
{showBaseRefPicker && onBaseBranchChange ? (
|
||||
<ComposerBaseRefPicker
|
||||
repoId={repoId}
|
||||
baseBranch={baseBranch}
|
||||
onBaseBranchChange={onBaseBranchChange}
|
||||
resetHint={startFromResetHint}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={cn(
|
||||
'grid overflow-hidden transition-[grid-template-rows] duration-200 ease-out',
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { shouldShowComposerBaseRefPicker } from './base-ref-picker-visibility'
|
||||
|
||||
const gitRepo = { selectedRepoIsGit: true, branchesEnabled: true }
|
||||
|
||||
describe('shouldShowComposerBaseRefPicker', () => {
|
||||
it('offers a base ref when no source is selected yet', () => {
|
||||
expect(shouldShowComposerBaseRefPicker({ ...gitRepo, 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)
|
||||
}
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
)
|
||||
|
||||
it('hides the base ref for a non-git project', () => {
|
||||
expect(
|
||||
shouldShowComposerBaseRefPicker({
|
||||
selectedRepoIsGit: false,
|
||||
branchesEnabled: true,
|
||||
smartNameSelectionKind: 'jira'
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('hides the base ref when branches are disabled, as on a folder workspace', () => {
|
||||
expect(
|
||||
shouldShowComposerBaseRefPicker({
|
||||
selectedRepoIsGit: true,
|
||||
branchesEnabled: false,
|
||||
smartNameSelectionKind: 'jira'
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { SmartWorkspaceNameSelection } from '@/components/new-workspace/SmartWorkspaceNameField'
|
||||
|
||||
/**
|
||||
* Whether the composer offers a base ref for the worktree it is about to create.
|
||||
*
|
||||
* The name field's tabs pick how the workspace is NAMED; the base ref is a separate
|
||||
* decision, so the picker stays available for every naming source — except those that
|
||||
* already carry a base of their own.
|
||||
*/
|
||||
export function shouldShowComposerBaseRefPicker(args: {
|
||||
selectedRepoIsGit: boolean
|
||||
branchesEnabled: boolean
|
||||
smartNameSelectionKind: SmartWorkspaceNameSelection['kind'] | null
|
||||
}): boolean {
|
||||
if (!args.selectedRepoIsGit || !args.branchesEnabled) {
|
||||
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 (
|
||||
args.smartNameSelectionKind !== 'github-pr' &&
|
||||
args.smartNameSelectionKind !== 'gitlab-mr' &&
|
||||
args.smartNameSelectionKind !== 'branch'
|
||||
)
|
||||
}
|
||||
@@ -62,6 +62,9 @@ export type NewWorkspaceComposerCardProps = {
|
||||
onNameValueChange: (value: string) => void
|
||||
branchNameOverride: string | undefined
|
||||
onBranchNameOverrideChange: (value: string | undefined) => void
|
||||
baseBranch?: string
|
||||
onBaseBranchChange?: (value: string | undefined) => void
|
||||
startFromResetHint?: string | null
|
||||
parentWorktreeId?: string | null
|
||||
onParentWorktreeIdChange?: (value: string | null) => void
|
||||
selectedRepoExecutionHostId?: ExecutionHostId | null
|
||||
|
||||
+35
-1
@@ -11,7 +11,19 @@ import {
|
||||
} from '@/runtime/runtime-repo-client'
|
||||
|
||||
vi.mock('@/components/ui/popover', () => ({
|
||||
Popover: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
// Why: the real popover owns open state; expose onOpenChange so a test can open the list.
|
||||
Popover: ({
|
||||
children,
|
||||
onOpenChange
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) => (
|
||||
<div>
|
||||
<button type="button" data-testid="open-popover" onClick={() => onOpenChange?.(true)} />
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
PopoverContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
PopoverTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>
|
||||
}))
|
||||
@@ -101,6 +113,28 @@ describe('CreateFromPicker host routing', () => {
|
||||
)
|
||||
})
|
||||
|
||||
// Why: the composer's Branch tab already lists on an empty query; a minimum-length gate left
|
||||
// this picker showing only the default ref and branches that happen to have a worktree.
|
||||
it('lists the repo branches as soon as it opens, with no query typed', async () => {
|
||||
const repo = makeRepo({ executionHostId: 'local' })
|
||||
storeState.repos = [repo]
|
||||
|
||||
await renderPicker(repo)
|
||||
await act(async () => {
|
||||
container.querySelector<HTMLButtonElement>('[data-testid="open-popover"]')?.click()
|
||||
})
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 250))
|
||||
})
|
||||
|
||||
expect(searchRuntimeRepoBaseRefs).toHaveBeenCalledWith(
|
||||
{ activeRuntimeEnvironmentId: null },
|
||||
repo.id,
|
||||
'',
|
||||
30
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps an explicit local repo on the local client even when a runtime is focused', async () => {
|
||||
const repo = makeRepo({ executionHostId: 'local' })
|
||||
storeState.repos = [repo]
|
||||
+3
-2
@@ -1,4 +1,3 @@
|
||||
/* oxlint-disable react-doctor/no-adjust-state-on-prop-change -- Why: picker base-ref defaults and search results come from debounced runtime IPC, so loading/result state is intentionally synchronized from effects. */
|
||||
import React from 'react'
|
||||
import { Check, ChevronsUpDown } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -144,7 +143,9 @@ export function CreateFromPicker({
|
||||
return
|
||||
}
|
||||
const trimmedQuery = query.trim()
|
||||
if (!open || !repoId || trimmedQuery.length < 2) {
|
||||
// Why: an empty query lists the repo's branches, as the composer's Branch tab already does;
|
||||
// a minimum length left this picker showing only the default and worktree branches.
|
||||
if (!open || !repoId) {
|
||||
setSearchResults([])
|
||||
setIsSearching(false)
|
||||
return
|
||||
@@ -0,0 +1,95 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { useBranchStartPointActions } from './branch-start-point-actions'
|
||||
|
||||
type Input = Parameters<typeof useBranchStartPointActions>[0]
|
||||
|
||||
function createInput(overrides: Partial<Input> = {}): Input {
|
||||
return {
|
||||
applyLinkedGitLabWorkItem: vi.fn(),
|
||||
applyLinkedWorkItem: vi.fn(),
|
||||
baseBranch: undefined,
|
||||
baseBranchNamesWorkspace: true,
|
||||
branchAutoNameRef: { current: 'alice/sta-42-fix-export' },
|
||||
handleRepoChange: vi.fn(),
|
||||
initialProjectGroupAppliedRef: { current: false },
|
||||
lastAutoNoteRef: { current: '' },
|
||||
noteRef: { current: '' },
|
||||
setBaseBranch: vi.fn(),
|
||||
setBaseBranchNamesWorkspace: vi.fn(),
|
||||
setBranchNameOverride: vi.fn(),
|
||||
setBranchNameOverridePreservesNameEdits: vi.fn(),
|
||||
setCompareBaseRef: vi.fn(),
|
||||
setForkPushWarning: vi.fn(),
|
||||
setNote: vi.fn(),
|
||||
setProjectError: vi.fn(),
|
||||
setSelectedProjectGroupId: vi.fn(),
|
||||
setPushTarget: vi.fn(),
|
||||
setReuseEligibleBranch: vi.fn(),
|
||||
setReuseSelectedBranch: vi.fn(),
|
||||
setSparseDirectories: vi.fn(),
|
||||
setSparseEnabled: vi.fn(),
|
||||
setSparseSelectedPresetId: vi.fn(),
|
||||
setStartFromResetHint: vi.fn(),
|
||||
smartGitHubPrStartPointSelectionRef: { current: null },
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('composer base changes', () => {
|
||||
it('marks a PR head as source-owned after replacing an independent base', () => {
|
||||
const input = createInput({ baseBranch: 'release/1.2', baseBranchNamesWorkspace: false })
|
||||
const { result } = renderHook(() => useBranchStartPointActions(input))
|
||||
|
||||
act(() =>
|
||||
result.current.handleBaseBranchPrSelect('refs/pull/42/head', {
|
||||
id: 'pr-42',
|
||||
type: 'pr',
|
||||
number: 42,
|
||||
title: 'Fix export',
|
||||
state: 'open',
|
||||
url: 'https://github.com/o/r/pull/42',
|
||||
labels: [],
|
||||
updatedAt: '',
|
||||
author: null,
|
||||
repoId: 'repo-1'
|
||||
})
|
||||
)
|
||||
|
||||
expect(input.setBaseBranch).toHaveBeenCalledWith('refs/pull/42/head')
|
||||
expect(input.setBaseBranchNamesWorkspace).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ baseBranch: undefined, baseBranchNamesWorkspace: true },
|
||||
{ baseBranch: 'release/1.2', baseBranchNamesWorkspace: false }
|
||||
])('preserves independent branch naming state from %j', (previous) => {
|
||||
const input = createInput(previous)
|
||||
const { result } = renderHook(() => useBranchStartPointActions(input))
|
||||
|
||||
act(() => result.current.handleBaseBranchChange('release/2.0'))
|
||||
act(() => result.current.handleBaseBranchChange(undefined))
|
||||
|
||||
expect(input.setBaseBranch).toHaveBeenNthCalledWith(1, 'release/2.0')
|
||||
expect(input.setBaseBranch).toHaveBeenNthCalledWith(2, undefined)
|
||||
expect(input.setBaseBranchNamesWorkspace).toHaveBeenCalledWith(false)
|
||||
expect(input.setBranchNameOverride).not.toHaveBeenCalled()
|
||||
expect(input.setBranchNameOverridePreservesNameEdits).not.toHaveBeenCalled()
|
||||
expect(input.branchAutoNameRef.current).toBe('alice/sta-42-fix-export')
|
||||
})
|
||||
|
||||
it('still drops branch reuse and its pinned name when replacing a source-owned base', () => {
|
||||
const input = createInput({ baseBranch: 'feature/existing' })
|
||||
const { result } = renderHook(() => useBranchStartPointActions(input))
|
||||
|
||||
act(() => result.current.handleBaseBranchChange('release/2.0'))
|
||||
|
||||
expect(input.setBranchNameOverride).toHaveBeenCalledWith(undefined)
|
||||
expect(input.setBranchNameOverridePreservesNameEdits).toHaveBeenCalledWith(false)
|
||||
expect(input.setReuseEligibleBranch).toHaveBeenCalledWith(null)
|
||||
expect(input.setReuseSelectedBranch).toHaveBeenCalledWith(false)
|
||||
expect(input.branchAutoNameRef.current).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -4,12 +4,15 @@ type BranchStartPointActionsInput = Pick<
|
||||
ComposerModel,
|
||||
| 'applyLinkedGitLabWorkItem'
|
||||
| 'applyLinkedWorkItem'
|
||||
| 'baseBranch'
|
||||
| 'baseBranchNamesWorkspace'
|
||||
| 'branchAutoNameRef'
|
||||
| 'handleRepoChange'
|
||||
| 'initialProjectGroupAppliedRef'
|
||||
| 'lastAutoNoteRef'
|
||||
| 'noteRef'
|
||||
| 'setBaseBranch'
|
||||
| 'setBaseBranchNamesWorkspace'
|
||||
| 'setBranchNameOverride'
|
||||
| 'setBranchNameOverridePreservesNameEdits'
|
||||
| 'setCompareBaseRef'
|
||||
@@ -38,12 +41,15 @@ export function useBranchStartPointActions(input: BranchStartPointActionsInput)
|
||||
const {
|
||||
applyLinkedGitLabWorkItem,
|
||||
applyLinkedWorkItem,
|
||||
baseBranch,
|
||||
baseBranchNamesWorkspace,
|
||||
branchAutoNameRef,
|
||||
handleRepoChange,
|
||||
initialProjectGroupAppliedRef,
|
||||
lastAutoNoteRef,
|
||||
noteRef,
|
||||
setBaseBranch,
|
||||
setBaseBranchNamesWorkspace,
|
||||
setBranchNameOverride,
|
||||
setBranchNameOverridePreservesNameEdits,
|
||||
setCompareBaseRef,
|
||||
@@ -91,20 +97,26 @@ export function useBranchStartPointActions(input: BranchStartPointActionsInput)
|
||||
(next: string | undefined): void => {
|
||||
smartGitHubPrStartPointSelectionRef.current = null
|
||||
setBaseBranch(next)
|
||||
setBaseBranchNamesWorkspace(false)
|
||||
setCompareBaseRef(undefined)
|
||||
setPushTarget(undefined)
|
||||
setBranchNameOverride(undefined)
|
||||
// Why (#5181): Start-from means "new branch from this base", so it never reuses — clear reuse state from a prior smart-field branch pick.
|
||||
setBranchNameOverridePreservesNameEdits(false)
|
||||
// Only source-owned refs pin a name that changing the base invalidates.
|
||||
if (baseBranch && baseBranchNamesWorkspace) {
|
||||
setBranchNameOverride(undefined)
|
||||
setBranchNameOverridePreservesNameEdits(false)
|
||||
branchAutoNameRef.current = ''
|
||||
}
|
||||
setReuseEligibleBranch(null)
|
||||
setReuseSelectedBranch(false)
|
||||
setForkPushWarning(null)
|
||||
branchAutoNameRef.current = ''
|
||||
setStartFromResetHint(null)
|
||||
},
|
||||
[
|
||||
baseBranch,
|
||||
baseBranchNamesWorkspace,
|
||||
branchAutoNameRef,
|
||||
setBaseBranch,
|
||||
setBaseBranchNamesWorkspace,
|
||||
setBranchNameOverride,
|
||||
setBranchNameOverridePreservesNameEdits,
|
||||
setCompareBaseRef,
|
||||
@@ -126,6 +138,7 @@ export function useBranchStartPointActions(input: BranchStartPointActionsInput)
|
||||
nextCompareBaseRef?: string
|
||||
): void => {
|
||||
setBaseBranch(nextBaseBranch)
|
||||
setBaseBranchNamesWorkspace(true)
|
||||
setCompareBaseRef(nextCompareBaseRef)
|
||||
setPushTarget(nextPushTarget)
|
||||
setBranchNameOverride(nextBranchNameOverride)
|
||||
@@ -151,6 +164,7 @@ export function useBranchStartPointActions(input: BranchStartPointActionsInput)
|
||||
lastAutoNoteRef,
|
||||
noteRef,
|
||||
setBaseBranch,
|
||||
setBaseBranchNamesWorkspace,
|
||||
setBranchNameOverride,
|
||||
setBranchNameOverridePreservesNameEdits,
|
||||
setCompareBaseRef,
|
||||
@@ -169,6 +183,7 @@ export function useBranchStartPointActions(input: BranchStartPointActionsInput)
|
||||
nextCompareBaseRef?: string
|
||||
): void => {
|
||||
setBaseBranch(nextBaseBranch)
|
||||
setBaseBranchNamesWorkspace(true)
|
||||
setCompareBaseRef(nextCompareBaseRef)
|
||||
setPushTarget(nextPushTarget)
|
||||
setBranchNameOverride(undefined)
|
||||
@@ -190,6 +205,7 @@ export function useBranchStartPointActions(input: BranchStartPointActionsInput)
|
||||
lastAutoNoteRef,
|
||||
noteRef,
|
||||
setBaseBranch,
|
||||
setBaseBranchNamesWorkspace,
|
||||
setBranchNameOverride,
|
||||
setCompareBaseRef,
|
||||
setNote,
|
||||
|
||||
@@ -85,6 +85,7 @@ export function useComposerExternalSync(target: ComposerTargetState): ComposerEx
|
||||
selectedRepoGitHubSourceContext: target.sourceContextState.selectedRepoGitHubSourceContext,
|
||||
selectedRepoIsGit: target.runtimeTargetSelection.selectedRepoIsGit,
|
||||
setBaseBranch: target.workspaceIdentityState.setBaseBranch,
|
||||
setBaseBranchNamesWorkspace: target.workspaceIdentityState.setBaseBranchNamesWorkspace,
|
||||
setBranchNameOverride: target.workspaceIdentityState.setBranchNameOverride,
|
||||
setBranchNameOverridePreservesNameEdits:
|
||||
target.workspaceIdentityState.setBranchNameOverridePreservesNameEdits,
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { useIssueSourceActions } from './issue-source-actions'
|
||||
import { resolveDraftBaseBranchNamesWorkspace } from './workspace-identity-state'
|
||||
import { useGitHubProviderSelection } from './github-provider-selection'
|
||||
|
||||
type Input = Parameters<typeof useIssueSourceActions>[0]
|
||||
|
||||
function createInput(overrides: Partial<Input> = {}): Input {
|
||||
return {
|
||||
baseBranch: undefined,
|
||||
baseBranchNamesWorkspace: true,
|
||||
branchAutoNameRef: { current: '' },
|
||||
isProjectGroupTarget: false,
|
||||
lastAutoNameRef: { current: '' },
|
||||
lastAutoNoteRef: { current: '' },
|
||||
linkedWorkItem: null,
|
||||
name: '',
|
||||
noteRef: { current: '' },
|
||||
setBaseBranch: vi.fn(),
|
||||
setBranchNameOverride: vi.fn(),
|
||||
setBranchNameOverridePreservesNameEdits: vi.fn(),
|
||||
setCompareBaseRef: vi.fn(),
|
||||
setForkPushWarning: vi.fn(),
|
||||
setLinkedGitLabIssue: vi.fn(),
|
||||
setLinkedGitLabMR: vi.fn(),
|
||||
setLinkedIssue: vi.fn(),
|
||||
setLinkedPR: vi.fn(),
|
||||
setLinkedTaskSourceContext: vi.fn(),
|
||||
setLinkedWorkItem: vi.fn(),
|
||||
setName: vi.fn(),
|
||||
setNote: vi.fn(),
|
||||
setPushTarget: vi.fn(),
|
||||
setReuseEligibleBranch: vi.fn(),
|
||||
setReuseSelectedBranch: vi.fn(),
|
||||
setStartFromResetHint: vi.fn(),
|
||||
smartGitHubPrStartPointSelectionRef: { current: null },
|
||||
...overrides
|
||||
} as Input
|
||||
}
|
||||
|
||||
describe('composer name source selection', () => {
|
||||
it.each([false, true])('clears only source-owned bases when selecting Jira (%s)', (owned) => {
|
||||
const input = createInput({ baseBranch: 'release/1.2', baseBranchNamesWorkspace: owned })
|
||||
const { result } = renderHook(() => useIssueSourceActions(input))
|
||||
|
||||
act(() =>
|
||||
result.current.handleSmartJiraIssueSelect(
|
||||
{
|
||||
id: '42',
|
||||
key: 'APP-42',
|
||||
title: 'Fix export',
|
||||
url: 'https://jira.example/browse/APP-42',
|
||||
project: { id: 'app', key: 'APP', name: 'App' },
|
||||
issueType: { id: 'bug', name: 'Bug' },
|
||||
status: { id: 'open', name: 'Open', categoryKey: 'new', categoryName: 'To do' },
|
||||
labels: [],
|
||||
updatedAt: '',
|
||||
createdAt: ''
|
||||
},
|
||||
{ kind: 'task-source', provider: 'jira', projectId: 'app', hostId: 'local' }
|
||||
)
|
||||
)
|
||||
|
||||
expect(input.setBaseBranch).toHaveBeenCalledTimes(owned ? 1 : 0)
|
||||
if (owned) {
|
||||
expect(input.setBaseBranch).toHaveBeenCalledWith(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
it.each([false, true])('clears only source-owned bases when removing a source (%s)', (owned) => {
|
||||
const input = createInput({ baseBranch: 'release/1.2', baseBranchNamesWorkspace: owned })
|
||||
const { result } = renderHook(() => useIssueSourceActions(input))
|
||||
|
||||
act(() => result.current.handleClearSmartNameSelection())
|
||||
|
||||
expect(input.setBaseBranch).toHaveBeenCalledTimes(owned ? 1 : 0)
|
||||
if (owned) {
|
||||
expect(input.setBaseBranch).toHaveBeenCalledWith(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
it.each([false, true])(
|
||||
'clears only source-owned bases when selecting a GitHub issue (%s)',
|
||||
(owned) => {
|
||||
const input = {
|
||||
...createInput({ baseBranch: 'release/1.2', baseBranchNamesWorkspace: owned }),
|
||||
applyLinkedWorkItem: vi.fn(),
|
||||
eligibleRepos: [],
|
||||
handleBaseBranchPrSelect: vi.fn(),
|
||||
selectedRepo: undefined,
|
||||
selectedRepoGitHubSourceContext: null,
|
||||
settings: null
|
||||
}
|
||||
const { result } = renderHook(() => useGitHubProviderSelection(input))
|
||||
|
||||
act(() =>
|
||||
result.current.handleSmartGitHubItemSelect({
|
||||
id: 'issue-42',
|
||||
type: 'issue',
|
||||
number: 42,
|
||||
title: 'Fix export',
|
||||
state: 'open',
|
||||
url: 'https://github.com/o/r/issues/42',
|
||||
labels: [],
|
||||
updatedAt: '',
|
||||
author: null,
|
||||
repoId: 'repo-1'
|
||||
})
|
||||
)
|
||||
|
||||
expect(input.setBaseBranch).toHaveBeenCalledTimes(owned ? 1 : 0)
|
||||
if (owned) {
|
||||
expect(input.setBaseBranch).toHaveBeenCalledWith(undefined)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('names the workspace after a branch the name field picked', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useIssueSourceActions(
|
||||
createInput({ baseBranch: 'feature/export-v2', baseBranchNamesWorkspace: true })
|
||||
)
|
||||
)
|
||||
|
||||
expect(result.current.smartNameSelection).toEqual({
|
||||
kind: 'branch',
|
||||
label: 'feature/export-v2'
|
||||
})
|
||||
})
|
||||
|
||||
// Why: the field swaps the text input for a source pill, so a pill here would hide a typed name.
|
||||
it('claims no source for a base ref the composer picker chose, leaving a typed name on screen', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useIssueSourceActions(
|
||||
createInput({
|
||||
baseBranch: 'release/1.2',
|
||||
baseBranchNamesWorkspace: false,
|
||||
name: 'my-own-name'
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
expect(result.current.smartNameSelection).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps a linked issue as the source whatever the base ref', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useIssueSourceActions(
|
||||
createInput({
|
||||
baseBranch: 'release/1.2',
|
||||
baseBranchNamesWorkspace: false,
|
||||
linkedWorkItem: {
|
||||
type: 'issue',
|
||||
number: 42,
|
||||
title: 'Broken export',
|
||||
url: 'https://github.com/o/r/issues/42'
|
||||
} as Input['linkedWorkItem']
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
expect(result.current.smartNameSelection?.kind).toBe('github-issue')
|
||||
})
|
||||
})
|
||||
|
||||
describe('base ref intent across a draft round trip', () => {
|
||||
it('restores a base ref chosen in the picker as a base, not as a name source', () => {
|
||||
expect(resolveDraftBaseBranchNamesWorkspace({ persistDraft: true, draftValue: false })).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('restores a branch picked in the name field as a name source', () => {
|
||||
expect(resolveDraftBaseBranchNamesWorkspace({ persistDraft: true, draftValue: true })).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('treats a draft written before the flag existed as a name source', () => {
|
||||
expect(
|
||||
resolveDraftBaseBranchNamesWorkspace({ persistDraft: true, draftValue: undefined })
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('ignores any stored intent when the composer does not persist drafts', () => {
|
||||
expect(resolveDraftBaseBranchNamesWorkspace({ persistDraft: false, draftValue: false })).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -132,6 +132,8 @@ export function useComposerSourceState(
|
||||
workspaceHostScope: target.composerTargetStore.workspaceHostScope
|
||||
})
|
||||
const branchStartPointActions = useBranchStartPointActions({
|
||||
baseBranch: target.workspaceIdentityState.baseBranch,
|
||||
baseBranchNamesWorkspace: target.workspaceIdentityState.baseBranchNamesWorkspace,
|
||||
applyLinkedGitLabWorkItem: sourceIdentityActions.applyLinkedGitLabWorkItem,
|
||||
applyLinkedWorkItem: external.githubSourceApplication.applyLinkedWorkItem,
|
||||
branchAutoNameRef: target.asyncComposerState.branchAutoNameRef,
|
||||
@@ -140,6 +142,7 @@ export function useComposerSourceState(
|
||||
lastAutoNoteRef: target.asyncComposerState.lastAutoNoteRef,
|
||||
noteRef: target.asyncComposerState.noteRef,
|
||||
setBaseBranch: target.workspaceIdentityState.setBaseBranch,
|
||||
setBaseBranchNamesWorkspace: target.workspaceIdentityState.setBaseBranchNamesWorkspace,
|
||||
setBranchNameOverride: target.workspaceIdentityState.setBranchNameOverride,
|
||||
setBranchNameOverridePreservesNameEdits:
|
||||
target.workspaceIdentityState.setBranchNameOverridePreservesNameEdits,
|
||||
@@ -159,6 +162,7 @@ export function useComposerSourceState(
|
||||
target.asyncComposerState.smartGitHubPrStartPointSelectionRef
|
||||
})
|
||||
const githubProviderSelection = useGitHubProviderSelection({
|
||||
baseBranchNamesWorkspace: target.workspaceIdentityState.baseBranchNamesWorkspace,
|
||||
applyLinkedWorkItem: external.githubSourceApplication.applyLinkedWorkItem,
|
||||
branchAutoNameRef: target.asyncComposerState.branchAutoNameRef,
|
||||
eligibleRepos: target.composerTargetStore.eligibleRepos,
|
||||
@@ -220,6 +224,7 @@ export function useComposerSourceState(
|
||||
repoId: target.initialTargetState.repoId,
|
||||
reuseEligibleBranch: target.workspaceIdentityState.reuseEligibleBranch,
|
||||
setBaseBranch: target.workspaceIdentityState.setBaseBranch,
|
||||
setBaseBranchNamesWorkspace: target.workspaceIdentityState.setBaseBranchNamesWorkspace,
|
||||
setBranchNameOverride: target.workspaceIdentityState.setBranchNameOverride,
|
||||
setBranchNameOverridePreservesNameEdits:
|
||||
target.workspaceIdentityState.setBranchNameOverridePreservesNameEdits,
|
||||
@@ -236,6 +241,7 @@ export function useComposerSourceState(
|
||||
})
|
||||
const issueSourceActions = useIssueSourceActions({
|
||||
baseBranch: target.workspaceIdentityState.baseBranch,
|
||||
baseBranchNamesWorkspace: target.workspaceIdentityState.baseBranchNamesWorkspace,
|
||||
branchAutoNameRef: target.asyncComposerState.branchAutoNameRef,
|
||||
isProjectGroupTarget: target.runtimeTargetSelection.isProjectGroupTarget,
|
||||
lastAutoNameRef: target.asyncComposerState.lastAutoNameRef,
|
||||
|
||||
@@ -156,6 +156,7 @@ export function useComposerTargetState(
|
||||
agentPrompt: sourceContextState.agentPrompt,
|
||||
attachmentPaths: sourceContextState.attachmentPaths,
|
||||
baseBranch: workspaceIdentityState.baseBranch,
|
||||
baseBranchNamesWorkspace: workspaceIdentityState.baseBranchNamesWorkspace,
|
||||
compareBaseRef: workspaceIdentityState.compareBaseRef,
|
||||
eligibleRepos: composerTargetStore.eligibleRepos,
|
||||
fetchSparsePresets: composerTargetStore.fetchSparsePresets,
|
||||
|
||||
@@ -19,6 +19,7 @@ function createState(overrides: Partial<DraftTargetSyncInput> = {}): DraftTarget
|
||||
agentPrompt: 'Fix the issue',
|
||||
attachmentPaths: ['/tmp/context.txt'],
|
||||
baseBranch: 'main',
|
||||
baseBranchNamesWorkspace: true,
|
||||
compareBaseRef: 'origin/main',
|
||||
eligibleRepos: [],
|
||||
fetchSparsePresets: vi.fn<DraftTargetSyncInput['fetchSparsePresets']>(),
|
||||
@@ -78,6 +79,25 @@ describe('useDraftTargetSync', () => {
|
||||
expect(setRepoId).toHaveBeenCalledWith('repo-1')
|
||||
})
|
||||
|
||||
it('persists whether the base ref also names the workspace', () => {
|
||||
const setNewWorkspaceDraft = vi.fn<DraftTargetSyncInput['setNewWorkspaceDraft']>()
|
||||
const state = createState({
|
||||
eligibleRepos: [createRepo('repo-1')],
|
||||
baseBranch: 'release/2.1',
|
||||
baseBranchNamesWorkspace: false,
|
||||
setNewWorkspaceDraft
|
||||
})
|
||||
|
||||
renderHook(() => useDraftTargetSync(state))
|
||||
|
||||
expect(setNewWorkspaceDraft).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
baseBranch: 'release/2.1',
|
||||
baseBranchNamesWorkspace: false
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('does not persist or repair transient quick-composer state', () => {
|
||||
const state = createState({
|
||||
persistDraft: false,
|
||||
|
||||
@@ -6,6 +6,7 @@ export type DraftTargetSyncInput = Pick<
|
||||
| 'agentPrompt'
|
||||
| 'attachmentPaths'
|
||||
| 'baseBranch'
|
||||
| 'baseBranchNamesWorkspace'
|
||||
| 'compareBaseRef'
|
||||
| 'eligibleRepos'
|
||||
| 'fetchSparsePresets'
|
||||
@@ -36,6 +37,7 @@ export function useDraftTargetSync(state: DraftTargetSyncInput): void {
|
||||
agentPrompt,
|
||||
attachmentPaths,
|
||||
baseBranch,
|
||||
baseBranchNamesWorkspace,
|
||||
compareBaseRef,
|
||||
eligibleRepos,
|
||||
fetchSparsePresets,
|
||||
@@ -98,7 +100,7 @@ export function useDraftTargetSync(state: DraftTargetSyncInput): void {
|
||||
linkedPR,
|
||||
linkedGitLabIssue,
|
||||
linkedGitLabMR,
|
||||
...(baseBranch !== undefined ? { baseBranch } : {}),
|
||||
...(baseBranch !== undefined ? { baseBranch, baseBranchNamesWorkspace } : {}),
|
||||
...(compareBaseRef !== undefined ? { compareBaseRef } : {})
|
||||
})
|
||||
}, [
|
||||
@@ -106,6 +108,7 @@ export function useDraftTargetSync(state: DraftTargetSyncInput): void {
|
||||
agentPrompt,
|
||||
attachmentPaths,
|
||||
baseBranch,
|
||||
baseBranchNamesWorkspace,
|
||||
compareBaseRef,
|
||||
linkedIssue,
|
||||
linkedPR,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ComposerModel } from './composer-model'
|
||||
type GitHubProviderSelectionInput = Pick<
|
||||
ComposerModel,
|
||||
| 'applyLinkedWorkItem'
|
||||
| 'baseBranchNamesWorkspace'
|
||||
| 'branchAutoNameRef'
|
||||
| 'eligibleRepos'
|
||||
| 'handleBaseBranchPrSelect'
|
||||
@@ -47,6 +48,7 @@ import type { SmartGitHubPrStartPointSelection } from './source-selection-decisi
|
||||
export function useGitHubProviderSelection(input: GitHubProviderSelectionInput) {
|
||||
const {
|
||||
applyLinkedWorkItem,
|
||||
baseBranchNamesWorkspace,
|
||||
branchAutoNameRef,
|
||||
eligibleRepos,
|
||||
handleBaseBranchPrSelect,
|
||||
@@ -112,7 +114,9 @@ export function useGitHubProviderSelection(input: GitHubProviderSelectionInput)
|
||||
const runRepo = selectedRepo ?? eligibleRepos.find((repo) => repo.id === item.repoId)
|
||||
applyLinkedWorkItem(normalizedItem)
|
||||
if (identity.type !== 'pr' || !runRepo) {
|
||||
setBaseBranch(undefined)
|
||||
if (identity.type === 'pr' || baseBranchNamesWorkspace) {
|
||||
setBaseBranch(undefined)
|
||||
}
|
||||
setCompareBaseRef(undefined)
|
||||
setPushTarget(undefined)
|
||||
return
|
||||
@@ -171,6 +175,7 @@ export function useGitHubProviderSelection(input: GitHubProviderSelectionInput)
|
||||
},
|
||||
[
|
||||
applyLinkedWorkItem,
|
||||
baseBranchNamesWorkspace,
|
||||
eligibleRepos,
|
||||
handleBaseBranchPrSelect,
|
||||
isProjectGroupTarget,
|
||||
|
||||
@@ -12,6 +12,7 @@ type GitHubSubmitResolutionInput = Pick<
|
||||
| 'selectedRepoGitHubSourceContext'
|
||||
| 'selectedRepoIsGit'
|
||||
| 'setBaseBranch'
|
||||
| 'setBaseBranchNamesWorkspace'
|
||||
| 'setBranchNameOverride'
|
||||
| 'setBranchNameOverridePreservesNameEdits'
|
||||
| 'setCompareBaseRef'
|
||||
@@ -62,6 +63,7 @@ export function useGitHubSubmitResolution(input: GitHubSubmitResolutionInput) {
|
||||
selectedRepoGitHubSourceContext,
|
||||
selectedRepoIsGit,
|
||||
setBaseBranch,
|
||||
setBaseBranchNamesWorkspace,
|
||||
setBranchNameOverride,
|
||||
setBranchNameOverridePreservesNameEdits,
|
||||
setCompareBaseRef,
|
||||
@@ -133,6 +135,7 @@ export function useGitHubSubmitResolution(input: GitHubSubmitResolutionInput) {
|
||||
: {})
|
||||
}
|
||||
setBaseBranch(selectedPrStartPoint.baseBranch)
|
||||
setBaseBranchNamesWorkspace(true)
|
||||
setCompareBaseRef(selectedPrStartPoint.compareBaseRef)
|
||||
setPushTarget(selectedPrStartPoint.pushTarget)
|
||||
if (selectedPrStartPoint.branchNameOverride) {
|
||||
@@ -234,6 +237,7 @@ export function useGitHubSubmitResolution(input: GitHubSubmitResolutionInput) {
|
||||
lastAutoNameRef.current = resolution.workspaceName
|
||||
if (prStartPoint) {
|
||||
setBaseBranch(prStartPoint.baseBranch)
|
||||
setBaseBranchNamesWorkspace(true)
|
||||
setCompareBaseRef(prStartPoint.compareBaseRef)
|
||||
setPushTarget(prStartPoint.pushTarget)
|
||||
if (prStartPoint.branchNameOverride) {
|
||||
@@ -263,6 +267,7 @@ export function useGitHubSubmitResolution(input: GitHubSubmitResolutionInput) {
|
||||
branchAutoNameRef,
|
||||
lastAutoNameRef,
|
||||
setBaseBranch,
|
||||
setBaseBranchNamesWorkspace,
|
||||
setBranchNameOverride,
|
||||
setBranchNameOverridePreservesNameEdits,
|
||||
setCompareBaseRef,
|
||||
|
||||
@@ -14,6 +14,8 @@ export type ComposerIdentityModel = {
|
||||
setLinkedGitLabMR: React.Dispatch<React.SetStateAction<number | null>>
|
||||
baseBranch: string | undefined
|
||||
setBaseBranch: React.Dispatch<React.SetStateAction<string | undefined>>
|
||||
baseBranchNamesWorkspace: boolean
|
||||
setBaseBranchNamesWorkspace: React.Dispatch<React.SetStateAction<boolean>>
|
||||
compareBaseRef: string | undefined
|
||||
setCompareBaseRef: React.Dispatch<React.SetStateAction<string | undefined>>
|
||||
branchNameOverride: string | undefined
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ComposerModel } from './composer-model'
|
||||
type IssueSourceActionsInput = Pick<
|
||||
ComposerModel,
|
||||
| 'baseBranch'
|
||||
| 'baseBranchNamesWorkspace'
|
||||
| 'branchAutoNameRef'
|
||||
| 'isProjectGroupTarget'
|
||||
| 'lastAutoNameRef'
|
||||
@@ -59,6 +60,7 @@ import type { TaskSourceContext } from '../../../../shared/task-source-context'
|
||||
export function useIssueSourceActions(input: IssueSourceActionsInput) {
|
||||
const {
|
||||
baseBranch,
|
||||
baseBranchNamesWorkspace,
|
||||
branchAutoNameRef,
|
||||
isProjectGroupTarget,
|
||||
lastAutoNameRef,
|
||||
@@ -161,7 +163,9 @@ export function useIssueSourceActions(input: IssueSourceActionsInput) {
|
||||
setLinkedPR(null)
|
||||
setLinkedGitLabIssue(null)
|
||||
setLinkedGitLabMR(null)
|
||||
setBaseBranch(undefined)
|
||||
if (baseBranchNamesWorkspace) {
|
||||
setBaseBranch(undefined)
|
||||
}
|
||||
setCompareBaseRef(undefined)
|
||||
setPushTarget(undefined)
|
||||
setBranchNameOverride(undefined)
|
||||
@@ -187,6 +191,7 @@ export function useIssueSourceActions(input: IssueSourceActionsInput) {
|
||||
},
|
||||
[
|
||||
name,
|
||||
baseBranchNamesWorkspace,
|
||||
branchAutoNameRef,
|
||||
lastAutoNameRef,
|
||||
setBaseBranch,
|
||||
@@ -213,7 +218,9 @@ export function useIssueSourceActions(input: IssueSourceActionsInput) {
|
||||
setLinkedGitLabMR(null)
|
||||
setLinkedWorkItem(null)
|
||||
setLinkedTaskSourceContext(null)
|
||||
setBaseBranch(undefined)
|
||||
if (baseBranchNamesWorkspace) {
|
||||
setBaseBranch(undefined)
|
||||
}
|
||||
setCompareBaseRef(undefined)
|
||||
setPushTarget(undefined)
|
||||
setBranchNameOverride(undefined)
|
||||
@@ -233,6 +240,7 @@ export function useIssueSourceActions(input: IssueSourceActionsInput) {
|
||||
}
|
||||
}, [
|
||||
name,
|
||||
baseBranchNamesWorkspace,
|
||||
branchAutoNameRef,
|
||||
lastAutoNameRef,
|
||||
lastAutoNoteRef,
|
||||
@@ -263,9 +271,11 @@ export function useIssueSourceActions(input: IssueSourceActionsInput) {
|
||||
}
|
||||
return buildWorkspaceSourceSelection({
|
||||
linkedWorkItem,
|
||||
baseBranch
|
||||
// Why: only a branch picked to NAME the workspace becomes a source pill; a base ref
|
||||
// chosen in the composer's own picker must leave a typed name on screen.
|
||||
baseBranch: baseBranchNamesWorkspace ? baseBranch : undefined
|
||||
}) as SmartWorkspaceNameSelection | null
|
||||
}, [baseBranch, isProjectGroupTarget, linkedWorkItem])
|
||||
}, [baseBranch, baseBranchNamesWorkspace, isProjectGroupTarget, linkedWorkItem])
|
||||
|
||||
return {
|
||||
handleSmartLinearIssueSelect,
|
||||
|
||||
@@ -63,6 +63,8 @@ function useSelectedSourceReset(
|
||||
)
|
||||
const source = useIssueSourceActions({
|
||||
baseBranch,
|
||||
// Why: a PR/issue/branch source owns the base, so "create more" must still clear it.
|
||||
baseBranchNamesWorkspace: true,
|
||||
branchAutoNameRef,
|
||||
isProjectGroupTarget,
|
||||
lastAutoNameRef,
|
||||
|
||||
@@ -8,6 +8,7 @@ type WorkItemSourceActionsInput = Pick<
|
||||
| 'repoId'
|
||||
| 'reuseEligibleBranch'
|
||||
| 'setBaseBranch'
|
||||
| 'setBaseBranchNamesWorkspace'
|
||||
| 'setBranchNameOverride'
|
||||
| 'setBranchNameOverridePreservesNameEdits'
|
||||
| 'setCompareBaseRef'
|
||||
@@ -35,6 +36,7 @@ export function useWorkItemSourceActions(input: WorkItemSourceActionsInput) {
|
||||
repoId,
|
||||
reuseEligibleBranch,
|
||||
setBaseBranch,
|
||||
setBaseBranchNamesWorkspace,
|
||||
setBranchNameOverride,
|
||||
setBranchNameOverridePreservesNameEdits,
|
||||
setCompareBaseRef,
|
||||
@@ -59,6 +61,7 @@ export function useWorkItemSourceActions(input: WorkItemSourceActionsInput) {
|
||||
worktreeBranches: getComposerRepoWorktreeBranches(worktreesByRepo[repoId] ?? [], repoId)
|
||||
})
|
||||
setBaseBranch(selection.baseBranch)
|
||||
setBaseBranchNamesWorkspace(true)
|
||||
setCompareBaseRef(undefined)
|
||||
setPushTarget(undefined)
|
||||
setStartFromResetHint(null)
|
||||
@@ -86,6 +89,7 @@ export function useWorkItemSourceActions(input: WorkItemSourceActionsInput) {
|
||||
branchAutoNameRef,
|
||||
lastAutoNameRef,
|
||||
setBaseBranch,
|
||||
setBaseBranchNamesWorkspace,
|
||||
setBranchNameOverride,
|
||||
setBranchNameOverridePreservesNameEdits,
|
||||
setCompareBaseRef,
|
||||
|
||||
@@ -27,6 +27,19 @@ import { filterEnabledTuiAgents, isTuiAgentEnabled } from '../../../../shared/tu
|
||||
import { getAgentCatalog } from '@/lib/agent-catalog'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
/**
|
||||
* Whether a restored draft's `baseBranch` still names the workspace.
|
||||
*
|
||||
* A draft written before the flag existed carries no intent, so it restores as a branch pick —
|
||||
* the behavior it had when it was saved.
|
||||
*/
|
||||
export function resolveDraftBaseBranchNamesWorkspace(args: {
|
||||
persistDraft: boolean
|
||||
draftValue: boolean | undefined
|
||||
}): boolean {
|
||||
return args.persistDraft ? (args.draftValue ?? true) : true
|
||||
}
|
||||
|
||||
export function useWorkspaceIdentityState(input: WorkspaceIdentityStateInput) {
|
||||
const {
|
||||
initialBaseBranch,
|
||||
@@ -91,6 +104,14 @@ export function useWorkspaceIdentityState(input: WorkspaceIdentityStateInput) {
|
||||
const [baseBranch, setBaseBranch] = useState<string | undefined>(
|
||||
persistDraft ? newWorkspaceDraft?.baseBranch : initialBaseBranch
|
||||
)
|
||||
// Why: a branch picked to name the workspace shows as a source pill; a base ref chosen in
|
||||
// the composer's own picker must not, or it would hide the name the user typed.
|
||||
const [baseBranchNamesWorkspace, setBaseBranchNamesWorkspace] = useState(() =>
|
||||
resolveDraftBaseBranchNamesWorkspace({
|
||||
persistDraft,
|
||||
draftValue: newWorkspaceDraft?.baseBranchNamesWorkspace
|
||||
})
|
||||
)
|
||||
|
||||
const [compareBaseRef, setCompareBaseRef] = useState<string | undefined>(
|
||||
persistDraft ? newWorkspaceDraft?.compareBaseRef : undefined
|
||||
@@ -193,6 +214,8 @@ export function useWorkspaceIdentityState(input: WorkspaceIdentityStateInput) {
|
||||
setLinkedGitLabMR,
|
||||
baseBranch,
|
||||
setBaseBranch,
|
||||
baseBranchNamesWorkspace,
|
||||
setBaseBranchNamesWorkspace,
|
||||
compareBaseRef,
|
||||
setCompareBaseRef,
|
||||
branchNameOverride,
|
||||
|
||||
@@ -99,6 +99,9 @@ export type NewWorkspaceDraft = {
|
||||
linkedGitLabMR?: number | null
|
||||
// Why: repo-scoped start ref from the "Start from" picker; absent means "use the repo's effective base ref".
|
||||
baseBranch?: string
|
||||
// Why: false when `baseBranch` came from the base-ref picker rather than a branch pick, so restoring the
|
||||
// draft doesn't turn it back into a name-field pill. Absent on pre-flag drafts, which restore as a pick.
|
||||
baseBranchNamesWorkspace?: boolean
|
||||
// Why: review worktrees start from a head ref/SHA while Source Control compares against the provider target branch.
|
||||
compareBaseRef?: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user