Skip to content

Commit cacbc7c

Browse files
committed
Persist last selected model when opening a new thread
- Track `lastSelectedModel` and `lastSelectedProvider` globally in `composerDraftStore` - Update `setModel`/`setProvider` to write these globals on explicit non-null selection - Pre-populate new thread drafts with the last selected model/provider in `useHandleNewThread`Persist last selected model when opening a new thread
1 parent 46ea594 commit cacbc7c

File tree

3 files changed

+68
-2
lines changed

3 files changed

+68
-2
lines changed

apps/web/src/composerDraftStore.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,8 @@ describe("composerDraftStore setModel", () => {
372372
draftsByThreadId: {},
373373
draftThreadsByThreadId: {},
374374
projectDraftThreadIdByProjectId: {},
375+
lastSelectedModel: null,
376+
lastSelectedProvider: null,
375377
});
376378
});
377379

@@ -384,6 +386,23 @@ describe("composerDraftStore setModel", () => {
384386
"gpt-5.3-codex",
385387
);
386388
});
389+
390+
it("updates lastSelectedModel when a non-null model is set", () => {
391+
const store = useComposerDraftStore.getState();
392+
393+
store.setModel(threadId, "gpt-5.3-codex");
394+
395+
expect(useComposerDraftStore.getState().lastSelectedModel).toBe("gpt-5.3-codex");
396+
});
397+
398+
it("does not update lastSelectedModel when model is set to null", () => {
399+
const store = useComposerDraftStore.getState();
400+
401+
store.setModel(threadId, "gpt-5.3-codex");
402+
store.setModel(threadId, null);
403+
404+
expect(useComposerDraftStore.getState().lastSelectedModel).toBe("gpt-5.3-codex");
405+
});
387406
});
388407

389408
describe("composerDraftStore setProvider", () => {
@@ -394,6 +413,8 @@ describe("composerDraftStore setProvider", () => {
394413
draftsByThreadId: {},
395414
draftThreadsByThreadId: {},
396415
projectDraftThreadIdByProjectId: {},
416+
lastSelectedModel: null,
417+
lastSelectedProvider: null,
397418
});
398419
});
399420

@@ -413,6 +434,14 @@ describe("composerDraftStore setProvider", () => {
413434

414435
expect(useComposerDraftStore.getState().draftsByThreadId[threadId]).toBeUndefined();
415436
});
437+
438+
it("updates lastSelectedProvider when a non-null provider is set", () => {
439+
const store = useComposerDraftStore.getState();
440+
441+
store.setProvider(threadId, "codex");
442+
443+
expect(useComposerDraftStore.getState().lastSelectedProvider).toBe("codex");
444+
});
416445
});
417446

