Keep New Workspace prompts when switching projects or hosts (#2036)

* fix(app): preserve New Workspace prompts across target changes

The New Workspace draft was keyed by the selected host and project, so changing either target replaced the visible prompt with another draft scope. Treat the screen as one persistent draft surface and migrate the newest active legacy draft.

* test(app): express draft persistence as user actions

* fix(app): clear stale picker PR context

* fix(app): persist picker attachment ownership

* fix(app): keep workspace target context safe
This commit is contained in:
Mohamed Boudra
2026-07-13 15:36:48 +02:00
committed by GitHub
parent 218097b7cc
commit 13d6ad598e
11 changed files with 462 additions and 79 deletions
@@ -50,7 +50,7 @@ import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store"
import { useLastWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
import { normalizeWorkspaceDescriptor, useSessionStore } from "@/stores/session-store";
import { useWorkspace } from "@/stores/session-store-hooks";
import { generateDraftId } from "@/stores/draft-keys";
import { buildNewWorkspaceDraftKey, generateDraftId } from "@/stores/draft-keys";
import { useDraftStore } from "@/stores/draft-store";
import { useProjectPickerStore } from "@/stores/project-picker-store";
import { isActiveCreateFlowForDraft, useCreateFlowStore } from "@/stores/create-flow-store";
@@ -92,7 +92,11 @@ import {
type PickerCheckoutRequest,
type PickerItem,
} from "./new-workspace-picker-item";
import { findCheckoutHintPrAttachment, syncPickerPrAttachment } from "./new-workspace-picker-state";
import {
clearPickerPrAttachmentForTargetChange,
findCheckoutHintPrAttachment,
syncPickerPrAttachment,
} from "./new-workspace-picker-state";
import {
resolveNewWorkspaceAutomaticServerId,
resolveNewWorkspaceInitialServerId,
@@ -179,7 +183,6 @@ interface PickerOptionData {
interface PickerSelection {
item: PickerItem;
attachedPrNumber: number | null;
}
const BRANCH_OPTION_PREFIX = "branch:";
@@ -786,18 +789,6 @@ function getContentStyle(input: { isCompact: boolean; insetBottom: number }) {
return [styles.content, styles.contentCentered];
}
function buildNewWorkspaceDraftKey(input: {
selectedServerId: string;
selectedSourceDirectory: string | null;
draftId?: string;
}): string {
const explicitDraftId = input.draftId?.trim();
if (explicitDraftId) {
return `new-workspace:draft:${explicitDraftId}`;
}
return `new-workspace:${input.selectedServerId}:${input.selectedSourceDirectory ?? "choose-project"}`;
}
function getSelectedPickerItem(selection: PickerSelection | null): PickerItem | null {
if (!selection) return null;
return selection.item;
@@ -1682,11 +1673,7 @@ export function NewWorkspaceScreen({
const projectIconDataByProjectKey = useProjectIconDataByProjectKey({
projects: projectIconTargets,
});
const draftKey = buildNewWorkspaceDraftKey({
selectedServerId,
selectedSourceDirectory,
draftId,
});
const draftKey = buildNewWorkspaceDraftKey(draftId);
const forkDraftSetup = usePendingWorkspaceDraftSetup(draftId);
const draftContextScopeKey = useDraftWorkspaceAttachmentScopeKey(draftId);
const visibleDraftContextScopeKeys = useMemo(
@@ -1801,22 +1788,16 @@ export function NewWorkspaceScreen({
}, [selectedItem]);
const selectPickerItem = useCallback(
(item: PickerItem) => {
const next = syncPickerPrAttachment({
const nextAttachments = syncPickerPrAttachment({
attachments: chatDraft.attachments,
previousPickerPrNumber: manualPickerSelection?.attachedPrNumber ?? null,
item,
});
setManualPickerSelection({
item,
attachedPrNumber: next.attachedPrNumber,
});
if (next.attachments !== chatDraft.attachments) {
chatDraft.setAttachments(next.attachments);
}
setManualPickerSelection({ item });
chatDraft.setAttachments(nextAttachments);
setPickerOpen(false);
},
[chatDraft, manualPickerSelection?.attachedPrNumber],
[chatDraft],
);
const handleSelectOption = useCallback(
@@ -1828,6 +1809,20 @@ export function NewWorkspaceScreen({
[itemById, selectPickerItem],
);
const clearManualPickerSelectionForTargetChange = useCallback(
(currentTargetId: string, nextTargetId: string) => {
const nextAttachments = clearPickerPrAttachmentForTargetChange({
attachments: chatDraft.attachments,
currentTargetId,
nextTargetId,
});
if (nextAttachments === chatDraft.attachments) return;
chatDraft.setAttachments(nextAttachments);
setManualPickerSelection(null);
},
[chatDraft],
);
const handleSelectProjectOption = useCallback(
(id: string) => {
// selectProjectOption enforces selectability (worktree-only when
@@ -1835,9 +1830,17 @@ export function NewWorkspaceScreen({
// canCreateWorktree or non-git projects become unselectable.
selectProjectOption(id);
setProjectPickerOpen(false);
setManualPickerSelection(null);
clearManualPickerSelectionForTargetChange(selectedProjectOptionId, id);
},
[selectProjectOption],
[clearManualPickerSelectionForTargetChange, selectProjectOption, selectedProjectOptionId],
);
const handleSelectWorkspaceHost = useCallback(
(id: string) => {
handleSelectHost(id);
clearManualPickerSelectionForTargetChange(selectedServerId, id);
},
[clearManualPickerSelectionForTargetChange, handleSelectHost, selectedServerId],
);
const handleAddProject = useCallback(() => {
@@ -2155,7 +2158,7 @@ export function NewWorkspaceScreen({
host: {
allHosts,
selectedServerId,
onSelect: handleSelectHost,
onSelect: handleSelectWorkspaceHost,
openState: hostPickerOpen,
onOpenChange: handleHostPickerOpenChange,
anchorRef: hostPickerAnchorRef,