Skip to content

Commit 3a30fd7

Browse files
Merge branch 'main' into brendan/anthropic-thinking-capabilities
2 parents be45a27 + cfcabcb commit 3a30fd7

9 files changed

Lines changed: 130 additions & 76 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
- Upgraded `ws` to `^8.20.1`. [#1286](https://github.com/sourcebot-dev/sourcebot/pull/1286)
2222
- Upgraded `hono` to `^4.12.24`. [#1289](https://github.com/sourcebot-dev/sourcebot/pull/1289)
2323
- Surfaced an actionable error when the Lighthouse licensing service is unreachable, instead of a generic "unexpected error". [#1293](https://github.com/sourcebot-dev/sourcebot/pull/1293)
24+
- Fixed the selected language model rapidly flipping in local storage after a language model was removed. [#1295](https://github.com/sourcebot-dev/sourcebot/pull/1295)
2425

2526
## [5.0.1] - 2026-06-04
2627

packages/web/src/app/(app)/askgh/[owner]/[repo]/components/landingPage.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ export const LandingPage = ({
7575
}}
7676
className="min-h-[50px]"
7777
isRedirecting={isLoading}
78-
languageModels={languageModels}
7978
selectedSearchScopes={selectedSearchScopes}
8079
searchContexts={[]}
8180
isDisabled={isChatBoxDisabled}

packages/web/src/app/(app)/chat/components/landingPageChatBox.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ export const LandingPageChatBox = ({
4242
}}
4343
className="min-h-[50px]"
4444
isRedirecting={isLoading}
45-
languageModels={languageModels}
4645
selectedSearchScopes={selectedSearchScopes}
4746
searchContexts={searchContexts}
4847
isDisabled={isChatBoxDisabled}

packages/web/src/app/(app)/layout.tsx

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import { CheckoutReturnHandler } from "@/features/billing/checkoutReturnHandler"
3636
import { RoleProvider } from "@/features/auth/roleProvider";
3737
import { HasLicenseProvider } from "@/features/billing/hasLicenseProvider";
3838
import { tryGetLatestSourcebotTag } from "./components/banners/actions";
39+
import { LanguageModelProvider } from "@/features/chat/languageModelContext";
40+
import { getConfiguredLanguageModelsInfo } from "@/features/chat/utils.server";
3941

4042
interface LayoutProps {
4143
children: React.ReactNode;
@@ -175,39 +177,43 @@ export default async function Layout(props: LayoutProps) {
175177
timeoutMs: 3000
176178
});
177179

180+
const languageModels = await getConfiguredLanguageModelsInfo();
181+
178182
return (
179183
<RoleProvider role={role}>
180184
<HasLicenseProvider
181185
hasLicense={offlineLicense !== null || license !== null}
182186
>
183-
<SyntaxGuideProvider>
184-
<div className="fixed inset-0 flex bg-shell">
185-
<SidebarProvider defaultOpen={cookieStore.get("sidebar_state")?.value !== "false"}>
186-
{sidebar}
187-
<div className="flex-1 min-h-0 flex flex-col pt-2 pb-2 pr-2 pl-2 md:pl-0">
188-
<div className="flex-1 min-h-0 bg-background flex flex-col border border-[#e6e6e6] dark:border-[#1d1d1f] rounded-xl overflow-hidden">
189-
<BannerHeightObserver>
190-
<BannerSlot
191-
role={role}
192-
license={license}
193-
offlineLicense={offlineLicense}
194-
hasPermissionSyncEntitlement={hasPermissionSyncEntitlement}
195-
hasPendingFirstSync={hasPendingFirstSync}
196-
currentVersion={SOURCEBOT_VERSION}
197-
latestVersion={latestVersion}
198-
/>
199-
</BannerHeightObserver>
200-
<div className="flex-1 min-h-0 overflow-y-scroll [scrollbar-gutter:stable]">
201-
{children}
187+
<LanguageModelProvider languageModels={languageModels}>
188+
<SyntaxGuideProvider>
189+
<div className="fixed inset-0 flex bg-shell">
190+
<SidebarProvider defaultOpen={cookieStore.get("sidebar_state")?.value !== "false"}>
191+
{sidebar}
192+
<div className="flex-1 min-h-0 flex flex-col pt-2 pb-2 pr-2 pl-2 md:pl-0">
193+
<div className="flex-1 min-h-0 bg-background flex flex-col border border-[#e6e6e6] dark:border-[#1d1d1f] rounded-xl overflow-hidden">
194+
<BannerHeightObserver>
195+
<BannerSlot
196+
role={role}
197+
license={license}
198+
offlineLicense={offlineLicense}
199+
hasPermissionSyncEntitlement={hasPermissionSyncEntitlement}
200+
hasPendingFirstSync={hasPendingFirstSync}
201+
currentVersion={SOURCEBOT_VERSION}
202+
latestVersion={latestVersion}
203+
/>
204+
</BannerHeightObserver>
205+
<div className="flex-1 min-h-0 overflow-y-scroll [scrollbar-gutter:stable]">
206+
{children}
207+
</div>
202208
</div>
203209
</div>
204-
</div>
205-
</SidebarProvider>
206-
</div>
207-
<SyntaxReferenceGuide />
208-
<GitHubStarToast />
209-
<CheckoutReturnHandler />
210-
</SyntaxGuideProvider>
210+
</SidebarProvider>
211+
</div>
212+
<SyntaxReferenceGuide />
213+
<GitHubStarToast />
214+
<CheckoutReturnHandler />
215+
</SyntaxGuideProvider>
216+
</LanguageModelProvider>
211217
</HasLicenseProvider>
212218
</RoleProvider>
213219
)

packages/web/src/ee/features/chat/components/chatThread/chatThread.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,7 @@ export const ChatThread = ({
115115
});
116116
const [isFailedMcpBannerVisible, setIsFailedMcpBannerVisible] = useState(false);
117117

118-
const { selectedLanguageModel } = useSelectedLanguageModel({
119-
languageModels,
120-
});
118+
const { selectedLanguageModel } = useSelectedLanguageModel();
121119

122120
// Refs to capture the latest request params for the transport body.
123121
// The transport is created once (useMemo) but params change over time,
@@ -478,7 +476,6 @@ export const ChatThread = ({
478476
isTurnInProgress={isTurnInProgress}
479477
isNetworkActive={isNetworkActive}
480478
onStop={stop}
481-
languageModels={languageModels}
482479
selectedSearchScopes={selectedSearchScopes}
483480
searchContexts={searchContexts}
484481
isDisabled={languageModels.length === 0}

packages/web/src/features/chat/components/chatBox/chatBox.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { VscodeFileIcon } from "@/app/components/vscodeFileIcon";
44
import { Button } from "@/components/ui/button";
55
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
6-
import { CustomEditor, LanguageModelInfo, MentionElement, RenderElementPropsFor, SearchScope } from "@/features/chat/types";
6+
import { CustomEditor, MentionElement, RenderElementPropsFor, SearchScope } from "@/features/chat/types";
77
import { insertMention, slateContentToString } from "@/features/chat/utils";
88
import { cn } from "@/lib/utils";
99
import { useIsMac } from "@/hooks/useIsMac";
@@ -37,7 +37,6 @@ interface ChatBoxProps {
3737
isTurnInProgress?: boolean;
3838
isNetworkActive?: boolean;
3939
isDisabled?: boolean;
40-
languageModels: LanguageModelInfo[];
4140
selectedSearchScopes: SearchScope[];
4241
searchContexts: SearchContextQuery[];
4342
isLoginWallEnabled: boolean;
@@ -55,7 +54,6 @@ const ChatBoxComponent = ({
5554
isDisabled,
5655
isLoginWallEnabled,
5756
isAuthenticated,
58-
languageModels,
5957
selectedSearchScopes,
6058
searchContexts,
6159
}: ChatBoxProps) => {
@@ -82,9 +80,7 @@ const ChatBoxComponent = ({
8280
return [];
8381
}).flat(),
8482
});
85-
const { selectedLanguageModel } = useSelectedLanguageModel({
86-
languageModels,
87-
});
83+
const { selectedLanguageModel } = useSelectedLanguageModel();
8884
const { toast } = useToast();
8985
const isAskEnabled = useHasEntitlement('ask');
9086
const [isLoginDialogOpen, setIsLoginDialogOpen] = useState<boolean>(false);

packages/web/src/features/chat/components/chatBox/chatBoxToolbar.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@ export const ChatBoxToolbar = ({
3434
onDisabledMcpServerIdsChange,
3535
isAuthenticated,
3636
}: ChatBoxToolbarProps) => {
37-
const { selectedLanguageModel, setSelectedLanguageModel } = useSelectedLanguageModel({
38-
languageModels,
39-
});
37+
const { selectedLanguageModel, setSelectedLanguageModel } = useSelectedLanguageModel();
4038

4139
return (
4240
<>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
'use client';
2+
3+
import { createContext, useEffect, useMemo, type ReactNode } from "react";
4+
import { useLocalStorage } from "usehooks-ts";
5+
import { LanguageModelInfo } from "./types";
6+
import { getLanguageModelKey } from "./utils";
7+
8+
export interface SelectedLanguageModelContextValue {
9+
languageModels: LanguageModelInfo[];
10+
selectedLanguageModel: LanguageModelInfo | undefined;
11+
setSelectedLanguageModel: (model: LanguageModelInfo | undefined) => void;
12+
}
13+
14+
export const SelectedLanguageModelContext = createContext<SelectedLanguageModelContextValue | null>(null);
15+
16+
interface LanguageModelProviderProps {
17+
languageModels: LanguageModelInfo[];
18+
children: ReactNode;
19+
}
20+
21+
// Single owner of the selected-language-model state. Mounted once (in the (app)
22+
// layout), so the selection lives in one place instead of being re-derived by a
23+
// `useSelectedLanguageModel` hook in every consumer. Previously each consumer
24+
// ran its own reset effect against the shared "selectedLanguageModel"
25+
// localStorage key, and because usehooks-ts broadcasts a storage event on every
26+
// write, those instances re-triggered each other into a rapid write loop when a
27+
// model was removed.
28+
export const LanguageModelProvider = ({
29+
languageModels,
30+
children,
31+
}: LanguageModelProviderProps) => {
32+
const fallbackLanguageModel = languageModels.length > 0 ? languageModels[0] : undefined;
33+
const [selectedLanguageModel, setSelectedLanguageModel] = useLocalStorage<LanguageModelInfo | undefined>(
34+
"selectedLanguageModel",
35+
fallbackLanguageModel,
36+
{
37+
initializeWithValue: false,
38+
}
39+
);
40+
41+
// Handle the case where the selected language model is no longer available.
42+
// Reset to the fallback language model in this case. Only write when the
43+
// resolved selection actually differs (compared by key, since the stored
44+
// value is a fresh object reference on every read) — otherwise the effect
45+
// would re-write on every render.
46+
useEffect(() => {
47+
const selectedKey = selectedLanguageModel
48+
? getLanguageModelKey(selectedLanguageModel)
49+
: undefined;
50+
51+
const isSelectedModelAvailable = selectedKey !== undefined && languageModels.some(
52+
(model) => getLanguageModelKey(model) === selectedKey
53+
);
54+
55+
if (isSelectedModelAvailable) {
56+
return;
57+
}
58+
59+
const fallbackKey = fallbackLanguageModel
60+
? getLanguageModelKey(fallbackLanguageModel)
61+
: undefined;
62+
63+
if (fallbackKey !== selectedKey) {
64+
setSelectedLanguageModel(fallbackLanguageModel);
65+
}
66+
}, [
67+
fallbackLanguageModel,
68+
languageModels,
69+
selectedLanguageModel,
70+
setSelectedLanguageModel,
71+
]);
72+
73+
const value = useMemo<SelectedLanguageModelContextValue>(() => ({
74+
languageModels,
75+
selectedLanguageModel,
76+
setSelectedLanguageModel,
77+
}), [languageModels, selectedLanguageModel, setSelectedLanguageModel]);
78+
79+
return (
80+
<SelectedLanguageModelContext.Provider value={value}>
81+
{children}
82+
</SelectedLanguageModelContext.Provider>
83+
);
84+
};
Lines changed: 9 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,17 @@
11
'use client';
22

3-
import { useLocalStorage } from "usehooks-ts";
4-
import { LanguageModelInfo } from "./types";
5-
import { useEffect } from "react";
6-
import { getLanguageModelKey } from "./utils";
3+
import { useContext } from "react";
4+
import { SelectedLanguageModelContext } from "./languageModelContext";
75

8-
type Props = {
9-
languageModels: LanguageModelInfo[];
10-
}
11-
12-
export const useSelectedLanguageModel = ({
13-
languageModels,
14-
}: Props) => {
15-
const fallbackLanguageModel = languageModels.length > 0 ? languageModels[0] : undefined;
16-
const [selectedLanguageModel, setSelectedLanguageModel] = useLocalStorage<LanguageModelInfo | undefined>(
17-
"selectedLanguageModel",
18-
fallbackLanguageModel,
19-
{
20-
initializeWithValue: false,
21-
}
22-
);
23-
24-
// Handle the case where the selected language model is no longer
25-
// available. Reset to the fallback language model in this case.
26-
useEffect(() => {
27-
if (!selectedLanguageModel || !languageModels.find(
28-
(model) => getLanguageModelKey(model) === getLanguageModelKey(selectedLanguageModel)
29-
)) {
30-
setSelectedLanguageModel(fallbackLanguageModel);
31-
}
32-
}, [
33-
fallbackLanguageModel,
34-
languageModels,
35-
selectedLanguageModel,
36-
setSelectedLanguageModel,
37-
]);
6+
export const useSelectedLanguageModel = () => {
7+
const context = useContext(SelectedLanguageModelContext);
8+
if (!context) {
9+
throw new Error("useSelectedLanguageModel must be used within a LanguageModelProvider");
10+
}
3811

12+
const { selectedLanguageModel, setSelectedLanguageModel } = context;
3913
return {
4014
selectedLanguageModel,
4115
setSelectedLanguageModel,
4216
};
43-
}
17+
};

0 commit comments

Comments
 (0)