418447
describe("composerDraftStore runtime and interaction settings", () => {

apps/web/src/composerDraftStore.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ interface PersistedComposerDraftStoreState {
9797
draftsByThreadId: Record<ThreadId, PersistedComposerThreadDraftState>;
9898
draftThreadsByThreadId: Record<ThreadId, PersistedDraftThreadState>;
9999
projectDraftThreadIdByProjectId: Record<ProjectId, ThreadId>;
100+
lastSelectedModel: string | null;
101+
lastSelectedProvider: ProviderKind | null;
100102
}
101103

102104
interface ComposerThreadDraftState {
@@ -130,6 +132,8 @@ interface ComposerDraftStoreState {
130132
draftsByThreadId: Record<ThreadId, ComposerThreadDraftState>;
131133
draftThreadsByThreadId: Record<ThreadId, DraftThreadState>;
132134
projectDraftThreadIdByProjectId: Record<ProjectId, ThreadId>;
135+
lastSelectedModel: string | null;
136+
lastSelectedProvider: ProviderKind | null;
133137
getDraftThreadByProjectId: (projectId: ProjectId) => ProjectDraftThread | null;
134138
getDraftThread: (threadId: ThreadId) => DraftThreadState | null;
135139
setProjectDraftThreadId: (
@@ -185,6 +189,8 @@ const EMPTY_PERSISTED_DRAFT_STORE_STATE: PersistedComposerDraftStoreState = {
185189
draftsByThreadId: {},
186190
draftThreadsByThreadId: {},
187191
projectDraftThreadIdByProjectId: {},
192+
lastSelectedModel: null,
193+
lastSelectedProvider: null,
188194
};
189195

190196
const EMPTY_IMAGES: ComposerImageAttachment[] = [];
@@ -450,10 +456,17 @@ function normalizePersistedComposerDraftState(value: unknown): PersistedComposer
450456
...(codexFastMode ? { codexFastMode } : {}),
451457
};
452458
}
459+
const lastSelectedModel =
460+
typeof candidate.lastSelectedModel === "string"
461+
? (normalizeModelSlug(candidate.lastSelectedModel) ?? null)
462+
: null;
463+
const lastSelectedProvider = normalizeProviderKind(candidate.lastSelectedProvider);
453464
return {
454465
draftsByThreadId: nextDraftsByThreadId,
455466
draftThreadsByThreadId,
456467
projectDraftThreadIdByProjectId,
468+
lastSelectedModel,
469+
lastSelectedProvider,
457470
};
458471
}
459472

@@ -563,6 +576,8 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()(
563576
draftsByThreadId: {},
564577
draftThreadsByThreadId: {},
565578
projectDraftThreadIdByProjectId: {},
579+
lastSelectedModel: null,
580+
lastSelectedProvider: null,
566581
getDraftThreadByProjectId: (projectId) => {
567582
if (projectId.length === 0) {
568583
return null;
@@ -841,7 +856,13 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()(
841856
} else {
842857
nextDraftsByThreadId[threadId] = nextDraft;
843858
}
844-
return { draftsByThreadId: nextDraftsByThreadId };
859+
const nextState: Partial<ComposerDraftStoreState> = {
860+
draftsByThreadId: nextDraftsByThreadId,
861+
};
862+
if (normalizedProvider !== null) {
863+
nextState.lastSelectedProvider = normalizedProvider;
864+
}
865+
return nextState;
845866
});
846867
},
847868
setModel: (threadId, model) => {
@@ -868,7 +889,13 @@ export const useComposerDraftStore = create<ComposerDraftStoreState>()(
868889
} else {
869890
nextDraftsByThreadId[threadId] = nextDraft;
870891
}
871-
return { draftsByThreadId: nextDraftsByThreadId };
892+
const nextState: Partial<ComposerDraftStoreState> = {
893+
draftsByThreadId: nextDraftsByThreadId,
894+
};
895+
if (normalizedModel !== null) {
896+
nextState.lastSelectedModel = normalizedModel;
897+
}
898+
return nextState;
872899
});
873900
},
874901
setRuntimeMode: (threadId, runtimeMode) => {

apps/web/src/hooks/useHandleNewThread.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ export function useHandleNewThread() {
8989
const threadId = newThreadId();
9090
const createdAt = new Date().toISOString();
9191
return (async () => {
92+
const { lastSelectedModel, lastSelectedProvider, setModel, setProvider } =
93+
useComposerDraftStore.getState();
94+
9295
setProjectDraftThreadId(projectId, threadId, {
9396
createdAt,
9497
branch: options?.branch ?? null,
@@ -97,6 +100,13 @@ export function useHandleNewThread() {
97100
runtimeMode: DEFAULT_RUNTIME_MODE,
98101
});
99102

103+
if (lastSelectedModel) {
104+
setModel(threadId, lastSelectedModel);
105+
}
106+
if (lastSelectedProvider) {
107+
setProvider(threadId, lastSelectedProvider);
108+
}
109+
100110
await navigate({
101111
to: "/$threadId",
102112
params: { threadId },

0 commit comments

Comments
 (0)