Skip to content

Commit a3b5ee8

Browse files
committed
persist session-specific mode and chat model selection across tab switches and session loads
1 parent 6bf5ee1 commit a3b5ee8

File tree

6 files changed

+104
-75
lines changed

6 files changed

+104
-75
lines changed

core/index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,10 @@ export interface Session {
272272
title: string;
273273
workspaceDirectory: string;
274274
history: ChatHistoryItem[];
275+
/** Optional: per-session UI mode (chat/agent/plan/background) */
276+
mode?: MessageModes;
277+
/** Optional: title of the selected chat model for this session */
278+
chatModelTitle?: string | null;
275279
}
276280

277281
export interface BaseSessionMetadata {

core/util/history.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,12 @@ export class HistoryManager {
107107
title: session.title,
108108
workspaceDirectory: session.workspaceDirectory,
109109
history: session.history,
110-
};
110+
// Optional fields persisted when present
111+
...(session.mode ? { mode: session.mode } : {}),
112+
...(typeof session.chatModelTitle === "undefined"
113+
? {}
114+
: { chatModelTitle: session.chatModelTitle }),
115+
} as Session;
111116
fs.writeFileSync(
112117
getSessionFilePath(session.sessionId),
113118
JSON.stringify(orderedSession, undefined, 2),

gui/src/components/TabBar/TabBar.tsx

Lines changed: 0 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,12 @@ import {
1111
removeTab,
1212
setActiveTab,
1313
setTabs,
14-
setTabMode,
15-
setTabModel,
1614
} from "../../redux/slices/tabsSlice";
1715
import { AppDispatch, RootState } from "../../redux/store";
1816
import { loadSession, saveCurrentSession } from "../../redux/thunks/session";
1917
import { varWithFallback } from "../../styles/theme";
2018
import { useAuth } from "../../context/Auth";
2119
import { selectSelectedChatModel } from "../../redux/slices/configSlice";
22-
import { updateSelectedModelByRole } from "../../redux/thunks/updateSelectedModelByRole";
23-
import { setMode } from "../../redux/slices/sessionSlice";
2420

2521
// Haven't set up theme colors for tabs yet
2622
// Will keep it simple and choose from existing ones. Comments show vars we could use
@@ -144,11 +140,6 @@ export const TabBar = React.forwardRef<HTMLDivElement>((_, ref) => {
144140
(state: RootState) => state.session.history.length > 0,
145141
);
146142
const tabs = useSelector((state: RootState) => state.tabs.tabs);
147-
const selectedModel = useAppSelector(selectSelectedChatModel);
148-
const { selectedProfile } = useAuth();
149-
const mode = useAppSelector((state: RootState) => state.session.mode);
150-
const activeTab = tabs.find((tab) => tab.isActive);
151-
const activeTabId = activeTab?.id;
152143

153144
// Simple UUID generator for our needs
154145
const generateId = useCallback(() => {
@@ -167,22 +158,6 @@ export const TabBar = React.forwardRef<HTMLDivElement>((_, ref) => {
167158
);
168159
}, [currentSessionId, currentSessionTitle]);
169160

170-
// Persist selected model into the active tab in Redux
171-
useEffect(() => {
172-
if (activeTabId && selectedModel?.title) {
173-
dispatch(
174-
setTabModel({ id: activeTabId, modelTitle: selectedModel.title }),
175-
);
176-
}
177-
}, [activeTabId, selectedModel?.title, mode, dispatch]);
178-
179-
// Persist the currently selected mode into the active tab
180-
useEffect(() => {
181-
if (activeTabId && mode) {
182-
dispatch(setTabMode({ id: activeTabId, mode }));
183-
}
184-
}, [activeTabId, mode, dispatch]);
185-
186161
const handleNewTab = async () => {
187162
// Save current session before creating new one
188163
if (hasHistory) {
@@ -199,8 +174,6 @@ export const TabBar = React.forwardRef<HTMLDivElement>((_, ref) => {
199174
title: `Chat ${tabs.length + 1}`,
200175
isActive: true,
201176
sessionId: undefined,
202-
modelTitle: selectedModel?.title,
203-
mode,
204177
}),
205178
);
206179
};
@@ -226,22 +199,6 @@ export const TabBar = React.forwardRef<HTMLDivElement>((_, ref) => {
226199
}
227200

228201
dispatch(setActiveTab(id));
229-
230-
// restore mode for this tab (if set)
231-
if (targetTab.mode) {
232-
dispatch(setMode(targetTab.mode));
233-
}
234-
235-
// restore model for this tab
236-
if (targetTab.modelTitle && selectedProfile) {
237-
void dispatch(
238-
updateSelectedModelByRole({
239-
selectedProfile,
240-
role: "chat",
241-
modelTitle: targetTab.modelTitle,
242-
}),
243-
);
244-
}
245202
};
246203

247204
const handleTabClose = async (id: string) => {

gui/src/redux/slices/sessionSlice.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,9 @@ export const sessionSlice = createSlice({
693693
state.history = payload.history as any;
694694
state.title = payload.title;
695695
state.id = payload.sessionId;
696+
if (payload.mode) {
697+
state.mode = payload.mode as MessageModes;
698+
}
696699
} else {
697700
state.history = [];
698701
state.title = NEW_SESSION_TITLE;

gui/src/redux/slices/tabsSlice.ts

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
2-
import { MessageModes } from "core";
32

43
export interface Tab {
54
id: string;
65
title: string;
76
isActive: boolean;
87
sessionId?: string;
9-
modelTitle?: string; // store per-tab model
10-
mode?: MessageModes; // store per-tab mode ('chat' | 'plan' | 'agent')
118
}
129

1310
interface TabsState {
@@ -20,8 +17,6 @@ export const INITIAL_TABS_STATE: TabsState = {
2017
id: Date.now().toString(36) + Math.random().toString(36).substring(2),
2118
title: "Chat 1",
2219
isActive: true,
23-
modelTitle: undefined,
24-
mode: "chat",
2520
},
2621
],
2722
};
@@ -42,26 +37,6 @@ export const tabsSlice = createSlice({
4237
tab.id === id ? { ...tab, ...updates } : tab,
4338
);
4439
},
45-
// Set the model title for a specific tab
46-
setTabModel: (
47-
state,
48-
action: PayloadAction<{ id: string; modelTitle: string }>,
49-
) => {
50-
const { id, modelTitle } = action.payload;
51-
state.tabs = state.tabs.map((tab) =>
52-
tab.id === id ? { ...tab, modelTitle } : tab,
53-
);
54-
},
55-
// Set the mode for a specific tab
56-
setTabMode: (
57-
state,
58-
action: PayloadAction<{ id: string; mode: MessageModes }>,
59-
) => {
60-
const { id, mode } = action.payload;
61-
state.tabs = state.tabs.map((tab) =>
62-
tab.id === id ? { ...tab, mode } : tab,
63-
);
64-
},
6540
addTab: (state, action: PayloadAction<Tab>) => {
6641
state.tabs = state.tabs
6742
.map((tab) => ({
@@ -147,7 +122,6 @@ export const tabsSlice = createSlice({
147122
title: currentSessionTitle,
148123
isActive: true,
149124
sessionId: currentSessionId,
150-
modelTitle: undefined,
151125
});
152126
}
153127
},
@@ -157,8 +131,6 @@ export const tabsSlice = createSlice({
157131
export const {
158132
setTabs,
159133
updateTab,
160-
setTabModel,
161-
setTabMode,
162134
addTab,
163135
removeTab,
164136
setActiveTab,

gui/src/redux/thunks/session.ts

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,10 @@ export const loadSession = createAsyncThunk<
103103
ThunkApiType
104104
>(
105105
"session/load",
106-
async ({ sessionId, saveCurrentSession: save }, { extra, dispatch }) => {
106+
async (
107+
{ sessionId, saveCurrentSession: save },
108+
{ extra, dispatch, getState },
109+
) => {
107110
if (save) {
108111
const result = await dispatch(
109112
saveCurrentSession({
@@ -115,6 +118,32 @@ export const loadSession = createAsyncThunk<
115118
}
116119
const session = await getSession(extra.ideMessenger, sessionId);
117120
dispatch(newSession(session));
121+
122+
// Restore selected chat model from session, if present
123+
const chatModelTitle = (session as any).chatModelTitle as
124+
| string
125+
| null
126+
| undefined;
127+
if (chatModelTitle) {
128+
const state = getState();
129+
const org = state.profiles.organizations.find(
130+
(o) => o.id === state.profiles.selectedOrganizationId,
131+
);
132+
const selectedProfile =
133+
org?.profiles.find((p) => p.id === state.profiles.selectedProfileId) ??
134+
null;
135+
if (selectedProfile) {
136+
await dispatch(
137+
(
138+
await import("../thunks/updateSelectedModelByRole")
139+
).updateSelectedModelByRole({
140+
role: "chat",
141+
modelTitle: chatModelTitle,
142+
selectedProfile,
143+
}) as any,
144+
);
145+
}
146+
}
118147
},
119148
);
120149

@@ -127,7 +156,10 @@ export const loadRemoteSession = createAsyncThunk<
127156
ThunkApiType
128157
>(
129158
"session/loadRemote",
130-
async ({ remoteId, saveCurrentSession: save }, { extra, dispatch }) => {
159+
async (
160+
{ remoteId, saveCurrentSession: save },
161+
{ extra, dispatch, getState },
162+
) => {
131163
if (save) {
132164
const result = await dispatch(
133165
saveCurrentSession({
@@ -139,6 +171,32 @@ export const loadRemoteSession = createAsyncThunk<
139171
}
140172
const session = await getRemoteSession(extra.ideMessenger, remoteId);
141173
dispatch(newSession(session));
174+
175+
// Restore selected chat model from session, if present
176+
const chatModelTitle = (session as any).chatModelTitle as
177+
| string
178+
| null
179+
| undefined;
180+
if (chatModelTitle) {
181+
const state = getState();
182+
const org = state.profiles.organizations.find(
183+
(o) => o.id === state.profiles.selectedOrganizationId,
184+
);
185+
const selectedProfile =
186+
org?.profiles.find((p) => p.id === state.profiles.selectedProfileId) ??
187+
null;
188+
if (selectedProfile) {
189+
await dispatch(
190+
(
191+
await import("../thunks/updateSelectedModelByRole")
192+
).updateSelectedModelByRole({
193+
role: "chat",
194+
modelTitle: chatModelTitle,
195+
selectedProfile,
196+
}) as any,
197+
);
198+
}
199+
}
142200
},
143201
);
144202

@@ -168,6 +226,32 @@ export const loadLastSession = createAsyncThunk<void, void, ThunkApiType>(
168226
session = await getSession(extra.ideMessenger, lastSessionId);
169227
}
170228
dispatch(newSession(session));
229+
230+
// Restore selected chat model from session, if present
231+
const chatModelTitle = (session as any).chatModelTitle as
232+
| string
233+
| null
234+
| undefined;
235+
if (chatModelTitle) {
236+
const state = getState();
237+
const org = state.profiles.organizations.find(
238+
(o) => o.id === state.profiles.selectedOrganizationId,
239+
);
240+
const selectedProfile =
241+
org?.profiles.find((p) => p.id === state.profiles.selectedProfileId) ??
242+
null;
243+
if (selectedProfile) {
244+
await dispatch(
245+
(
246+
await import("../thunks/updateSelectedModelByRole")
247+
).updateSelectedModelByRole({
248+
role: "chat",
249+
modelTitle: chatModelTitle,
250+
selectedProfile,
251+
}) as any,
252+
);
253+
}
254+
}
171255
},
172256
);
173257

@@ -246,12 +330,16 @@ export const saveCurrentSession = createAsyncThunk<
246330
title = NEW_SESSION_TITLE;
247331
}
248332

333+
const selectedChatModel = selectSelectedChatModel(state);
334+
249335
const session: Session = {
250336
sessionId: state.session.id,
251337
title,
252338
workspaceDirectory: window.workspacePaths?.[0] || "",
253339
history: state.session.history,
254-
};
340+
mode: state.session.mode,
341+
chatModelTitle: selectedChatModel?.title ?? null,
342+
} as Session;
255343

256344
const result = await dispatch(updateSession(session));
257345
unwrapResult(result);

0 commit comments

Comments
 (0)