From 70af97eac1b09a2eb6f2b7519dbc6ef1bbc74278 Mon Sep 17 00:00:00 2001 From: ferenci84 Date: Sat, 6 Sep 2025 07:30:37 +0200 Subject: [PATCH 1/5] add per-tab model persistence and selection functionality --- gui/src/components/TabBar/TabBar.tsx | 30 ++++++++++++++++++++++++++++ gui/src/redux/slices/tabsSlice.ts | 14 +++++++++++++ 2 files changed, 44 insertions(+) diff --git a/gui/src/components/TabBar/TabBar.tsx b/gui/src/components/TabBar/TabBar.tsx index 6d24853c360..fb27625c759 100644 --- a/gui/src/components/TabBar/TabBar.tsx +++ b/gui/src/components/TabBar/TabBar.tsx @@ -1,6 +1,7 @@ import { XMarkIcon } from "@heroicons/react/24/outline"; import React, { useCallback, useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; +import { useAppSelector } from "../../redux/hooks"; import styled from "styled-components"; import { defaultBorderRadius } from ".."; import { newSession } from "../../redux/slices/sessionSlice"; @@ -10,10 +11,14 @@ import { removeTab, setActiveTab, setTabs, + setTabModel, } from "../../redux/slices/tabsSlice"; import { AppDispatch, RootState } from "../../redux/store"; import { loadSession, saveCurrentSession } from "../../redux/thunks/session"; import { varWithFallback } from "../../styles/theme"; +import { useAuth } from "../../context/Auth"; +import { selectSelectedChatModel } from "../../redux/slices/configSlice"; +import { updateSelectedModelByRole } from "../../redux/thunks/updateSelectedModelByRole"; // Haven't set up theme colors for tabs yet // Will keep it simple and choose from existing ones. Comments show vars we could use @@ -137,6 +142,11 @@ export const TabBar = React.forwardRef((_, ref) => { (state: RootState) => state.session.history.length > 0, ); const tabs = useSelector((state: RootState) => state.tabs.tabs); + const selectedModel = useAppSelector(selectSelectedChatModel); + const { selectedProfile } = useAuth(); + const activeTab = tabs.find((tab) => tab.isActive); + const activeTabId = activeTab?.id; + const activeTabModel = activeTab?.modelTitle; // Simple UUID generator for our needs const generateId = useCallback(() => { @@ -155,6 +165,15 @@ export const TabBar = React.forwardRef((_, ref) => { ); }, [currentSessionId, currentSessionTitle]); + // Persist selected model into the active tab in Redux + useEffect(() => { + if (activeTabId && selectedModel?.title) { + dispatch( + setTabModel({ id: activeTabId, modelTitle: selectedModel.title }), + ); + } + }, [activeTabId, selectedModel?.title, dispatch]); + const handleNewTab = async () => { // Save current session before creating new one if (hasHistory) { @@ -171,6 +190,7 @@ export const TabBar = React.forwardRef((_, ref) => { title: `Chat ${tabs.length + 1}`, isActive: true, sessionId: undefined, + modelTitle: selectedModel?.title, }), ); }; @@ -196,6 +216,16 @@ export const TabBar = React.forwardRef((_, ref) => { } dispatch(setActiveTab(id)); + // restore model for this tab + if (targetTab.modelTitle && selectedProfile) { + void dispatch( + updateSelectedModelByRole({ + selectedProfile, + role: "chat", + modelTitle: targetTab.modelTitle, + }), + ); + } }; const handleTabClose = async (id: string) => { diff --git a/gui/src/redux/slices/tabsSlice.ts b/gui/src/redux/slices/tabsSlice.ts index bceab688b1b..49782713a18 100644 --- a/gui/src/redux/slices/tabsSlice.ts +++ b/gui/src/redux/slices/tabsSlice.ts @@ -5,6 +5,7 @@ export interface Tab { title: string; isActive: boolean; sessionId?: string; + modelTitle?: string; // store per-tab model } interface TabsState { @@ -17,6 +18,7 @@ const initialState: TabsState = { id: Date.now().toString(36) + Math.random().toString(36).substring(2), title: "Chat 1", isActive: true, + modelTitle: undefined, }, ], }; @@ -37,6 +39,16 @@ export const tabsSlice = createSlice({ tab.id === id ? { ...tab, ...updates } : tab, ); }, + // Set the model title for a specific tab + setTabModel: ( + state, + action: PayloadAction<{ id: string; modelTitle: string }>, + ) => { + const { id, modelTitle } = action.payload; + state.tabs = state.tabs.map((tab) => + tab.id === id ? { ...tab, modelTitle } : tab, + ); + }, addTab: (state, action: PayloadAction) => { state.tabs = state.tabs .map((tab) => ({ @@ -122,6 +134,7 @@ export const tabsSlice = createSlice({ title: currentSessionTitle, isActive: true, sessionId: currentSessionId, + modelTitle: undefined, }); } }, @@ -131,6 +144,7 @@ export const tabsSlice = createSlice({ export const { setTabs, updateTab, + setTabModel, addTab, removeTab, setActiveTab, From de68ac96447ed30ad0892bb5e5d17aaf77ddfb0d Mon Sep 17 00:00:00 2001 From: ferenci84 Date: Sat, 6 Sep 2025 12:40:12 +0200 Subject: [PATCH 2/5] persist and restore per-tab mode state in TabBar component and tabsSlice --- gui/src/components/TabBar/TabBar.tsx | 20 ++++++++++++++++++-- gui/src/redux/slices/tabsSlice.ts | 14 ++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/gui/src/components/TabBar/TabBar.tsx b/gui/src/components/TabBar/TabBar.tsx index fb27625c759..476232faaf9 100644 --- a/gui/src/components/TabBar/TabBar.tsx +++ b/gui/src/components/TabBar/TabBar.tsx @@ -11,6 +11,7 @@ import { removeTab, setActiveTab, setTabs, + setTabMode, setTabModel, } from "../../redux/slices/tabsSlice"; import { AppDispatch, RootState } from "../../redux/store"; @@ -19,6 +20,7 @@ import { varWithFallback } from "../../styles/theme"; import { useAuth } from "../../context/Auth"; import { selectSelectedChatModel } from "../../redux/slices/configSlice"; import { updateSelectedModelByRole } from "../../redux/thunks/updateSelectedModelByRole"; +import { setMode } from "../../redux/slices/sessionSlice"; // Haven't set up theme colors for tabs yet // Will keep it simple and choose from existing ones. Comments show vars we could use @@ -144,9 +146,9 @@ export const TabBar = React.forwardRef((_, ref) => { const tabs = useSelector((state: RootState) => state.tabs.tabs); const selectedModel = useAppSelector(selectSelectedChatModel); const { selectedProfile } = useAuth(); + const mode = useAppSelector((state: RootState) => state.session.mode); const activeTab = tabs.find((tab) => tab.isActive); const activeTabId = activeTab?.id; - const activeTabModel = activeTab?.modelTitle; // Simple UUID generator for our needs const generateId = useCallback(() => { @@ -172,7 +174,14 @@ export const TabBar = React.forwardRef((_, ref) => { setTabModel({ id: activeTabId, modelTitle: selectedModel.title }), ); } - }, [activeTabId, selectedModel?.title, dispatch]); + }, [activeTabId, selectedModel?.title, mode, dispatch]); + + // Persist the currently selected mode into the active tab + useEffect(() => { + if (activeTabId && mode) { + dispatch(setTabMode({ id: activeTabId, mode })); + } + }, [activeTabId, mode, dispatch]); const handleNewTab = async () => { // Save current session before creating new one @@ -191,6 +200,7 @@ export const TabBar = React.forwardRef((_, ref) => { isActive: true, sessionId: undefined, modelTitle: selectedModel?.title, + mode, }), ); }; @@ -216,6 +226,12 @@ export const TabBar = React.forwardRef((_, ref) => { } dispatch(setActiveTab(id)); + + // restore mode for this tab (if set) + if (targetTab.mode) { + dispatch(setMode(targetTab.mode)); + } + // restore model for this tab if (targetTab.modelTitle && selectedProfile) { void dispatch( diff --git a/gui/src/redux/slices/tabsSlice.ts b/gui/src/redux/slices/tabsSlice.ts index 49782713a18..47415e69911 100644 --- a/gui/src/redux/slices/tabsSlice.ts +++ b/gui/src/redux/slices/tabsSlice.ts @@ -1,4 +1,5 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; +import { MessageModes } from "core"; export interface Tab { id: string; @@ -6,6 +7,7 @@ export interface Tab { isActive: boolean; sessionId?: string; modelTitle?: string; // store per-tab model + mode?: MessageModes; // store per-tab mode ('chat' | 'plan' | 'agent') } interface TabsState { @@ -19,6 +21,7 @@ const initialState: TabsState = { title: "Chat 1", isActive: true, modelTitle: undefined, + mode: "chat", }, ], }; @@ -49,6 +52,16 @@ export const tabsSlice = createSlice({ tab.id === id ? { ...tab, modelTitle } : tab, ); }, + // Set the mode for a specific tab + setTabMode: ( + state, + action: PayloadAction<{ id: string; mode: MessageModes }>, + ) => { + const { id, mode } = action.payload; + state.tabs = state.tabs.map((tab) => + tab.id === id ? { ...tab, mode } : tab, + ); + }, addTab: (state, action: PayloadAction) => { state.tabs = state.tabs .map((tab) => ({ @@ -145,6 +158,7 @@ export const { setTabs, updateTab, setTabModel, + setTabMode, addTab, removeTab, setActiveTab, From a3b5ee840f75e3e1e628bcfa55abeae3d8963095 Mon Sep 17 00:00:00 2001 From: ferenci84 Date: Sun, 9 Nov 2025 12:23:18 +0100 Subject: [PATCH 3/5] persist session-specific mode and chat model selection across tab switches and session loads --- core/index.d.ts | 4 ++ core/util/history.ts | 7 ++- gui/src/components/TabBar/TabBar.tsx | 43 ------------- gui/src/redux/slices/sessionSlice.ts | 3 + gui/src/redux/slices/tabsSlice.ts | 28 --------- gui/src/redux/thunks/session.ts | 94 +++++++++++++++++++++++++++- 6 files changed, 104 insertions(+), 75 deletions(-) diff --git a/core/index.d.ts b/core/index.d.ts index de08c1ea1f8..f31b62ed7d6 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -272,6 +272,10 @@ export interface Session { title: string; workspaceDirectory: string; history: ChatHistoryItem[]; + /** Optional: per-session UI mode (chat/agent/plan/background) */ + mode?: MessageModes; + /** Optional: title of the selected chat model for this session */ + chatModelTitle?: string | null; } export interface BaseSessionMetadata { diff --git a/core/util/history.ts b/core/util/history.ts index 02e3f8feeee..2a9fa1505bb 100644 --- a/core/util/history.ts +++ b/core/util/history.ts @@ -107,7 +107,12 @@ export class HistoryManager { title: session.title, workspaceDirectory: session.workspaceDirectory, history: session.history, - }; + // Optional fields persisted when present + ...(session.mode ? { mode: session.mode } : {}), + ...(typeof session.chatModelTitle === "undefined" + ? {} + : { chatModelTitle: session.chatModelTitle }), + } as Session; fs.writeFileSync( getSessionFilePath(session.sessionId), JSON.stringify(orderedSession, undefined, 2), diff --git a/gui/src/components/TabBar/TabBar.tsx b/gui/src/components/TabBar/TabBar.tsx index 476232faaf9..13867647969 100644 --- a/gui/src/components/TabBar/TabBar.tsx +++ b/gui/src/components/TabBar/TabBar.tsx @@ -11,16 +11,12 @@ import { removeTab, setActiveTab, setTabs, - setTabMode, - setTabModel, } from "../../redux/slices/tabsSlice"; import { AppDispatch, RootState } from "../../redux/store"; import { loadSession, saveCurrentSession } from "../../redux/thunks/session"; import { varWithFallback } from "../../styles/theme"; import { useAuth } from "../../context/Auth"; import { selectSelectedChatModel } from "../../redux/slices/configSlice"; -import { updateSelectedModelByRole } from "../../redux/thunks/updateSelectedModelByRole"; -import { setMode } from "../../redux/slices/sessionSlice"; // Haven't set up theme colors for tabs yet // Will keep it simple and choose from existing ones. Comments show vars we could use @@ -144,11 +140,6 @@ export const TabBar = React.forwardRef((_, ref) => { (state: RootState) => state.session.history.length > 0, ); const tabs = useSelector((state: RootState) => state.tabs.tabs); - const selectedModel = useAppSelector(selectSelectedChatModel); - const { selectedProfile } = useAuth(); - const mode = useAppSelector((state: RootState) => state.session.mode); - const activeTab = tabs.find((tab) => tab.isActive); - const activeTabId = activeTab?.id; // Simple UUID generator for our needs const generateId = useCallback(() => { @@ -167,22 +158,6 @@ export const TabBar = React.forwardRef((_, ref) => { ); }, [currentSessionId, currentSessionTitle]); - // Persist selected model into the active tab in Redux - useEffect(() => { - if (activeTabId && selectedModel?.title) { - dispatch( - setTabModel({ id: activeTabId, modelTitle: selectedModel.title }), - ); - } - }, [activeTabId, selectedModel?.title, mode, dispatch]); - - // Persist the currently selected mode into the active tab - useEffect(() => { - if (activeTabId && mode) { - dispatch(setTabMode({ id: activeTabId, mode })); - } - }, [activeTabId, mode, dispatch]); - const handleNewTab = async () => { // Save current session before creating new one if (hasHistory) { @@ -199,8 +174,6 @@ export const TabBar = React.forwardRef((_, ref) => { title: `Chat ${tabs.length + 1}`, isActive: true, sessionId: undefined, - modelTitle: selectedModel?.title, - mode, }), ); }; @@ -226,22 +199,6 @@ export const TabBar = React.forwardRef((_, ref) => { } dispatch(setActiveTab(id)); - - // restore mode for this tab (if set) - if (targetTab.mode) { - dispatch(setMode(targetTab.mode)); - } - - // restore model for this tab - if (targetTab.modelTitle && selectedProfile) { - void dispatch( - updateSelectedModelByRole({ - selectedProfile, - role: "chat", - modelTitle: targetTab.modelTitle, - }), - ); - } }; const handleTabClose = async (id: string) => { diff --git a/gui/src/redux/slices/sessionSlice.ts b/gui/src/redux/slices/sessionSlice.ts index 0c20cdf8924..d6e853748b7 100644 --- a/gui/src/redux/slices/sessionSlice.ts +++ b/gui/src/redux/slices/sessionSlice.ts @@ -693,6 +693,9 @@ export const sessionSlice = createSlice({ state.history = payload.history as any; state.title = payload.title; state.id = payload.sessionId; + if (payload.mode) { + state.mode = payload.mode as MessageModes; + } } else { state.history = []; state.title = NEW_SESSION_TITLE; diff --git a/gui/src/redux/slices/tabsSlice.ts b/gui/src/redux/slices/tabsSlice.ts index fb63e57823f..670ca9966e3 100644 --- a/gui/src/redux/slices/tabsSlice.ts +++ b/gui/src/redux/slices/tabsSlice.ts @@ -1,13 +1,10 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import { MessageModes } from "core"; export interface Tab { id: string; title: string; isActive: boolean; sessionId?: string; - modelTitle?: string; // store per-tab model - mode?: MessageModes; // store per-tab mode ('chat' | 'plan' | 'agent') } interface TabsState { @@ -20,8 +17,6 @@ export const INITIAL_TABS_STATE: TabsState = { id: Date.now().toString(36) + Math.random().toString(36).substring(2), title: "Chat 1", isActive: true, - modelTitle: undefined, - mode: "chat", }, ], }; @@ -42,26 +37,6 @@ export const tabsSlice = createSlice({ tab.id === id ? { ...tab, ...updates } : tab, ); }, - // Set the model title for a specific tab - setTabModel: ( - state, - action: PayloadAction<{ id: string; modelTitle: string }>, - ) => { - const { id, modelTitle } = action.payload; - state.tabs = state.tabs.map((tab) => - tab.id === id ? { ...tab, modelTitle } : tab, - ); - }, - // Set the mode for a specific tab - setTabMode: ( - state, - action: PayloadAction<{ id: string; mode: MessageModes }>, - ) => { - const { id, mode } = action.payload; - state.tabs = state.tabs.map((tab) => - tab.id === id ? { ...tab, mode } : tab, - ); - }, addTab: (state, action: PayloadAction) => { state.tabs = state.tabs .map((tab) => ({ @@ -147,7 +122,6 @@ export const tabsSlice = createSlice({ title: currentSessionTitle, isActive: true, sessionId: currentSessionId, - modelTitle: undefined, }); } }, @@ -157,8 +131,6 @@ export const tabsSlice = createSlice({ export const { setTabs, updateTab, - setTabModel, - setTabMode, addTab, removeTab, setActiveTab, diff --git a/gui/src/redux/thunks/session.ts b/gui/src/redux/thunks/session.ts index e89acb78f6b..a7e14d237e3 100644 --- a/gui/src/redux/thunks/session.ts +++ b/gui/src/redux/thunks/session.ts @@ -103,7 +103,10 @@ export const loadSession = createAsyncThunk< ThunkApiType >( "session/load", - async ({ sessionId, saveCurrentSession: save }, { extra, dispatch }) => { + async ( + { sessionId, saveCurrentSession: save }, + { extra, dispatch, getState }, + ) => { if (save) { const result = await dispatch( saveCurrentSession({ @@ -115,6 +118,32 @@ export const loadSession = createAsyncThunk< } const session = await getSession(extra.ideMessenger, sessionId); dispatch(newSession(session)); + + // Restore selected chat model from session, if present + const chatModelTitle = (session as any).chatModelTitle as + | string + | null + | undefined; + if (chatModelTitle) { + const state = getState(); + const org = state.profiles.organizations.find( + (o) => o.id === state.profiles.selectedOrganizationId, + ); + const selectedProfile = + org?.profiles.find((p) => p.id === state.profiles.selectedProfileId) ?? + null; + if (selectedProfile) { + await dispatch( + ( + await import("../thunks/updateSelectedModelByRole") + ).updateSelectedModelByRole({ + role: "chat", + modelTitle: chatModelTitle, + selectedProfile, + }) as any, + ); + } + } }, ); @@ -127,7 +156,10 @@ export const loadRemoteSession = createAsyncThunk< ThunkApiType >( "session/loadRemote", - async ({ remoteId, saveCurrentSession: save }, { extra, dispatch }) => { + async ( + { remoteId, saveCurrentSession: save }, + { extra, dispatch, getState }, + ) => { if (save) { const result = await dispatch( saveCurrentSession({ @@ -139,6 +171,32 @@ export const loadRemoteSession = createAsyncThunk< } const session = await getRemoteSession(extra.ideMessenger, remoteId); dispatch(newSession(session)); + + // Restore selected chat model from session, if present + const chatModelTitle = (session as any).chatModelTitle as + | string + | null + | undefined; + if (chatModelTitle) { + const state = getState(); + const org = state.profiles.organizations.find( + (o) => o.id === state.profiles.selectedOrganizationId, + ); + const selectedProfile = + org?.profiles.find((p) => p.id === state.profiles.selectedProfileId) ?? + null; + if (selectedProfile) { + await dispatch( + ( + await import("../thunks/updateSelectedModelByRole") + ).updateSelectedModelByRole({ + role: "chat", + modelTitle: chatModelTitle, + selectedProfile, + }) as any, + ); + } + } }, ); @@ -168,6 +226,32 @@ export const loadLastSession = createAsyncThunk( session = await getSession(extra.ideMessenger, lastSessionId); } dispatch(newSession(session)); + + // Restore selected chat model from session, if present + const chatModelTitle = (session as any).chatModelTitle as + | string + | null + | undefined; + if (chatModelTitle) { + const state = getState(); + const org = state.profiles.organizations.find( + (o) => o.id === state.profiles.selectedOrganizationId, + ); + const selectedProfile = + org?.profiles.find((p) => p.id === state.profiles.selectedProfileId) ?? + null; + if (selectedProfile) { + await dispatch( + ( + await import("../thunks/updateSelectedModelByRole") + ).updateSelectedModelByRole({ + role: "chat", + modelTitle: chatModelTitle, + selectedProfile, + }) as any, + ); + } + } }, ); @@ -246,12 +330,16 @@ export const saveCurrentSession = createAsyncThunk< title = NEW_SESSION_TITLE; } + const selectedChatModel = selectSelectedChatModel(state); + const session: Session = { sessionId: state.session.id, title, workspaceDirectory: window.workspacePaths?.[0] || "", history: state.session.history, - }; + mode: state.session.mode, + chatModelTitle: selectedChatModel?.title ?? null, + } as Session; const result = await dispatch(updateSession(session)); unwrapResult(result); From ea6e871db56dadc53b007c70e2b5fdc52332704a Mon Sep 17 00:00:00 2001 From: ferenci84 Date: Mon, 10 Nov 2025 22:42:23 +0100 Subject: [PATCH 4/5] remove unnecessary type assertions and clean up session loading logic --- gui/src/components/TabBar/TabBar.tsx | 3 --- gui/src/redux/thunks/session.ts | 23 +++++++---------------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/gui/src/components/TabBar/TabBar.tsx b/gui/src/components/TabBar/TabBar.tsx index 13867647969..6d24853c360 100644 --- a/gui/src/components/TabBar/TabBar.tsx +++ b/gui/src/components/TabBar/TabBar.tsx @@ -1,7 +1,6 @@ import { XMarkIcon } from "@heroicons/react/24/outline"; import React, { useCallback, useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { useAppSelector } from "../../redux/hooks"; import styled from "styled-components"; import { defaultBorderRadius } from ".."; import { newSession } from "../../redux/slices/sessionSlice"; @@ -15,8 +14,6 @@ import { import { AppDispatch, RootState } from "../../redux/store"; import { loadSession, saveCurrentSession } from "../../redux/thunks/session"; import { varWithFallback } from "../../styles/theme"; -import { useAuth } from "../../context/Auth"; -import { selectSelectedChatModel } from "../../redux/slices/configSlice"; // Haven't set up theme colors for tabs yet // Will keep it simple and choose from existing ones. Comments show vars we could use diff --git a/gui/src/redux/thunks/session.ts b/gui/src/redux/thunks/session.ts index a7e14d237e3..9dbc5edd430 100644 --- a/gui/src/redux/thunks/session.ts +++ b/gui/src/redux/thunks/session.ts @@ -120,10 +120,7 @@ export const loadSession = createAsyncThunk< dispatch(newSession(session)); // Restore selected chat model from session, if present - const chatModelTitle = (session as any).chatModelTitle as - | string - | null - | undefined; + const chatModelTitle = session.chatModelTitle; if (chatModelTitle) { const state = getState(); const org = state.profiles.organizations.find( @@ -140,7 +137,7 @@ export const loadSession = createAsyncThunk< role: "chat", modelTitle: chatModelTitle, selectedProfile, - }) as any, + }), ); } } @@ -173,10 +170,7 @@ export const loadRemoteSession = createAsyncThunk< dispatch(newSession(session)); // Restore selected chat model from session, if present - const chatModelTitle = (session as any).chatModelTitle as - | string - | null - | undefined; + const chatModelTitle = session.chatModelTitle; if (chatModelTitle) { const state = getState(); const org = state.profiles.organizations.find( @@ -193,7 +187,7 @@ export const loadRemoteSession = createAsyncThunk< role: "chat", modelTitle: chatModelTitle, selectedProfile, - }) as any, + }), ); } } @@ -228,10 +222,7 @@ export const loadLastSession = createAsyncThunk( dispatch(newSession(session)); // Restore selected chat model from session, if present - const chatModelTitle = (session as any).chatModelTitle as - | string - | null - | undefined; + const chatModelTitle = session.chatModelTitle; if (chatModelTitle) { const state = getState(); const org = state.profiles.organizations.find( @@ -248,7 +239,7 @@ export const loadLastSession = createAsyncThunk( role: "chat", modelTitle: chatModelTitle, selectedProfile, - }) as any, + }), ); } } @@ -339,7 +330,7 @@ export const saveCurrentSession = createAsyncThunk< history: state.session.history, mode: state.session.mode, chatModelTitle: selectedChatModel?.title ?? null, - } as Session; + }; const result = await dispatch(updateSession(session)); unwrapResult(result); From b2f565e5e35b89b5e04966ab89fc5cd213ac9ec7 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Tue, 18 Nov 2025 16:17:13 -0800 Subject: [PATCH 5/5] fix: followup on persisted chat model pr --- core/util/history.ts | 16 ++--- gui/src/redux/slices/sessionSlice.ts | 2 +- gui/src/redux/thunks/session.ts | 92 ++++++++++------------------ 3 files changed, 41 insertions(+), 69 deletions(-) diff --git a/core/util/history.ts b/core/util/history.ts index 2a9fa1505bb..65d7d53f0bf 100644 --- a/core/util/history.ts +++ b/core/util/history.ts @@ -1,6 +1,6 @@ import * as fs from "fs"; -import { Session, BaseSessionMetadata } from "../index.js"; +import { BaseSessionMetadata, Session } from "../index.js"; import { ListHistoryOptions } from "../protocol/core.js"; import { NEW_SESSION_TITLE } from "./constants.js"; @@ -107,12 +107,14 @@ export class HistoryManager { title: session.title, workspaceDirectory: session.workspaceDirectory, history: session.history, - // Optional fields persisted when present - ...(session.mode ? { mode: session.mode } : {}), - ...(typeof session.chatModelTitle === "undefined" - ? {} - : { chatModelTitle: session.chatModelTitle }), - } as Session; + }; + if (session.mode) { + orderedSession.mode = session.mode; + } + if (session.chatModelTitle !== undefined) { + orderedSession.chatModelTitle = session.chatModelTitle; + } + fs.writeFileSync( getSessionFilePath(session.sessionId), JSON.stringify(orderedSession, undefined, 2), diff --git a/gui/src/redux/slices/sessionSlice.ts b/gui/src/redux/slices/sessionSlice.ts index d6e853748b7..3656af0c185 100644 --- a/gui/src/redux/slices/sessionSlice.ts +++ b/gui/src/redux/slices/sessionSlice.ts @@ -694,7 +694,7 @@ export const sessionSlice = createSlice({ state.title = payload.title; state.id = payload.sessionId; if (payload.mode) { - state.mode = payload.mode as MessageModes; + state.mode = payload.mode; } } else { state.history = []; diff --git a/gui/src/redux/thunks/session.ts b/gui/src/redux/thunks/session.ts index 9dbc5edd430..03a94fa7aa2 100644 --- a/gui/src/redux/thunks/session.ts +++ b/gui/src/redux/thunks/session.ts @@ -5,6 +5,7 @@ import { NEW_SESSION_TITLE } from "core/util/constants"; import { renderChatMessage } from "core/util/messageContent"; import { IIdeMessenger } from "../../context/IdeMessenger"; import { selectSelectedChatModel } from "../slices/configSlice"; +import { selectSelectedProfile } from "../slices/profilesSlice"; import { deleteSessionMetadata, newSession, @@ -13,6 +14,7 @@ import { updateSessionMetadata, } from "../slices/sessionSlice"; import { ThunkApiType } from "../store"; +import { updateSelectedModelByRole } from "../thunks/updateSelectedModelByRole"; const MAX_TITLE_LENGTH = 100; @@ -120,26 +122,8 @@ export const loadSession = createAsyncThunk< dispatch(newSession(session)); // Restore selected chat model from session, if present - const chatModelTitle = session.chatModelTitle; - if (chatModelTitle) { - const state = getState(); - const org = state.profiles.organizations.find( - (o) => o.id === state.profiles.selectedOrganizationId, - ); - const selectedProfile = - org?.profiles.find((p) => p.id === state.profiles.selectedProfileId) ?? - null; - if (selectedProfile) { - await dispatch( - ( - await import("../thunks/updateSelectedModelByRole") - ).updateSelectedModelByRole({ - role: "chat", - modelTitle: chatModelTitle, - selectedProfile, - }), - ); - } + if (session.chatModelTitle) { + dispatch(selectChatModelForProfile(session.chatModelTitle)); } }, ); @@ -170,26 +154,32 @@ export const loadRemoteSession = createAsyncThunk< dispatch(newSession(session)); // Restore selected chat model from session, if present - const chatModelTitle = session.chatModelTitle; - if (chatModelTitle) { - const state = getState(); - const org = state.profiles.organizations.find( - (o) => o.id === state.profiles.selectedOrganizationId, + if (session.chatModelTitle) { + dispatch(selectChatModelForProfile(session.chatModelTitle)); + } + }, +); + +export const selectChatModelForProfile = createAsyncThunk< + void, + string, + ThunkApiType +>( + "session/selectModelForCurrentProfile", + async (modelTitle, { extra, dispatch, getState }) => { + const state = getState(); + const modelMatch = state.config.config?.modelsByRole?.chat?.find( + (m) => m.title === modelTitle, + ); + const selectedProfile = selectSelectedProfile(state); + if (selectedProfile && modelMatch) { + await dispatch( + updateSelectedModelByRole({ + role: "chat", + modelTitle: modelTitle, + selectedProfile, + }), ); - const selectedProfile = - org?.profiles.find((p) => p.id === state.profiles.selectedProfileId) ?? - null; - if (selectedProfile) { - await dispatch( - ( - await import("../thunks/updateSelectedModelByRole") - ).updateSelectedModelByRole({ - role: "chat", - modelTitle: chatModelTitle, - selectedProfile, - }), - ); - } } }, ); @@ -220,28 +210,8 @@ export const loadLastSession = createAsyncThunk( session = await getSession(extra.ideMessenger, lastSessionId); } dispatch(newSession(session)); - - // Restore selected chat model from session, if present - const chatModelTitle = session.chatModelTitle; - if (chatModelTitle) { - const state = getState(); - const org = state.profiles.organizations.find( - (o) => o.id === state.profiles.selectedOrganizationId, - ); - const selectedProfile = - org?.profiles.find((p) => p.id === state.profiles.selectedProfileId) ?? - null; - if (selectedProfile) { - await dispatch( - ( - await import("../thunks/updateSelectedModelByRole") - ).updateSelectedModelByRole({ - role: "chat", - modelTitle: chatModelTitle, - selectedProfile, - }), - ); - } + if (session.chatModelTitle) { + dispatch(selectChatModelForProfile(session.chatModelTitle)); } }, );