Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 18 additions & 36 deletions apps/web/src/composerDraftStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,7 @@ import {
clearComposerDraftsEnvironment,
composerDraftHasUserContent,
finalizePromotedDraftThreadByRef,
markPromotedDraftThread,
markPromotedDraftThreadByRef,
markPromotedDraftThreads,
markPromotedDraftThreadsByRef,
type ComposerFileAttachment,
type ComposerImageAttachment,
composerFileNeedsReattach,
Expand Down Expand Up @@ -1200,7 +1197,7 @@ describe("composerDraftStore project draft thread mapping", () => {
interactionMode: "plan",
});
store.setPrompt(draftId, "keep this prompt");
markPromotedDraftThread(threadId);
markPromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId));

store.setLogicalProjectDraftThreadId(scopedProjectKey(projectRef), projectRef, draftId, {
threadId: retryThreadId,
Expand Down Expand Up @@ -1363,12 +1360,12 @@ describe("composerDraftStore project draft thread mapping", () => {
expect(draftByKey(draftId)).toBeUndefined();
});

it("marks a promoted draft by thread id without deleting composer state", () => {
it("marks a promoted draft by scoped ref without deleting composer state", () => {
const store = useComposerDraftStore.getState();
store.setProjectDraftThreadId(projectRef, draftId, { threadId });
store.setPrompt(draftId, "promote me");

markPromotedDraftThread(threadId);
markPromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId));

expect(useComposerDraftStore.getState().getDraftThreadByProjectRef(projectRef)).toBeNull();
expect(useComposerDraftStore.getState().getDraftThread(draftId)?.promotedTo).toEqual(
Expand All @@ -1393,20 +1390,20 @@ describe("composerDraftStore project draft thread mapping", () => {
const threadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId);
store.setPrompt(threadRef, "keep me");

markPromotedDraftThread(threadId);
markPromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId));

expect(useComposerDraftStore.getState().getDraftThread(threadRef)).toBeNull();
expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.prompt).toBe("keep me");
});

it("marks promoted drafts from an iterable of server thread ids", () => {
it("promotes a draft without changing another thread's draft", () => {
const store = useComposerDraftStore.getState();
store.setProjectDraftThreadId(projectRef, draftId, { threadId });
store.setPrompt(draftId, "promote me");
store.setProjectDraftThreadId(otherProjectRef, otherDraftId, { threadId: otherThreadId });
store.setPrompt(otherDraftId, "keep me");

markPromotedDraftThreads([threadId]);
markPromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId));

expect(useComposerDraftStore.getState().getDraftThread(draftId)?.promotedTo).toEqual(
scopeThreadRef(TEST_ENVIRONMENT_ID, threadId),
Expand All @@ -1418,7 +1415,7 @@ describe("composerDraftStore project draft thread mapping", () => {
expect(draftByKey(otherDraftId)?.prompt).toBe("keep me");
});

it("marks every matching scoped draft when multiple environments share a thread id", () => {
it("promotes matching thread ids separately for each environment", () => {
const store = useComposerDraftStore.getState();
const localThreadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId);
const remoteThreadRef = scopeThreadRef(OTHER_TEST_ENVIRONMENT_ID, threadId);
Expand All @@ -1428,7 +1425,16 @@ describe("composerDraftStore project draft thread mapping", () => {
store.setProjectDraftThreadId(remoteProjectRef, remoteDraftId, { threadId });
store.setPrompt(remoteDraftId, "remote draft");

markPromotedDraftThread(threadId);
markPromotedDraftThreadByRef(localThreadRef);

expect(store.getDraftThreadByProjectRef(projectRef)).toBeNull();
expect(store.getDraftThreadByProjectRef(remoteProjectRef)?.threadId).toBe(threadId);
expect(store.getDraftThreadByRef(localThreadRef)?.promotedTo).toEqual(localThreadRef);
expect(store.getDraftThreadByRef(remoteThreadRef)?.promotedTo).toBeNull();
expect(draftByKey(localDraftId)?.prompt).toBe("local draft");
expect(draftByKey(remoteDraftId)?.prompt).toBe("remote draft");

markPromotedDraftThreadByRef(remoteThreadRef);

expect(store.getDraftThreadByProjectRef(projectRef)).toBeNull();
expect(store.getDraftThreadByProjectRef(remoteProjectRef)).toBeNull();
Expand All @@ -1451,34 +1457,10 @@ describe("composerDraftStore project draft thread mapping", () => {
expect(draftByKey(draftId)?.prompt).toBe("promote me");
});

it("only marks iterable promotion cleanup entries for the matching environment refs", () => {
const store = useComposerDraftStore.getState();
store.setProjectDraftThreadId(projectRef, draftId, { threadId });
store.setPrompt(draftId, "promote me");

markPromotedDraftThreadsByRef([scopeThreadRef(OTHER_TEST_ENVIRONMENT_ID, threadId)]);

expect(useComposerDraftStore.getState().getDraftThreadByProjectRef(projectRef)?.threadId).toBe(
threadId,
);
expect(draftByKey(draftId)?.prompt).toBe("promote me");
});

it("keeps existing server-thread composer drafts during iterable promotion cleanup", () => {
const store = useComposerDraftStore.getState();
const threadRef = scopeThreadRef(TEST_ENVIRONMENT_ID, threadId);
store.setPrompt(threadRef, "keep me");

markPromotedDraftThreads([threadId]);

expect(useComposerDraftStore.getState().getDraftThread(threadRef)).toBeNull();
expect(draftFor(threadId, TEST_ENVIRONMENT_ID)?.prompt).toBe("keep me");
});

it("moves composer edits made during promotion to the canonical thread", () => {
const store = useComposerDraftStore.getState();
store.setProjectDraftThreadId(projectRef, draftId, { threadId });
markPromotedDraftThread(threadId);
markPromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId));
store.setPrompt(draftId, "typed during setup");

finalizePromotedDraftThreadByRef(scopeThreadRef(TEST_ENVIRONMENT_ID, threadId));
Expand Down
35 changes: 0 additions & 35 deletions apps/web/src/composerDraftStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4106,29 +4106,6 @@ export function useEffectiveComposerModelState(input: {
);
}

/**
* Mark a draft thread as promoting once the server has materialized the same thread id.
*
* Use the single-thread helper for live `thread.created` events and the
* iterable helper for bootstrap/recovery paths that discover multiple server
* threads at once.
*/
export function markPromotedDraftThread(threadId: ThreadId): void {
const store = useComposerDraftStore.getState();
const draftThreadTargets: ComposerThreadTarget[] = [];
for (const [draftId, draftThread] of Object.entries(store.draftThreadsByThreadKey)) {
if (draftThread.threadId === threadId) {
draftThreadTargets.push(DraftId.make(draftId));
}
}
if (draftThreadTargets.length === 0) {
return;
}
for (const draftThreadTarget of draftThreadTargets) {
store.markDraftThreadPromoting(draftThreadTarget);
}
}

export function markPromotedDraftThreadByRef(threadRef: ScopedThreadRef): void {
const draftStore = useComposerDraftStore.getState();
for (const [draftId, draftThread] of Object.entries(draftStore.draftThreadsByThreadKey)) {
Expand All @@ -4141,18 +4118,6 @@ export function markPromotedDraftThreadByRef(threadRef: ScopedThreadRef): void {
}
}

export function markPromotedDraftThreads(serverThreadIds: Iterable<ThreadId>): void {
for (const threadId of serverThreadIds) {
markPromotedDraftThread(threadId);
}
}

export function markPromotedDraftThreadsByRef(serverThreadRefs: Iterable<ScopedThreadRef>): void {
for (const threadRef of serverThreadRefs) {
markPromotedDraftThreadByRef(threadRef);
}
}

export function finalizePromotedDraftThreadByRef(threadRef: ScopedThreadRef): void {
const draftStore = useComposerDraftStore.getState();
for (const [draftId, draftThread] of Object.entries(draftStore.draftThreadsByThreadKey)) {
Expand Down
Loading