authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-02 05:03:47-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-02 05:03:47-04:00
logf14f41b894448298a86865c9114e6700245356e7
treedc9e369d5003c3e53371419ec40ac82b06ce0f8a
parent7e9d5a7efa70f92f91d960a4f50243ba44d805da
signature Signed by PGP key B5690EEEBB952194

fix(web): preserve composer draft during worktree setup (#9197)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

2 files changed, 12 insertions(+), 5 deletions(-)

apps/web/src/composerDraftStore.test.ts+4-2
...@@ -1458,17 +1458,18 @@ describe("composerDraftStore project draft thread mapping", () => {...@@ -1458,17 +1458,18 @@ describe("composerDraftStore project draft thread mapping", () => {
1458 expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.prompt).toBe("keep me");1458 expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.prompt).toBe("keep me");
1459 });1459 });
14601460
1461 it("finalizes a promoted draft after the canonical thread route is active", () => {1461 it("moves composer edits made during promotion to the canonical thread", () => {
1462 const store = useComposerDraftStore.getState();1462 const store = useComposerDraftStore.getState();
1463 store.setProjectDraftThreadId(projectRef, draftId, { threadId });1463 store.setProjectDraftThreadId(projectRef, draftId, { threadId });
1464 store.setPrompt(draftId, "promote me");
1465 markPromotedDraftThread(threadId);1464 markPromotedDraftThread(threadId);
1465 store.setPrompt(draftId, "typed during setup");
14661466
1467 finalizePromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId));1467 finalizePromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId));
14681468
1469 expect(useComposerDraftStore.getState().getDraftThreadByProjectRef(projectRef)).toBeNull();1469 expect(useComposerDraftStore.getState().getDraftThreadByProjectRef(projectRef)).toBeNull();
1470 expect(useComposerDraftStore.getState().getDraftThread(draftId)).toBeNull();1470 expect(useComposerDraftStore.getState().getDraftThread(draftId)).toBeNull();
1471 expect(draftByKey(draftId)).toBeUndefined();1471 expect(draftByKey(draftId)).toBeUndefined();
1472 expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.prompt).toBe("typed during setup");
1472 });1473 });
14731474
1474 it("finalizes a matching materialized draft even when promotion was not pre-marked", () => {1475 it("finalizes a matching materialized draft even when promotion was not pre-marked", () => {
...@@ -1481,6 +1482,7 @@ describe("composerDraftStore project draft thread mapping", () => {...@@ -1481,6 +1482,7 @@ describe("composerDraftStore project draft thread mapping", () => {
1481 expect(useComposerDraftStore.getState().getDraftThreadByProjectRef(projectRef)).toBeNull();1482 expect(useComposerDraftStore.getState().getDraftThreadByProjectRef(projectRef)).toBeNull();
1482 expect(useComposerDraftStore.getState().getDraftThread(draftId)).toBeNull();1483 expect(useComposerDraftStore.getState().getDraftThread(draftId)).toBeNull();
1483 expect(draftByKey(draftId)).toBeUndefined();1484 expect(draftByKey(draftId)).toBeUndefined();
1485 expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.prompt).toBe("promote me");
1484 });1486 });
14851487
1486 it("updates branch context on an existing draft thread", () => {1488 it("updates branch context on an existing draft thread", () => {
apps/web/src/composerDraftStore.ts+8-3
...@@ -1583,6 +1583,7 @@ function removeDraftThreadReferences(...@@ -1583,6 +1583,7 @@ function removeDraftThreadReferences(
1583 | "logicalProjectDraftThreadKeyByLogicalProjectKey"1583 | "logicalProjectDraftThreadKeyByLogicalProjectKey"
1584 >,1584 >,
1585 threadKey: string,1585 threadKey: string,
1586 composerDestination?: ScopedThreadRef,
1586): Pick<1587): Pick<
1587 ComposerDraftStoreState,1588 ComposerDraftStoreState,
1588 | "draftThreadsByThreadKey"1589 | "draftThreadsByThreadKey"
...@@ -1597,7 +1598,11 @@ function removeDraftThreadReferences(...@@ -1597,7 +1598,11 @@ function removeDraftThreadReferences(
1597 const { [threadKey]: _removedDraftThread, ...restDraftThreadsByThreadKey } =1598 const { [threadKey]: _removedDraftThread, ...restDraftThreadsByThreadKey } =
1598 state.draftThreadsByThreadKey;1599 state.draftThreadsByThreadKey;
1599 const { [threadKey]: removedComposerDraft, ...restDraftsByThreadKey } = state.draftsByThreadKey;1600 const { [threadKey]: removedComposerDraft, ...restDraftsByThreadKey } = state.draftsByThreadKey;
1600 revokeDraftThreadPreviewUrls(removedComposerDraft);1601 if (composerDestination && removedComposerDraft) {
1602 restDraftsByThreadKey[composerTargetKey(composerDestination)] = removedComposerDraft;
1603 } else {
1604 revokeDraftThreadPreviewUrls(removedComposerDraft);
1605 }
1601 return {1606 return {
1602 draftsByThreadKey: restDraftsByThreadKey,1607 draftsByThreadKey: restDraftsByThreadKey,
1603 draftThreadsByThreadKey: restDraftThreadsByThreadKey,1608 draftThreadsByThreadKey: restDraftThreadsByThreadKey,
...@@ -2785,10 +2790,10 @@ const composerDraftStore = create<ComposerDraftStoreState>()(...@@ -2785,10 +2790,10 @@ const composerDraftStore = create<ComposerDraftStoreState>()(
2785 }2790 }
2786 set((state) => {2791 set((state) => {
2787 const existing = state.draftThreadsByThreadKey[threadKey];2792 const existing = state.draftThreadsByThreadKey[threadKey];
2788 if (!isDraftThreadPromoting(existing)) {2793 if (!existing || !isDraftThreadPromoting(existing)) {
2789 return state;2794 return state;
2790 }2795 }
2791 return removeDraftThreadReferences(state, threadKey);2796 return removeDraftThreadReferences(state, threadKey, existing.promotedTo ?? undefined);
2792 });2797 });
2793 },2798 },
2794 clearDraftThread: (threadRef) => {2799 clearDraftThread: (threadRef) => {