summaryrefslogtreecommitdiffhomepage
path: root/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/app')
-rw-r--r--src/app/store.svelte.ts22
-rw-r--r--src/app/store.test.ts82
2 files changed, 98 insertions, 6 deletions
diff --git a/src/app/store.svelte.ts b/src/app/store.svelte.ts
index 08a7cd8..52136ee 100644
--- a/src/app/store.svelte.ts
+++ b/src/app/store.svelte.ts
@@ -1077,15 +1077,20 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore {
* stamped with the conversation's actual `workspaceId`, NOT the viewer's
* currently active workspace.
*/
- function openConversation(conversationId: string, workspaceId: string): void {
+ function openConversation(conversationId: string, workspaceId: string, model?: string): void {
if (chatStores.has(conversationId)) return;
- const store = createChatFor(conversationId, activeModel, workspaceId);
+ // Stamp the tab with the conversation's PERSISTED model when the open
+ // broadcast carries it (falling back to the current global selection) —
+ // never let the globally-active model overwrite a conversation's real one
+ // (fix-toolcall-duplicate-keys plan Part 4).
+ const tabModel = model ?? activeModel;
+ const store = createChatFor(conversationId, tabModel, workspaceId);
chatStores.set(conversationId, store);
void store.load();
subscribeChat(conversationId);
tabsStore.openTab({
conversationId,
- model: activeModel,
+ model: tabModel,
title: "Conversation",
workspaceId,
});
@@ -1172,13 +1177,18 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore {
const existingIds = new Set(chatStores.keys());
for (const conv of data.conversations) {
if (!existingIds.has(conv.id)) {
- const store = createChatFor(conv.id, activeModel, conv.workspaceId);
+ // Stamp the tab with the conversation's PERSISTED model (when the
+ // list carries it) instead of the global active model — the fix for
+ // tabs showing/overwriting the wrong model
+ // (fix-toolcall-duplicate-keys plan Part 4).
+ const model = conv.model ?? activeModel;
+ const store = createChatFor(conv.id, model, conv.workspaceId);
chatStores.set(conv.id, store);
void store.load();
subscribeChat(conv.id);
tabsStore.openTab({
conversationId: conv.id,
- model: activeModel,
+ model,
title: conv.title,
workspaceId: conv.workspaceId,
});
@@ -1218,7 +1228,7 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore {
onMessage: handleServerMessage,
onChat: handleChatMessage,
onConversationOpen(msg: ConversationOpenMessage): void {
- openConversation(msg.conversationId, msg.workspaceId);
+ openConversation(msg.conversationId, msg.workspaceId, msg.model);
},
onConversationStatusChanged(msg: ConversationStatusChangedMessage): void {
const { conversationId, status, workspaceId } = msg;
diff --git a/src/app/store.test.ts b/src/app/store.test.ts
index c41efc2..f8b878a 100644
--- a/src/app/store.test.ts
+++ b/src/app/store.test.ts
@@ -799,6 +799,88 @@ describe("createAppStore", () => {
store.dispose();
});
+ it("fetchOpenConversations stamps tabs with the conversation's persisted model (not the global active model)", async () => {
+ const ws = fakeSocket();
+ const fetchImpl: typeof fetch = async (
+ input: string | URL | Request,
+ init?: RequestInit,
+ ): Promise<Response> => {
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
+ if (url.endsWith("/models")) {
+ return new Response(JSON.stringify({ models: ["opencode-1/deepseek-v4-flash"] }), {
+ status: 200,
+ });
+ }
+ // The authoritative open-conversation list — one conversation whose
+ // persisted model differs from the global activeModel.
+ if (url.includes("/conversations?status=")) {
+ return new Response(
+ JSON.stringify({
+ conversations: [
+ {
+ id: "conv-model",
+ createdAt: 1000,
+ lastActivityAt: 2000,
+ title: "Model conv",
+ status: "idle",
+ workspaceId: "default",
+ model: "openrouter/stealth/ox-alpha",
+ },
+ ],
+ }),
+ { status: 200 },
+ );
+ }
+ return new Response(JSON.stringify({ chunks: [], latestSeq: 0 }), { status: 200 });
+ };
+
+ const store = createAppStore({
+ socketFactory: () => ws,
+ fetchImpl,
+ localStorage: createFakeStorage(),
+ });
+ ws.resolveOpen();
+
+ // global activeModel is the default until anything changes it
+ expect(store.activeModel).toBe("opencode-1/deepseek-v4-flash");
+
+ // fetchOpenConversations (boot reconcile) opens the conversation's tab —
+ // stamped with the PERSISTED model from the list, not activeModel.
+ await vi.waitFor(() => {
+ const tab = store.tabs.find((t) => t.conversationId === "conv-model");
+ expect(tab?.model).toBe("openrouter/stealth/ox-alpha");
+ });
+
+ store.dispose();
+ });
+
+ it("conversation.open WS message carrying a model stamps the tab with it", async () => {
+ const ws = fakeSocket();
+ const store = createAppStore({
+ socketFactory: () => ws,
+ fetchImpl: fakeFetchImpl(),
+ localStorage: createFakeStorage(),
+ });
+ ws.resolveOpen();
+
+ // The user stays on their current (draft) tab; the backend broadcasts
+ // `conversation.open` with the conversation's persisted model.
+ ws.feedServerMessage({
+ type: "conversation.open",
+ conversationId: "conv-ws",
+ workspaceId: "default",
+ model: "openrouter/stealth/ox-alpha",
+ });
+
+ await vi.waitFor(() => {
+ const tab = store.tabs.find((t) => t.conversationId === "conv-ws");
+ expect(tab?.model).toBe("openrouter/stealth/ox-alpha");
+ expect(tab?.workspaceId).toBe("default");
+ });
+
+ store.dispose();
+ });
+
it("chat.delta routes to the matching tab only", () => {
const ws = fakeSocket();
const store = createAppStore({