fix(create-worktree): keep composer open when adding a project from the dialog (#9397)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-07-18 16:36:52 -07:00
committed by GitHub
co-authored by Orca
parent 927ca9e94b
commit 6a573f376f
10 changed files with 334 additions and 31 deletions
@@ -154,6 +154,9 @@ type NewWorkspaceComposerCardProps = {
sparseSelectedPresetId: string | null
onSparseSelectPreset: (preset: SparsePreset | null) => void
sparseControlsEnabled?: boolean
/** When set, "Add project" opens a host-provided flow (e.g. a nested dialog
* over the composer modal) instead of swapping the store's active modal. */
onAddProjectOverride?: () => void
}
const SSH_STATUS_LABELS: Partial<Record<SshConnectionStatus, string>> = {
@@ -615,7 +618,8 @@ export default function NewWorkspaceComposerCard({
sparsePresets,
sparseSelectedPresetId,
onSparseSelectPreset,
sparseControlsEnabled = true
sparseControlsEnabled = true,
onAddProjectOverride
}: NewWorkspaceComposerCardProps): React.JSX.Element {
// Why: this form uses the lightweight translate() helper directly; subscribe
// so an already-open create dialog repaints when the UI language changes.
@@ -728,8 +732,14 @@ export default function NewWorkspaceComposerCard({
}, [detectedAgentIds, disabledTuiAgents])
const handleAddRepo = React.useCallback((): void => {
// Why: inside the composer modal, swapping activeModal would abruptly
// unmount the composer; the override layers Add Project on top instead.
if (onAddProjectOverride) {
onAddProjectOverride()
return
}
openModal('add-repo')
}, [openModal])
}, [onAddProjectOverride, openModal])
const handleNotePaste = React.useCallback((event: React.ClipboardEvent<HTMLTextAreaElement>) => {
const text = event.clipboardData.getData('text/plain')
const byteLengthMeasurement = measureTextControlPasteByteLength(text, {
@@ -1,5 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import React, { Suspense, useCallback, useEffect, useMemo, useState } from 'react'
import { useAppStore } from '@/store'
import { lazyWithRetry } from '@/lib/lazy-with-retry'
import {
Dialog,
DialogContent,
@@ -9,6 +10,7 @@ import {
} from '@/components/ui/dialog'
import NewWorkspaceComposerCard from '@/components/NewWorkspaceComposerCard'
import AgentSettingsDialog from '@/components/agent/AgentSettingsDialog'
import type { AddRepoDialogHostedController } from '@/components/sidebar/use-add-repo-hosted-controller'
import { useComposerState } from '@/hooks/useComposerState'
import {
pickQuickWorkspaceAgent,
@@ -27,6 +29,12 @@ import { translate } from '@/i18n/i18n'
import { getWorkspaceComposerInitialFocusTarget } from '@/lib/workspace-composer-initial-focus'
import { getFolderWorkspacePrimaryActionLabel } from '@/components/sidebar/folder-workspace-composer-helpers'
// Why: match App-level AddRepoDialog loading — the add flow is off the hot
// path for the composer, so keep its clone/SSH machinery out of the entry render.
const HostedAddRepoDialog = lazyWithRetry(() => import('@/components/sidebar/AddRepoDialog'), {
reloadKey: 'composer-add-repo'
})
type ComposerModalData = {
prefilledName?: string
initialRepoId?: string
@@ -120,7 +128,8 @@ function QuickTabBody({
onComposerNodeChange,
nameInputRef,
submitQuick,
createDisabled
createDisabled,
selectAddedProjectRepo
} = useComposerState({
initialName: modalData.prefilledName ?? '',
// Why: the modal is quick-create only now, so prompt-prefill state is
@@ -180,6 +189,44 @@ function QuickTabBody({
const handleCreate = useCallback(async (): Promise<void> => {
await submitQuick(quickAgent)
}, [quickAgent, submitQuick])
// Why: Add Project layers over the composer as a nested dialog instead of
// replacing it in the activeModal slot — closing the composer mid-flow (and
// losing the typed name/prompt) was the old, abrupt behavior. Once opened it
// stays mounted so cancel/complete plays the close animation and the
// dialog's close effects can abort in-flight clone/scan work. (Folder/non-git
// outcomes still navigate away and tear the whole modal down.)
const [addProjectOpen, setAddProjectOpen] = useState(false)
const [addProjectMounted, setAddProjectMounted] = useState(false)
const handleOpenAddProject = useCallback((): void => {
setAddProjectMounted(true)
setAddProjectOpen(true)
}, [])
const handleProjectAdded = useCallback(
(repoId: string): void => {
selectAddedProjectRepo(repoId)
},
[selectAddedProjectRepo]
)
const handleAddProjectCloseAutoFocus = useCallback(
(event: Event): void => {
// Why: after adding a project the next step is naming the worktree.
// Radix would try to restore focus to the (unmounted) combobox row that
// opened the dialog; send it to the name field instead — the same place
// picking a project from the combobox lands.
event.preventDefault()
nameInputRef?.current?.focus()
},
[nameInputRef]
)
const addProjectController = useMemo<AddRepoDialogHostedController>(
() => ({
open: addProjectOpen,
onOpenChange: setAddProjectOpen,
onProjectAdded: handleProjectAdded,
onCloseAutoFocus: handleAddProjectCloseAutoFocus
}),
[addProjectOpen, handleAddProjectCloseAutoFocus, handleProjectAdded]
)
const selectedProjectOption = cardProps.projectOptions.find(
(option) => option.id === cardProps.selectedProjectId
)
@@ -191,8 +238,12 @@ function QuickTabBody({
: translate('auto.components.NewWorkspaceComposerModal.createWorkspace', 'Create workspace')
// Cmd/Ctrl+Enter submits, Esc first blurs the focused input (like the full page).
const nestedDialogOpen = agentSettingsOpen || addProjectOpen
useEffect(() => {
if (!active) {
if (!active || nestedDialogOpen) {
// Why: while a nested dialog (Add Project / Agents) is layered on top,
// this capture-phase handler must not steal its Escape (which should
// close only the nested dialog) or fire composer submit underneath it.
return
}
const onKeyDown = (event: KeyboardEvent): void => {
@@ -236,7 +287,7 @@ function QuickTabBody({
}
window.addEventListener('keydown', onKeyDown, { capture: true })
return () => window.removeEventListener('keydown', onKeyDown, { capture: true })
}, [active, composerRef, createDisabled, handleCreate, onClose])
}, [active, composerRef, createDisabled, handleCreate, nestedDialogOpen, onClose])
return (
<>
@@ -271,8 +322,14 @@ function QuickTabBody({
primaryActionLabel={primaryActionLabel}
onOpenAgentSettings={() => setAgentSettingsOpen(true)}
onCreate={() => void handleCreate()}
onAddProjectOverride={handleOpenAddProject}
/>
<AgentSettingsDialog open={agentSettingsOpen} onOpenChange={setAgentSettingsOpen} />
{addProjectMounted ? (
<Suspense fallback={null}>
<HostedAddRepoDialog hosted={addProjectController} />
</Suspense>
) : null}
</>
)
}
@@ -16,24 +16,36 @@ import { useAddRepoHostChangeReset } from './use-add-repo-host-change-reset'
import { AddRepoDialogChrome } from './AddRepoDialogChrome'
import { AddRepoHostSelectorSlot } from './AddRepoHostSelectorSlot'
import { useAddRepoRemoteNestedScan } from './use-add-repo-remote-nested-scan'
import {
useAddRepoHostedController,
type AddRepoDialogHostedController
} from './use-add-repo-hosted-controller'
const AddRepoDialog = React.memo(function AddRepoDialog() {
const activeModal = useAppStore((s) => s.activeModal)
const modalData = useAppStore((s) => s.modalData)
const closeModal = useAppStore((s) => s.closeModal)
const AddRepoDialog = React.memo(function AddRepoDialog({
hosted
}: {
hosted?: AddRepoDialogHostedController
}) {
const isOpen = useAppStore((s) => (hosted ? hosted.open : s.activeModal === 'add-repo'))
// Why: hosted mode never receives dropped paths through modalData — that
// channel belongs to the store-modal instance.
const droppedLocalPath = useAppStore((s) =>
!hosted && typeof s.modalData.droppedLocalPath === 'string' ? s.modalData.droppedLocalPath : ''
)
const addRepoPath = useAppStore((s) => s.addRepoPath)
const scanNestedRepos = useAppStore((s) => s.scanNestedRepos)
const cancelNestedRepoScan = useAppStore((s) => s.cancelNestedRepoScan)
const importNestedRepos = useAppStore((s) => s.importNestedRepos)
const repos = useAppStore((s) => s.repos)
const fetchWorktrees = useAppStore((s) => s.fetchWorktrees)
const openSettingsPage = useAppStore((s) => s.openSettingsPage)
const openSettingsTarget = useAppStore((s) => s.openSettingsTarget)
const setHideDefaultBranchWorkspace = useAppStore((s) => s.setHideDefaultBranchWorkspace)
const settings = useAppStore((s) => s.settings)
const { closeModal, closeForFolderHandoff, finishProjectAdd, handleOpenSshSettings } =
useAddRepoHostedController(hosted)
const completeGitRepoAdd = useCompleteGitRepoAdd({
closeModal,
setHideDefaultBranchWorkspace
setHideDefaultBranchWorkspace,
finishProjectAdd
})
const [step, setStep] = useState<AddRepoDialogStep>('add')
@@ -63,7 +75,7 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
setStep
})
const hostSelection = useAddRepoHostSelection({ isOpen: activeModal === 'add-repo', setStep })
const hostSelection = useAddRepoHostSelection({ isOpen, setStep })
const selectedRuntimeEnvironmentId =
hostSelection.selectedParsedHost?.kind === 'runtime'
? hostSelection.selectedParsedHost.environmentId
@@ -90,7 +102,8 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
} = useRemoteRepo(
fetchWorktrees,
setStep,
closeModal,
// Why: useRemoteRepo closes only for the non-git → confirm-dialog handoff.
closeForFolderHandoff,
(repoId) => completeGitRepoAdd(repoId, 'ssh_remote_path'),
scanNestedRepos,
showRemoteNestedRepoReview,
@@ -110,7 +123,8 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
handleCreate
} = useCreateRepo(
fetchWorktrees,
closeModal,
// Why: useCreateRepo closes only after a folder (non-git) create.
closeForFolderHandoff,
(repoId) => completeGitRepoAdd(repoId, 'create_project'),
{
hostId: hostSelection.selectedHostId,
@@ -155,9 +169,6 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
onGitRepoReady: completeGitRepoAdd
})
const isOpen = activeModal === 'add-repo'
const droppedLocalPath =
typeof modalData.droppedLocalPath === 'string' ? modalData.droppedLocalPath : ''
const isRuntimeEnvironmentActive = Boolean(selectedRuntimeEnvironmentId)
const selectedHostKind = hostSelection.selectedParsedHost?.kind
const { handleBrowse, resetLocalFolderFlow } = useAddRepoLocalFolderFlow({
@@ -165,7 +176,8 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
droppedLocalPath,
activeRuntimeEnvironmentId: selectedRuntimeEnvironmentId,
addRepoPath,
closeModal,
// Why: this flow's closes are all folder/non-git outcomes that navigate.
closeModal: closeForFolderHandoff,
fetchWorktrees,
scanNestedRepos,
setActiveNestedScanId,
@@ -183,7 +195,8 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
handleAddServerPath
} = useAddRepoServerPathFlow({
addRepoPath,
closeModal,
// Why: closes only after a folder add, which activates the folder workspace.
closeModal: closeForFolderHandoff,
fetchWorktrees,
getNestedRepoRuntimeKind,
scanNestedRepos,
@@ -207,6 +220,9 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
nestedGroupName,
nestedImportScanId,
activeRuntimeEnvironmentId: selectedRuntimeEnvironmentId,
// Why: open-as-folder outcomes navigate; git imports finish via
// completeGitRepoAdd instead.
closeModal: closeForFolderHandoff,
fetchWorktrees,
importNestedRepos,
getNestedRepoRuntimeKind,
@@ -289,6 +305,7 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
step={step}
isAdding={isAdding}
onBack={handleBack}
onCloseAutoFocus={hosted?.onCloseAutoFocus}
onOpenChange={handleOpenChange}
>
<AddRepoDialogStepContent
@@ -365,11 +382,7 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
setRemoteError(null)
}}
onAddRemoteRepo={handleAddRemoteRepo}
onOpenSshSettings={() => {
closeModal()
openSettingsTarget({ pane: 'ssh', repoId: null, sectionId: 'ssh' })
openSettingsPage()
}}
onOpenSshSettings={handleOpenSshSettings}
onConnectTarget={handleConnectTarget}
onStopRemoteNestedScan={stopRemoteNestedScan}
onCloneUrlChange={(value) => {
@@ -8,6 +8,7 @@ export function AddRepoDialogChrome({
isAdding,
isOpen,
onBack,
onCloseAutoFocus,
onOpenChange,
step
}: {
@@ -15,12 +16,14 @@ export function AddRepoDialogChrome({
isAdding: boolean
isOpen: boolean
onBack: () => void
onCloseAutoFocus?: (event: Event) => void
onOpenChange: (open: boolean) => void
step: AddRepoDialogStep
}) {
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent
onCloseAutoFocus={onCloseAutoFocus}
className={`min-w-0 overflow-hidden sm:max-w-lg [&>*]:min-w-0 ${
step === 'nested' ? 'max-h-[calc(100vh-2rem)] grid-rows-[auto_auto_minmax(0,1fr)]' : ''
}`}
@@ -0,0 +1,113 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as ReactModule from 'react'
vi.mock('react', async (importOriginal) => {
const actual = await importOriginal<typeof ReactModule>()
return {
...actual,
useCallback: <T extends (...args: never[]) => unknown>(fn: T) => fn,
useMemo: <T>(factory: () => T) => factory()
}
})
const mocks = vi.hoisted(() => ({
state: {
closeModal: vi.fn(),
openSettingsPage: vi.fn(),
openSettingsTarget: vi.fn()
},
markOnboardingProjectAdded: vi.fn(() => Promise.resolve())
}))
vi.mock('@/store', () => ({
useAppStore: Object.assign(
(selector: (state: typeof mocks.state) => unknown) => selector(mocks.state),
{ getState: () => mocks.state }
)
}))
vi.mock('@/lib/onboarding-project-checklist', () => ({
markOnboardingProjectAdded: mocks.markOnboardingProjectAdded
}))
import { useAddRepoHostedController } from './use-add-repo-hosted-controller'
describe('useAddRepoHostedController', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('falls back to the store closeModal without a hosted controller', () => {
const { closeModal, closeForFolderHandoff, finishProjectAdd } =
useAddRepoHostedController(undefined)
closeModal()
expect(mocks.state.closeModal).toHaveBeenCalledTimes(1)
closeForFolderHandoff()
expect(mocks.state.closeModal).toHaveBeenCalledTimes(2)
expect(finishProjectAdd).toBeUndefined()
})
it('closes only the hosted dialog, never the store modal slot', () => {
const onOpenChange = vi.fn()
const { closeModal } = useAddRepoHostedController({
open: true,
onOpenChange,
onProjectAdded: vi.fn()
})
closeModal()
expect(onOpenChange).toHaveBeenCalledWith(false)
expect(mocks.state.closeModal).not.toHaveBeenCalled()
})
it('folder handoffs close both the hosted dialog and the composer modal', () => {
const onOpenChange = vi.fn()
const { closeForFolderHandoff } = useAddRepoHostedController({
open: true,
onOpenChange,
onProjectAdded: vi.fn()
})
// Why: folder/non-git outcomes navigate (folder-workspace activation or
// the confirm-non-git-folder store modal); leaving the composer open
// would hide that navigation behind a stale project selection.
closeForFolderHandoff()
expect(onOpenChange).toHaveBeenCalledWith(false)
expect(mocks.state.closeModal).toHaveBeenCalledTimes(1)
})
it('finishProjectAdd closes the hosted dialog and hands the repo to the host', async () => {
const order: string[] = []
const onOpenChange = vi.fn(() => order.push('close'))
const onProjectAdded = vi.fn(() => {
order.push('added')
})
const { finishProjectAdd } = useAddRepoHostedController({
open: true,
onOpenChange,
onProjectAdded
})
await finishProjectAdd?.('repo-1')
expect(mocks.markOnboardingProjectAdded).toHaveBeenCalledWith('addedRepo')
expect(onProjectAdded).toHaveBeenCalledWith('repo-1')
// Why: closing before selection keeps the composer visible under the
// dialog's close animation while the new project lands in the picker.
expect(order).toEqual(['close', 'added'])
})
it('SSH settings navigation closes both hosted dialog and composer modal', () => {
const onOpenChange = vi.fn()
const { handleOpenSshSettings } = useAddRepoHostedController({
open: true,
onOpenChange,
onProjectAdded: vi.fn()
})
handleOpenSshSettings()
expect(onOpenChange).toHaveBeenCalledWith(false)
expect(mocks.state.closeModal).toHaveBeenCalledTimes(1)
expect(mocks.state.openSettingsTarget).toHaveBeenCalledWith({
pane: 'ssh',
repoId: null,
sectionId: 'ssh'
})
expect(mocks.state.openSettingsPage).toHaveBeenCalledTimes(1)
})
})
@@ -0,0 +1,72 @@
import { useCallback, useMemo } from 'react'
import { useAppStore } from '@/store'
import { markOnboardingProjectAdded } from '@/lib/onboarding-project-checklist'
/** Contract a host surface (e.g. the workspace composer modal) passes to
* AddRepoDialog to nest it as a layered dialog instead of the store modal. */
export type AddRepoDialogHostedController = {
open: boolean
onOpenChange: (open: boolean) => void
/** Called after a Git project is added; the host selects the new project
* instead of running the default-checkout navigation handoff. */
onProjectAdded: (repoId: string) => void | Promise<void>
/** Radix close-autofocus hook for the nested dialog. Lets the host redirect
* focus (e.g. to the composer's name field) instead of Radix restoring it
* to the already-unmounted combobox row that opened the dialog. */
onCloseAutoFocus?: (event: Event) => void
}
export function useAddRepoHostedController(hosted: AddRepoDialogHostedController | undefined): {
closeModal: () => void
/** Close for folder/non-git outcomes, which end in folder-workspace
* activation (or the confirm-non-git-folder store modal) instead of a
* composer selection. Hosted mode must close the composer too — leaving it
* open would hide the navigation behind a stale project selection. */
closeForFolderHandoff: () => void
finishProjectAdd: ((repoId: string) => Promise<void>) | undefined
handleOpenSshSettings: () => void
} {
const storeCloseModal = useAppStore((s) => s.closeModal)
const openSettingsPage = useAppStore((s) => s.openSettingsPage)
const openSettingsTarget = useAppStore((s) => s.openSettingsTarget)
const hostedOnOpenChange = hosted?.onOpenChange
const hostedOnProjectAdded = hosted?.onProjectAdded
// Why: hosted mode (nested inside the workspace composer) must close only
// this dialog, never the composer modal that lives in the activeModal slot.
const closeModal = useMemo(
() => (hostedOnOpenChange ? () => hostedOnOpenChange(false) : storeCloseModal),
[hostedOnOpenChange, storeCloseModal]
)
const finishProjectAdd = useMemo(
() =>
hostedOnOpenChange && hostedOnProjectAdded
? async (repoId: string): Promise<void> => {
await markOnboardingProjectAdded('addedRepo')
hostedOnOpenChange(false)
await hostedOnProjectAdded(repoId)
}
: undefined,
[hostedOnOpenChange, hostedOnProjectAdded]
)
const closeForFolderHandoff = useMemo(
() =>
hostedOnOpenChange
? () => {
hostedOnOpenChange(false)
storeCloseModal()
}
: storeCloseModal,
[hostedOnOpenChange, storeCloseModal]
)
const handleOpenSshSettings = useCallback((): void => {
closeModal()
// Why: Settings is a full page; in hosted mode the composer modal in the
// activeModal slot would otherwise stay open on top of it.
if (hostedOnOpenChange) {
storeCloseModal()
}
openSettingsTarget({ pane: 'ssh', repoId: null, sectionId: 'ssh' })
openSettingsPage()
}, [closeModal, hostedOnOpenChange, openSettingsPage, openSettingsTarget, storeCloseModal])
return { closeModal, closeForFolderHandoff, finishProjectAdd, handleOpenSshSettings }
}
@@ -12,11 +12,16 @@ import { finishProjectAddWithDefaultCheckout } from './project-added-default-che
type CompleteGitRepoAddOptions = {
closeModal: () => void
setHideDefaultBranchWorkspace: (hide: boolean) => void
/** Why: the nested Add Project flow (hosted inside the workspace composer)
* keeps the composer open and selects the new project instead of running
* the default-checkout navigation handoff. Telemetry above still applies. */
finishProjectAdd?: (repoId: string, source: AddRepoExistingWorkspaceSource) => Promise<void>
}
export function useCompleteGitRepoAdd({
closeModal,
setHideDefaultBranchWorkspace
setHideDefaultBranchWorkspace,
finishProjectAdd
}: CompleteGitRepoAddOptions): (
repoId: string,
source: AddRepoExistingWorkspaceSource
@@ -44,6 +49,10 @@ export function useCompleteGitRepoAdd({
detectedTelemetryTrackedRef.current.add(repoId)
track('add_repo_existing_workspaces_detected', existingWorkspaceTelemetry)
}
if (finishProjectAdd) {
await finishProjectAdd(repoId, source)
return
}
await finishProjectAddWithDefaultCheckout({
repoId,
source,
@@ -51,6 +60,6 @@ export function useCompleteGitRepoAdd({
setHideDefaultBranchWorkspace
})
},
[closeModal, setHideDefaultBranchWorkspace]
[closeModal, finishProjectAdd, setHideDefaultBranchWorkspace]
)
}
@@ -78,6 +78,7 @@ function useTestAddRepoNestedImportFlow(
nestedGroupName: 'platform',
nestedImportScanId: 'scan-1',
activeRuntimeEnvironmentId: null,
closeModal: mocks.state.closeModal,
fetchWorktrees: vi.fn(),
importNestedRepos: vi.fn<() => Promise<ProjectGroupImportResult | null>>(),
getNestedRepoRuntimeKind: vi.fn(() => 'local' as const),
@@ -22,6 +22,7 @@ export function useAddRepoNestedImportFlow({
nestedGroupName,
nestedImportScanId,
activeRuntimeEnvironmentId,
closeModal,
fetchWorktrees,
importNestedRepos,
getNestedRepoRuntimeKind,
@@ -36,6 +37,9 @@ export function useAddRepoNestedImportFlow({
nestedGroupName: string
nestedImportScanId: string | null
activeRuntimeEnvironmentId: string | null | undefined
/** Why: hosted (composer-nested) mode routes this to closing only the
* nested dialog; store-modal mode routes it to the activeModal slot. */
closeModal: () => void
fetchWorktrees: (repoId: string, options?: { requireAuthoritative?: boolean }) => Promise<unknown>
importNestedRepos: (args: {
parentPath: string
@@ -260,7 +264,10 @@ export function useAddRepoNestedImportFlow({
try {
const state = useAppStore.getState()
if (nestedConnectionId) {
state.closeModal()
// Why: the non-git confirm dialog is a store-modal handoff that ends
// in folder-workspace activation; close this add flow (nested dialog
// or store modal) before handing over.
closeModal()
state.openModal('confirm-non-git-folder', {
folderPath: path,
connectionId: nestedConnectionId
@@ -274,7 +281,7 @@ export function useAddRepoNestedImportFlow({
return
}
if (repo) {
useAppStore.getState().closeModal()
closeModal()
}
} catch (err) {
if (gen === nestedImportGenRef.current) {
@@ -287,6 +294,7 @@ export function useAddRepoNestedImportFlow({
}
}, [
activeRuntimeEnvironmentId,
closeModal,
getNestedRepoRuntimeKind,
nestedAttemptId,
nestedConnectionId,
+18 -1
View File
@@ -391,6 +391,9 @@ export type UseComposerStateResult = {
submitQuick: (agent: TuiAgent | null) => Promise<void>
/** Invoked by the Enter handler to re-check whether submission should fire. */
createDisabled: boolean
/** Selects the repo a nested Add Project flow just added, clearing any
* folder-group target so the composer lands on the new project. */
selectAddedProjectRepo: (repoId: string) => void
}
export type InitialWorkspaceRunSeedInput = {
@@ -2819,6 +2822,19 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
workspaceHostScope
]
)
const selectAddedProjectRepo = useCallback(
(nextRepoId: string): void => {
// Why: the nested Add Project flow hands back a repo id. Selecting it
// must also clear a folder-group target, whose onRepoChange handler
// only accepts repos inside the selected group.
initialProjectGroupAppliedRef.current = true
setSelectedProjectGroupId(null)
setProjectError(null)
handleRepoChange(nextRepoId)
},
[handleRepoChange]
)
const showProjectRequiredError = useCallback((): void => {
setProjectError('Choose or add a project before creating a workspace.')
requestAnimationFrame(() => {
@@ -4440,6 +4456,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
nameInputRef,
submit,
submitQuick,
createDisabled
createDisabled,
selectAddedProjectRepo
}
}