Fix Smart create retaining a task checkout hash with Create more (#18727)

This commit is contained in:
Neil
2026-09-04 15:56:38 -07:00
committed by GitHub
parent a65332a8bd
commit 4fab8e2f15
4 changed files with 283 additions and 70 deletions
@@ -140,27 +140,14 @@ export function useComposerSubmitOrchestration(
workspaceSeedName: target.derivedComposerState.workspaceSeedName
})
const multipleCreateReset = useMultipleCreateReset({
handleClearSmartNameSelection: source.issueSourceActions.handleClearSmartNameSelection,
lastAutoNameRef: target.asyncComposerState.lastAutoNameRef,
nameInputRef: target.asyncComposerState.nameInputRef,
setAgentPrompt: target.sourceContextState.setAgentPrompt,
setAttachmentPaths: target.sourceContextState.setAttachmentPaths,
setBranchNameOverride: target.workspaceIdentityState.setBranchNameOverride,
setBranchNameOverridePreservesNameEdits:
target.workspaceIdentityState.setBranchNameOverridePreservesNameEdits,
setCompareBaseRef: target.workspaceIdentityState.setCompareBaseRef,
setCreateError: target.asyncComposerState.setCreateError,
setForkPushWarning: target.workspaceIdentityState.setForkPushWarning,
setLinkedGitLabIssue: target.workspaceIdentityState.setLinkedGitLabIssue,
setLinkedGitLabMR: target.workspaceIdentityState.setLinkedGitLabMR,
setLinkedIssue: target.workspaceIdentityState.setLinkedIssue,
setLinkedPR: target.workspaceIdentityState.setLinkedPR,
setLinkedTaskSourceContext: target.sourceContextState.setLinkedTaskSourceContext,
setLinkedWorkItem: target.sourceContextState.setLinkedWorkItem,
setName: target.sourceContextState.setName,
setNote: target.sourceContextState.setNote,
setPushTarget: target.workspaceIdentityState.setPushTarget,
setReuseSelectedBranch: target.workspaceIdentityState.setReuseSelectedBranch,
setStartFromResetHint: target.workspaceIdentityState.setStartFromResetHint
setNote: target.sourceContextState.setNote
})
const quickSubmitSourcePreparation = useQuickSubmitSourcePreparation({
baseBranch: target.workspaceIdentityState.baseBranch,
@@ -0,0 +1,168 @@
// @vitest-environment happy-dom
import { useRef, useState } from 'react'
import { act, renderHook } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import type { LinkedWorkItemSummary } from '@/lib/new-workspace'
import { useIssueSourceActions } from './issue-source-actions'
import { useMultipleCreateReset } from './multiple-create-reset'
import type { SmartGitHubPrStartPointSelection } from './source-selection-decisions'
const sources: LinkedWorkItemSummary[] = [
{
provider: 'github',
type: 'pr',
number: 42,
title: 'Fix checkout',
url: 'https://github.com/acme/app/pull/42'
},
{
provider: 'github',
type: 'issue',
number: 43,
title: 'Fix checkout',
url: 'https://github.com/acme/app/issues/43'
},
{
provider: 'gitlab',
type: 'mr',
number: 44,
title: 'Fix checkout',
url: 'https://gitlab.com/acme/app/-/merge_requests/44'
}
]
function useSelectedSourceReset(
initialItem: LinkedWorkItemSummary | null,
isProjectGroupTarget = false,
initialBaseBranch: string | undefined = '1234567890abcdef1234567890abcdef12345678'
) {
const [linkedWorkItem, setLinkedWorkItem] = useState<LinkedWorkItemSummary | null>(initialItem)
const [baseBranch, setBaseBranch] = useState<string | undefined>(initialBaseBranch)
const [name, setName] = useState('fix-checkout')
const [note, setNote] = useState('User note')
const lastAutoNameRef = useRef(name)
const branchAutoNameRef = useRef('fix-checkout')
const lastAutoNoteRef = useRef('Generated note')
const smartGitHubPrStartPointSelectionRef = useRef<SmartGitHubPrStartPointSelection | null>(
initialItem?.provider === 'github' && initialItem.type === 'pr'
? {
repoId: 'repo-1',
item: {
...initialItem,
type: 'pr',
id: 'pr-42',
repoId: 'repo-1',
state: 'open',
labels: [],
updatedAt: '2026-09-01T00:00:00Z',
author: null
}
}
: null
)
const source = useIssueSourceActions({
baseBranch,
branchAutoNameRef,
isProjectGroupTarget,
lastAutoNameRef,
lastAutoNoteRef,
linkedWorkItem,
name,
noteRef: useRef(note),
setBaseBranch,
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,
setName,
setNote,
setPushTarget: vi.fn(),
setReuseEligibleBranch: vi.fn(),
setReuseSelectedBranch: vi.fn(),
setStartFromResetHint: vi.fn(),
smartGitHubPrStartPointSelectionRef
})
const reset = useMultipleCreateReset({
handleClearSmartNameSelection: source.handleClearSmartNameSelection,
lastAutoNameRef,
nameInputRef: useRef(null),
setAgentPrompt: vi.fn(),
setAttachmentPaths: vi.fn(),
setCreateError: vi.fn(),
setName,
setNote
})
return {
...reset,
selection: source.smartNameSelection,
linkedWorkItem,
baseBranch,
name,
note,
branchAutoNameRef,
smartGitHubPrStartPointSelectionRef
}
}
describe('create more source reset', () => {
it.each(sources)(
'clears $provider $type and its checkout source before the next create',
(item) => {
const { result } = renderHook(() => useSelectedSourceReset(item))
expect(result.current.selection?.label).toContain('Fix checkout')
if (item.provider === 'github' && item.type === 'pr') {
expect(result.current.smartGitHubPrStartPointSelectionRef.current).not.toBeNull()
}
act(() => result.current.resetForNextCreate())
expect(result.current.smartGitHubPrStartPointSelectionRef.current).toBeNull()
expect(result.current.selection).toBeNull()
expect(result.current.linkedWorkItem).toBeNull()
expect(result.current.baseBranch).toBeUndefined()
expect(result.current.name).toBe('')
expect(result.current.note).toBe('')
expect(result.current.branchAutoNameRef.current).toBe('')
}
)
it.each(['linear', 'jira'] as const)('clears a %s task on a folder target', (provider) => {
const item: LinkedWorkItemSummary = {
provider,
type: 'issue',
number: 0,
title: 'Fix checkout',
url:
provider === 'linear'
? 'https://linear.app/acme/issue/APP-45'
: 'https://acme.atlassian.net/browse/APP-45'
}
const { result } = renderHook(() => useSelectedSourceReset(item, true))
expect(result.current.selection?.kind).toBe(provider)
act(() => result.current.resetForNextCreate())
expect(result.current.selection).toBeNull()
expect(result.current.linkedWorkItem).toBeNull()
expect(result.current.name).toBe('')
expect(result.current.note).toBe('')
})
it('clears a plain branch selection before the next create', () => {
const { result } = renderHook(() => useSelectedSourceReset(null, false, 'feature/checkout'))
expect(result.current.selection).toEqual({ kind: 'branch', label: 'feature/checkout' })
act(() => result.current.resetForNextCreate())
expect(result.current.selection).toBeNull()
expect(result.current.baseBranch).toBeUndefined()
})
})
@@ -2,96 +2,48 @@ import type { ComposerModel } from './composer-model'
type MultipleCreateResetInput = Pick<
ComposerModel,
| 'handleClearSmartNameSelection'
| 'lastAutoNameRef'
| 'nameInputRef'
| 'setAgentPrompt'
| 'setAttachmentPaths'
| 'setBranchNameOverride'
| 'setBranchNameOverridePreservesNameEdits'
| 'setCompareBaseRef'
| 'setCreateError'
| 'setForkPushWarning'
| 'setLinkedGitLabIssue'
| 'setLinkedGitLabMR'
| 'setLinkedIssue'
| 'setLinkedPR'
| 'setLinkedTaskSourceContext'
| 'setLinkedWorkItem'
| 'setName'
| 'setNote'
| 'setPushTarget'
| 'setReuseSelectedBranch'
| 'setStartFromResetHint'
>
import { useCallback } from 'react'
export function useMultipleCreateReset(input: MultipleCreateResetInput) {
const {
handleClearSmartNameSelection,
lastAutoNameRef,
nameInputRef,
setAgentPrompt,
setAttachmentPaths,
setBranchNameOverride,
setBranchNameOverridePreservesNameEdits,
setCompareBaseRef,
setCreateError,
setForkPushWarning,
setLinkedGitLabIssue,
setLinkedGitLabMR,
setLinkedIssue,
setLinkedPR,
setLinkedTaskSourceContext,
setLinkedWorkItem,
setName,
setNote,
setPushTarget,
setReuseSelectedBranch,
setStartFromResetHint
setNote
} = input
const resetForNextCreate = useCallback(() => {
// Why: clear identity fields derived from a PR pick while retaining repo, base, agent, and group context for sequential creates.
// Clear the checkout source too, so a PR's resolved SHA cannot become the next selection.
handleClearSmartNameSelection()
setName('')
lastAutoNameRef.current = ''
setAgentPrompt('')
setNote('')
setAttachmentPaths([])
setLinkedWorkItem(null)
setLinkedTaskSourceContext(null)
setLinkedIssue('')
setLinkedPR(null)
setLinkedGitLabIssue(null)
setLinkedGitLabMR(null)
setBranchNameOverride(undefined)
setBranchNameOverridePreservesNameEdits(false)
setCompareBaseRef(undefined)
setPushTarget(undefined)
setReuseSelectedBranch(false)
setStartFromResetHint(null)
setForkPushWarning(null)
setCreateError(null)
requestAnimationFrame(() => nameInputRef.current?.focus())
}, [
handleClearSmartNameSelection,
lastAutoNameRef,
nameInputRef,
setAgentPrompt,
setAttachmentPaths,
setBranchNameOverride,
setBranchNameOverridePreservesNameEdits,
setCompareBaseRef,
setCreateError,
setForkPushWarning,
setLinkedGitLabIssue,
setLinkedGitLabMR,
setLinkedIssue,
setLinkedPR,
setLinkedTaskSourceContext,
setLinkedWorkItem,
setName,
setNote,
setPushTarget,
setReuseSelectedBranch,
setStartFromResetHint
setNote
])
return {
+106
View File
@@ -0,0 +1,106 @@
import { writeFileSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { test, expect } from './helpers/orca-app'
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
test.use({ orcaAppExtraEnv: { ORCA_BACKGROUND_LAUNCH: '1' } })
test('Create more clears the GitHub PR source before the next worktree', async ({
electronApp,
orcaPage,
testRepoPath
}, testInfo) => {
await waitForSessionReady(orcaPage)
await waitForActiveWorktree(orcaPage)
const sha = execFileSync('git', ['rev-parse', 'HEAD'], {
cwd: testRepoPath,
encoding: 'utf8'
}).trim()
await electronApp.evaluate(({ ipcMain }, baseBranch) => {
ipcMain.removeHandler('worktrees:resolvePrBase')
ipcMain.handle('worktrees:resolvePrBase', () => ({ baseBranch }))
}, sha)
await orcaPage.evaluate(() => {
const store = window.__store!
const state = store.getState()
store.setState({ settings: { ...state.settings!, defaultTuiAgent: 'blank' } })
})
await orcaPage.getByRole('button', { name: 'New workspace', exact: true }).click()
await orcaPage.evaluate(() => {
const store = window.__store!
const repoId = store.getState().repos[0].id
const item = {
id: 'pr-4242',
provider: 'github' as const,
type: 'pr' as const,
number: 4242,
title: 'Fix workspace task reset',
state: 'open' as const,
url: 'https://github.com/acme/app/pull/4242',
labels: [],
updatedAt: '2026-09-01T00:00:00Z',
author: 'e2e',
repoId
}
store.setState({
getCachedWorkItems: () => [item],
fetchWorkItems: async () => [item],
fetchWorkItemsAcrossRepos: async () => ({
items: [item],
failedCount: 0,
githubUnavailable: false
})
})
})
const dialog = orcaPage.getByRole('dialog', { name: /Create (Workspace|Worktree)/i })
const input = dialog.locator('[data-workspace-name-input="true"]')
await input.click()
await orcaPage
.getByRole('option', { name: '#4242 Fix workspace task reset', exact: true })
.click()
const pill = dialog.locator('[data-workspace-source-pill="true"]')
await expect(pill).toContainText('Fix workspace task reset')
await dialog.getByRole('switch', { name: 'Create more' }).click()
await dialog.getByRole('button', { name: /^Create/ }).click()
await expect(dialog).toBeVisible()
await expect(input).toHaveValue('')
await expect
.poll(() =>
orcaPage.evaluate(() =>
window
.__store!.getState()
.allWorktrees()
.some((worktree) => worktree.linkedPR === 4242)
)
)
.toBe(true)
const cdp = await orcaPage.context().newCDPSession(orcaPage)
const screenshot = await cdp.send('Page.captureScreenshot')
const proofPath = testInfo.outputPath('create-more-result.png')
writeFileSync(proofPath, Buffer.from(screenshot.data, 'base64'))
await testInfo.attach('create-more-result.png', {
path: proofPath,
contentType: 'image/png'
})
await cdp.detach()
await expect(pill).toHaveCount(0)
await expect(dialog.getByRole('switch', { name: 'Create more' })).toHaveAttribute(
'aria-checked',
'true'
)
await input.fill('next-independent-worktree')
await dialog.getByRole('button', { name: /^Create/ }).click()
await expect
.poll(() =>
orcaPage.evaluate(() => {
const worktree = window
.__store!.getState()
.allWorktrees()
.find((entry) => entry.displayName === 'next-independent-worktree')
return worktree ? { linkedPR: worktree.linkedPR, linkedIssue: worktree.linkedIssue } : null
})
)
.toEqual({ linkedPR: null, linkedIssue: null })
await expect(input).toHaveValue('')
await expect(pill).toHaveCount(0)
})