fix(ui): keep New Worktree form state when worktree topology changes
The reset-on-open effect also depended on generateUniqueSlug, which is derived from the available-worktree list, so a worktree appearing or disappearing while the dialog was open wiped the form (branch name, linked issue). Initialize once per open via a ref guard, pinned by a behavior test. Also tightens CreateWorktreeArgs/GitHub selection typing in place of type assertions.
This commit is contained in:
@@ -51,7 +51,7 @@ import {
|
||||
} from '@/lib/worktrees/worktreeSourceBranchPreference';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { useGitBranches, useGitStore, useGitLoadingBranches } from '@/stores/useGitStore';
|
||||
import { GitHubIntegrationDialog } from './GitHubIntegrationDialog';
|
||||
import { GitHubIntegrationDialog, type GitHubWorktreeSelection } from './GitHubIntegrationDialog';
|
||||
import { LinearIssuePickerDialog } from './LinearIssuePickerDialog';
|
||||
import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip';
|
||||
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
|
||||
@@ -65,7 +65,7 @@ import type {
|
||||
LinearIssue,
|
||||
LinearIssueComment,
|
||||
} from '@/lib/api/types';
|
||||
import type { ProjectRef } from '@/lib/worktrees/worktreeManager';
|
||||
import type { CreateWorktreeArgs, ProjectRef } from '@/lib/worktrees/worktreeManager';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
type Mode = 'new-branch' | 'existing-branch';
|
||||
@@ -456,6 +456,7 @@ export function NewWorktreeDialog({
|
||||
// Creation state
|
||||
const [isCreating, setIsCreating] = React.useState(false);
|
||||
const [validationAbortController, setValidationAbortController] = React.useState<AbortController | null>(null);
|
||||
const initializedForCurrentOpen = React.useRef(false);
|
||||
|
||||
const resolveDefaultAgentName = React.useCallback((): string | undefined => {
|
||||
const configState = useConfigStore.getState();
|
||||
@@ -493,9 +494,7 @@ export function NewWorktreeDialog({
|
||||
: undefined;
|
||||
|
||||
const provider = configState.providers.find((p) => p.id === providerID);
|
||||
const model = provider?.models.find((m: Record<string, unknown>) => (m as { id?: string }).id === modelID) as
|
||||
| { variants?: Record<string, unknown> }
|
||||
| undefined;
|
||||
const model = provider?.models.find((m) => m.id === modelID);
|
||||
const variants = model?.variants;
|
||||
if (!variants) return settingsDefaultVariant || currentVariant || undefined;
|
||||
if (settingsDefaultVariant && Object.prototype.hasOwnProperty.call(variants, settingsDefaultVariant)) return settingsDefaultVariant;
|
||||
@@ -763,7 +762,12 @@ export function NewWorktreeDialog({
|
||||
// Reset state on each open. Resetting on close would empty the form during
|
||||
// the close animation, causing visible flicker.
|
||||
React.useLayoutEffect(() => {
|
||||
if (!open) return;
|
||||
if (!open) {
|
||||
initializedForCurrentOpen.current = false;
|
||||
return;
|
||||
}
|
||||
if (initializedForCurrentOpen.current) return;
|
||||
initializedForCurrentOpen.current = true;
|
||||
|
||||
setMode('new-branch');
|
||||
setExistingBranchState({
|
||||
@@ -840,14 +844,15 @@ export function NewWorktreeDialog({
|
||||
if (normalizedBranch && normalizedWorktree) {
|
||||
const linkedPr = mode === 'new-branch' ? newBranchState.linkedPr : null;
|
||||
const prConfig = linkedPr ? resolvePrWorktreeConfig(linkedPr, localBranches, remoteBranches) : null;
|
||||
const result = await validateWorktreeCreate(projectRef, {
|
||||
const validateArgs: CreateWorktreeArgs = {
|
||||
mode: mode === 'existing-branch' || prConfig ? 'existing' : 'new',
|
||||
branchName: normalizedBranch,
|
||||
worktreeName: normalizedWorktree,
|
||||
existingBranch: prConfig?.existingBranch ?? (mode === 'existing-branch' ? normalizedBranch : undefined),
|
||||
...(prConfig?.ensureRemoteName ? { ensureRemoteName: prConfig.ensureRemoteName } : {}),
|
||||
...(prConfig?.ensureRemoteUrl ? { ensureRemoteUrl: prConfig.ensureRemoteUrl } : {}),
|
||||
});
|
||||
};
|
||||
if (prConfig?.ensureRemoteName) validateArgs.ensureRemoteName = prConfig.ensureRemoteName;
|
||||
if (prConfig?.ensureRemoteUrl) validateArgs.ensureRemoteUrl = prConfig.ensureRemoteUrl;
|
||||
const result = await validateWorktreeCreate(projectRef, validateArgs);
|
||||
|
||||
if (abortController.signal.aborted) return;
|
||||
|
||||
@@ -961,13 +966,13 @@ export function NewWorktreeDialog({
|
||||
const sourceBranch = newBranchState.sourceBranch;
|
||||
|
||||
let sourceLabel = '';
|
||||
const args = (() => {
|
||||
const args: CreateWorktreeArgs = (() => {
|
||||
if (linkedPr) {
|
||||
const prConfig = resolvePrWorktreeConfig(linkedPr, localBranches, remoteBranches);
|
||||
sourceLabel = prConfig.sourceLabel;
|
||||
return {
|
||||
const prArgs: CreateWorktreeArgs = {
|
||||
preferredName: normalizedBranch || normalizedWorktree,
|
||||
mode: 'existing' as const,
|
||||
mode: 'existing',
|
||||
branchName: normalizedBranch,
|
||||
worktreeName: normalizedWorktree,
|
||||
existingBranch: prConfig.existingBranch,
|
||||
@@ -976,22 +981,24 @@ export function NewWorktreeDialog({
|
||||
upstreamRemote: prConfig.upstreamRemote,
|
||||
upstreamBranch: prConfig.upstreamBranch,
|
||||
returnAfterDirectoryCreated: true,
|
||||
...(prConfig.ensureRemoteName ? { ensureRemoteName: prConfig.ensureRemoteName } : {}),
|
||||
...(prConfig.ensureRemoteUrl ? { ensureRemoteUrl: prConfig.ensureRemoteUrl } : {}),
|
||||
};
|
||||
if (prConfig.ensureRemoteName) prArgs.ensureRemoteName = prConfig.ensureRemoteName;
|
||||
if (prConfig.ensureRemoteUrl) prArgs.ensureRemoteUrl = prConfig.ensureRemoteUrl;
|
||||
return prArgs;
|
||||
}
|
||||
|
||||
sourceLabel = mode === 'new-branch' ? sourceBranch : '';
|
||||
return {
|
||||
const baseArgs: CreateWorktreeArgs = {
|
||||
preferredName: normalizedBranch || normalizedWorktree,
|
||||
mode: mode === 'existing-branch' ? 'existing' as const : 'new' as const,
|
||||
mode: mode === 'existing-branch' ? 'existing' : 'new',
|
||||
branchName: mode === 'existing-branch' ? undefined : normalizedBranch,
|
||||
worktreeName: normalizedWorktree,
|
||||
existingBranch: mode === 'existing-branch' ? normalizedBranch : undefined,
|
||||
setupCommands,
|
||||
returnAfterDirectoryCreated: true,
|
||||
...(sourceBranch && mode === 'new-branch' ? { startRef: sourceBranch } : {}),
|
||||
};
|
||||
if (sourceBranch && mode === 'new-branch') baseArgs.startRef = sourceBranch;
|
||||
return baseArgs;
|
||||
})();
|
||||
|
||||
const metadata = await createWorktreeWithDefaults(projectRef, args);
|
||||
@@ -1084,11 +1091,7 @@ export function NewWorktreeDialog({
|
||||
};
|
||||
|
||||
// Handle GitHub selection
|
||||
const handleGitHubSelect = (result: {
|
||||
type: 'issue' | 'pr';
|
||||
item: GitHubIssue | GitHubPullRequestSummary;
|
||||
includeDiff?: boolean;
|
||||
} | null) => {
|
||||
const handleGitHubSelect = (result: GitHubWorktreeSelection | null) => {
|
||||
if (!result) {
|
||||
setNewBranchState(prev => ({
|
||||
...prev,
|
||||
@@ -1102,7 +1105,7 @@ export function NewWorktreeDialog({
|
||||
}
|
||||
|
||||
if (result.type === 'issue') {
|
||||
const issue = result.item as GitHubIssue;
|
||||
const issue = result.item;
|
||||
const newBranchName = `issue-${issue.number}-${generateBranchSlug()}`;
|
||||
setNewBranchState(prev => ({
|
||||
...prev,
|
||||
@@ -1115,7 +1118,7 @@ export function NewWorktreeDialog({
|
||||
isSyncingWorktreeName: true,
|
||||
}));
|
||||
} else if (result.type === 'pr') {
|
||||
const pr = result.item as GitHubPullRequestSummary;
|
||||
const pr = result.item;
|
||||
setNewBranchState(prev => ({
|
||||
...prev,
|
||||
linkedPr: pr,
|
||||
@@ -1261,7 +1264,11 @@ export function NewWorktreeDialog({
|
||||
{ id: 'existing-branch', label: t('session.newWorktree.mode.existingBranch'), icon: <Icon name="git-repository" className="h-3.5 w-3.5" /> },
|
||||
]}
|
||||
activeId={mode}
|
||||
onSelect={(id) => handleModeChange(id as Mode)}
|
||||
onSelect={(id) => {
|
||||
if (id === 'new-branch' || id === 'existing-branch') {
|
||||
handleModeChange(id);
|
||||
}
|
||||
}}
|
||||
variant="active-pill"
|
||||
layoutMode="fit"
|
||||
className="w-full"
|
||||
@@ -1767,7 +1774,11 @@ export function NewWorktreeDialog({
|
||||
{ id: 'existing-branch', label: t('session.newWorktree.mode.existingBranch'), icon: <Icon name="git-repository" className="h-3.5 w-3.5" /> },
|
||||
]}
|
||||
activeId={mode}
|
||||
onSelect={(id) => handleModeChange(id as Mode)}
|
||||
onSelect={(id) => {
|
||||
if (id === 'new-branch' || id === 'existing-branch') {
|
||||
handleModeChange(id);
|
||||
}
|
||||
}}
|
||||
variant="active-pill"
|
||||
layoutMode="fit"
|
||||
className="w-full"
|
||||
|
||||
Reference in New Issue
Block a user