authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-01 19:26:07-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-01 19:26:07-07:00
log0e1570bde5b64458449a45989f1b56d6fd651883
tree15b40f93cec5f336f577f2b50fae67c79ca4e353
parentc37fd136edf546fd58daeac146548436c1fa38c2
signature Signed by PGP key B5690EEEBB952194

fix(web): project default model works on the hosted app (#9142)

Co-authored-by: Claude Code <noreply@anthropic.com>

8 files changed, 124 insertions(+), 20 deletions(-)

apps/web/src/components/settings/IntegrationsSettings.tsx+4-2
...@@ -408,6 +408,7 @@ function AgentBrowserAccessSetting() {...@@ -408,6 +408,7 @@ function AgentBrowserAccessSetting() {
408408
409 return (409 return (
410 <SettingsRow410 <SettingsRow
411 serverScoped
411 {...searchableSetting("agent-browser-access")}412 {...searchableSetting("agent-browser-access")}
412 description="Let agents open and drive the preview browser. When off, the browser tools and the instructions describing them are withheld from agent sessions. Your own browser panel is unaffected."413 description="Let agents open and drive the preview browser. When off, the browser tools and the instructions describing them are withheld from agent sessions. Your own browser panel is unaffected."
413 status={414 status={
...@@ -516,8 +517,9 @@ export function IntegrationsSettingsPanel() {...@@ -516,8 +517,9 @@ export function IntegrationsSettingsPanel() {
516 return (517 return (
517 <SettingsPageContainer>518 <SettingsPageContainer>
518 <SettingsSection id="browser" title="Browser">519 <SettingsSection id="browser" title="Browser">
519 {/* Server-authoritative, so it stays editable on every client and sits520 {/* Server-authoritative, so it stays editable on any client anchored to
520 outside the block covering the desktop-only defaults. */}521 a server; `serverScoped` covers the hosted app, which has none. It
522 sits outside the block covering the desktop-only defaults. */}
521 <AgentBrowserAccessSetting />523 <AgentBrowserAccessSetting />
522 {previewDefaultsDisabled ? (524 {previewDefaultsDisabled ? (
523 <DesktopOnlyBrowserDefaults>{previewDefaults}</DesktopOnlyBrowserDefaults>525 <DesktopOnlyBrowserDefaults>{previewDefaults}</DesktopOnlyBrowserDefaults>
apps/web/src/components/settings/ProjectSettingsPanel.tsx+26-11
...@@ -39,6 +39,7 @@ import { useComposerDraftStore } from "../../composerDraftStore";...@@ -39,6 +39,7 @@ import { useComposerDraftStore } from "../../composerDraftStore";
39import { isElectron } from "../../env";39import { isElectron } from "../../env";
40import {40import {
41 useClientSettings,41 useClientSettings,
42 useEnvironmentSettings,
42 useUpdateClientSettings,43 useUpdateClientSettings,
43 usePrimarySettings,44 usePrimarySettings,
44} from "../../hooks/useSettings";45} from "../../hooks/useSettings";
...@@ -69,7 +70,7 @@ import {...@@ -69,7 +70,7 @@ import {
69import { useEnvironments, usePrimaryEnvironmentId } from "../../state/environments";70import { useEnvironments, usePrimaryEnvironmentId } from "../../state/environments";
70import { useProjects, useThreadShells } from "../../state/entities";71import { useProjects, useThreadShells } from "../../state/entities";
71import { projectEnvironment } from "../../state/projects";72import { projectEnvironment } from "../../state/projects";
72import { primaryServerProvidersAtom, serverEnvironment } from "../../state/server";73import { EMPTY_SERVER_PROVIDERS, serverEnvironment } from "../../state/server";
73import { useAtomCommand } from "../../state/use-atom-command";74import { useAtomCommand } from "../../state/use-atom-command";
74import { ProviderModelPicker } from "../chat/ProviderModelPicker";75import { ProviderModelPicker } from "../chat/ProviderModelPicker";
75import { TraitsPicker } from "../chat/TraitsPicker";76import { TraitsPicker } from "../chat/TraitsPicker";
...@@ -292,10 +293,20 @@ export function ProjectSettingsPanel({ projectKey }: { projectKey: string }) {...@@ -292,10 +293,20 @@ export function ProjectSettingsPanel({ projectKey }: { projectKey: string }) {
292function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {293function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {
293 const navigate = useNavigate();294 const navigate = useNavigate();
294 const primaryEnvironmentId = usePrimaryEnvironmentId();295 const primaryEnvironmentId = usePrimaryEnvironmentId();
296 const representative =
297 group.memberProjects.find(
298 (member) => member.environmentId === group.environmentId && member.id === group.id,
299 ) ?? group.memberProjects[0]!;
295 const settings = usePrimarySettings();300 const settings = usePrimarySettings();
301 // Provider instances and model options belong to the environment that runs
302 // the project's threads. The hosted app has no primary environment, so
303 // reading them from there would show "No providers available" everywhere.
304 const projectSettings = useEnvironmentSettings(representative.environmentId);
305 const serverProviders =
306 useAtomValue(serverEnvironment.providersValueAtom(representative.environmentId)) ??
307 EMPTY_SERVER_PROVIDERS;
296 const updateClientSettings = useUpdateClientSettings();308 const updateClientSettings = useUpdateClientSettings();
297 const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings);309 const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings);
298 const serverProviders = useAtomValue(primaryServerProvidersAtom);
299 const threads = useThreadShells();310 const threads = useThreadShells();
300 const updateProject = useAtomCommand(projectEnvironment.update, { reportFailure: false });311 const updateProject = useAtomCommand(projectEnvironment.update, { reportFailure: false });
301 const deleteProject = useAtomCommand(projectEnvironment.delete, { reportFailure: false });312 const deleteProject = useAtomCommand(projectEnvironment.delete, { reportFailure: false });
...@@ -321,10 +332,6 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {...@@ -321,10 +332,6 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {
321 },332 },
322 });333 });
323334
324 const representative =
325 group.memberProjects.find(
326 (member) => member.environmentId === group.environmentId && member.id === group.id,
327 ) ?? group.memberProjects[0]!;
328 const faviconPath = representative.faviconPath ?? null;335 const faviconPath = representative.faviconPath ?? null;
329 const pickProjectFavicon =336 const pickProjectFavicon =
330 typeof window !== "undefined" &&337 typeof window !== "undefined" &&
...@@ -422,14 +429,22 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {...@@ -422,14 +429,22 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {
422 const instanceEntries = useMemo(429 const instanceEntries = useMemo(
423 () =>430 () =>
424 sortProviderInstanceEntries(431 sortProviderInstanceEntries(
425 applyProviderInstanceSettings(deriveProviderInstanceEntries(serverProviders), settings),432 applyProviderInstanceSettings(
433 deriveProviderInstanceEntries(serverProviders),
434 projectSettings,
435 ),
426 ),436 ),
427 [serverProviders, settings],437 [serverProviders, projectSettings],
428 );438 );
429 const modelOptionsByInstance = useMemo(439 const modelOptionsByInstance = useMemo(
430 () =>440 () =>
431 getCustomModelOptionsByInstance(settings, serverProviders, resolvedInstanceId, resolvedModel),441 getCustomModelOptionsByInstance(
432 [resolvedInstanceId, resolvedModel, serverProviders, settings],442 projectSettings,
443 serverProviders,
444 resolvedInstanceId,
445 resolvedModel,
446 ),
447 [resolvedInstanceId, resolvedModel, serverProviders, projectSettings],
433 );448 );
434 const activeEntry = instanceEntries.find((entry) => entry.instanceId === resolvedInstanceId);449 const activeEntry = instanceEntries.find((entry) => entry.instanceId === resolvedInstanceId);
435 const setDefaultModel = useCallback(450 const setDefaultModel = useCallback(
...@@ -864,7 +879,7 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {...@@ -864,7 +879,7 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {
864 onPromptChange={() => {}}879 onPromptChange={() => {}}
865 modelOptions={resolvedSelection.options ?? []}880 modelOptions={resolvedSelection.options ?? []}
866 allowPromptInjectedEffort={false}881 allowPromptInjectedEffort={false}
867 planModeEnabled={settings.planModeEnabled}882 planModeEnabled={projectSettings.planModeEnabled}
868 triggerVariant="outline"883 triggerVariant="outline"
869 triggerClassName="min-w-0 max-w-none shrink-0 text-foreground/90 hover:text-foreground"884 triggerClassName="min-w-0 max-w-none shrink-0 text-foreground/90 hover:text-foreground"
870 onModelOptionsChange={(nextOptions) => {885 onModelOptionsChange={(nextOptions) => {
apps/web/src/components/settings/SettingsPanels.tsx+10
...@@ -1827,6 +1827,7 @@ function LegacyFeaturesSection() {...@@ -1827,6 +1827,7 @@ function LegacyFeaturesSection() {
1827 }1827 }
1828 />1828 />
1829 <SettingsRow1829 <SettingsRow
1830 serverScoped
1830 {...searchableSetting("legacy-token-streaming")}1831 {...searchableSetting("legacy-token-streaming")}
1831 description="Paints assistant output token by token instead of in complete chunks. Not recommended: it is significantly slower, and long responses become harder to follow. Kept only for compatibility with the old behavior."1832 description="Paints assistant output token by token instead of in complete chunks. Not recommended: it is significantly slower, and long responses become harder to follow. Kept only for compatibility with the old behavior."
1832 control={1833 control={
...@@ -1970,6 +1971,7 @@ export function GeneralSettingsPanel() {...@@ -1970,6 +1971,7 @@ export function GeneralSettingsPanel() {
1970 {supportsAutoSettlement ? (1971 {supportsAutoSettlement ? (
1971 <>1972 <>
1972 <SettingsRow1973 <SettingsRow
1974 serverScoped
1973 {...searchableSetting("auto-settle-merged-threads")}1975 {...searchableSetting("auto-settle-merged-threads")}
1974 description="Settle a thread when its pull request merges. Closed pull requests still settle automatically."1976 description="Settle a thread when its pull request merges. Closed pull requests still settle automatically."
1975 resetAction={1977 resetAction={
...@@ -1997,6 +1999,7 @@ export function GeneralSettingsPanel() {...@@ -1997,6 +1999,7 @@ export function GeneralSettingsPanel() {
1997 />1999 />
19982000
1999 <SettingsRow2001 <SettingsRow
2002 serverScoped
2000 {...searchableSetting("auto-settle-inactive-threads")}2003 {...searchableSetting("auto-settle-inactive-threads")}
2001 description="Sidebar threads with no activity for this long settle automatically."2004 description="Sidebar threads with no activity for this long settle automatically."
2002 resetAction={2005 resetAction={
...@@ -2027,6 +2030,7 @@ export function GeneralSettingsPanel() {...@@ -2027,6 +2030,7 @@ export function GeneralSettingsPanel() {
2027 />2030 />
2028 {settings.sidebarAutoSettleAfterDays !== null ? (2031 {settings.sidebarAutoSettleAfterDays !== null ? (
2029 <SettingsRow2032 <SettingsRow
2033 serverScoped
2030 title={searchableSetting("days-before-auto-settle").title}2034 title={searchableSetting("days-before-auto-settle").title}
2031 description="Any new activity un-settles a thread automatically."2035 description="Any new activity un-settles a thread automatically."
2032 control={2036 control={
...@@ -2135,6 +2139,7 @@ export function GeneralSettingsPanel() {...@@ -2135,6 +2139,7 @@ export function GeneralSettingsPanel() {
2135 />2139 />
21362140
2137 <SettingsRow2141 <SettingsRow
2142 serverScoped
2138 {...searchableSetting("provider-update-checks")}2143 {...searchableSetting("provider-update-checks")}
2139 description="Check installed provider CLIs for newer available versions."2144 description="Check installed provider CLIs for newer available versions."
2140 resetAction={2145 resetAction={
...@@ -2162,6 +2167,7 @@ export function GeneralSettingsPanel() {...@@ -2162,6 +2167,7 @@ export function GeneralSettingsPanel() {
2162 />2167 />
21632168
2164 <SettingsRow2169 <SettingsRow
2170 serverScoped
2165 id={searchableSetting("background-activity").id}2171 id={searchableSetting("background-activity").id}
2166 title={2172 title={
2167 <span className="inline-flex items-center gap-1.5">2173 <span className="inline-flex items-center gap-1.5">
...@@ -2245,6 +2251,7 @@ export function GeneralSettingsPanel() {...@@ -2245,6 +2251,7 @@ export function GeneralSettingsPanel() {
2245 />2251 />
22462252
2247 <SettingsRow2253 <SettingsRow
2254 serverScoped
2248 {...searchableSetting("new-threads")}2255 {...searchableSetting("new-threads")}
2249 description="Pick the default workspace mode for newly created draft threads."2256 description="Pick the default workspace mode for newly created draft threads."
2250 resetAction={2257 resetAction={
...@@ -2291,6 +2298,7 @@ export function GeneralSettingsPanel() {...@@ -2291,6 +2298,7 @@ export function GeneralSettingsPanel() {
22912298
2292 {settings.defaultThreadEnvMode === "worktree" ? (2299 {settings.defaultThreadEnvMode === "worktree" ? (
2293 <SettingsRow2300 <SettingsRow
2301 serverScoped
2294 className="bg-muted/20 sm:pl-9"2302 className="bg-muted/20 sm:pl-9"
2295 title={searchableSetting("start-from-origin").title}2303 title={searchableSetting("start-from-origin").title}
2296 description="Creates the worktree from the latest matching branch on origin instead of your local branch."2304 description="Creates the worktree from the latest matching branch on origin instead of your local branch."
...@@ -2321,6 +2329,7 @@ export function GeneralSettingsPanel() {...@@ -2321,6 +2329,7 @@ export function GeneralSettingsPanel() {
2321 ) : null}2329 ) : null}
23222330
2323 <SettingsRow2331 <SettingsRow
2332 serverScoped
2324 {...searchableSetting("add-project-starts-in")}2333 {...searchableSetting("add-project-starts-in")}
2325 description='Leave empty to use "~/" when the Add Project browser opens.'2334 description='Leave empty to use "~/" when the Add Project browser opens.'
2326 resetAction={2335 resetAction={
...@@ -2465,6 +2474,7 @@ export function GeneralSettingsPanel() {...@@ -2465,6 +2474,7 @@ export function GeneralSettingsPanel() {
2465 ) : null}2474 ) : null}
24662475
2467 <SettingsRow2476 <SettingsRow
2477 serverScoped
2468 {...searchableSetting("text-generation-model")}2478 {...searchableSetting("text-generation-model")}
2469 description="Default model for generated text like thread titles and source control content. Source control settings can override it with a dedicated source control writer model."2479 description="Default model for generated text like thread titles and source control content. Source control settings can override it with a dedicated source control writer model."
2470 resetAction={2480 resetAction={
apps/web/src/components/settings/SourceControlSettings.tsx+3-1
...@@ -593,7 +593,9 @@ export function SourceControlSettingsPanel() {...@@ -593,7 +593,9 @@ export function SourceControlSettingsPanel() {
593 />593 />
594 )}594 )}
595595
596 {isPrimaryEnvironment ? <SourceControlWritingSettingsSection /> : null}596 {/* Its rows are serverScoped: without a primary they render inert with
597 an explanation, which beats disappearing. */}
598 <SourceControlWritingSettingsSection />
597 </SettingsPageContainer>599 </SettingsPageContainer>
598 );600 );
599}601}
apps/web/src/components/settings/SourceControlWritingSettings.tsx+3
...@@ -74,6 +74,7 @@ export function SourceControlWritingSettingsSection() {...@@ -74,6 +74,7 @@ export function SourceControlWritingSettingsSection() {
74 return (74 return (
75 <SettingsSection title="Text generation">75 <SettingsSection title="Text generation">
76 <SettingsRow76 <SettingsRow
77 serverScoped
77 {...searchableSetting("source-control-writing-style")}78 {...searchableSetting("source-control-writing-style")}
78 description={MODE_OPTIONS[style.mode].description}79 description={MODE_OPTIONS[style.mode].description}
79 resetAction={80 resetAction={
...@@ -138,6 +139,7 @@ export function SourceControlWritingSettingsSection() {...@@ -138,6 +139,7 @@ export function SourceControlWritingSettingsSection() {
138 </SettingsRow>139 </SettingsRow>
139140
140 <SettingsRow141 <SettingsRow
142 serverScoped
141 {...searchableSetting("follow-change-request-templates")}143 {...searchableSetting("follow-change-request-templates")}
142 description="Structures change request descriptions using the current repository's template when one is available."144 description="Structures change request descriptions using the current repository's template when one is available."
143 resetAction={145 resetAction={
...@@ -170,6 +172,7 @@ export function SourceControlWritingSettingsSection() {...@@ -170,6 +172,7 @@ export function SourceControlWritingSettingsSection() {
170 />172 />
171173
172 <SettingsRow174 <SettingsRow
175 serverScoped
173 {...searchableSetting("source-control-writer-model")}176 {...searchableSetting("source-control-writer-model")}
174 description="Optional model override for change descriptions, change request titles and descriptions, and branch or bookmark names. Off uses the global text generation model."177 description="Optional model override for change descriptions, change request titles and descriptions, and branch or bookmark names. Off uses the global text generation model."
175 control={178 control={
apps/web/src/components/settings/settingsLayout.tsx+48-4
...@@ -11,6 +11,10 @@ import {...@@ -11,6 +11,10 @@ import {
11 useState,11 useState,
12} from "react";12} from "react";
1313
14import {
15 PRIMARY_SETTINGS_UNAVAILABLE_MESSAGE,
16 usePrimarySettingsAvailable,
17} from "../../hooks/useSettings";
14import { cn } from "../../lib/utils";18import { cn } from "../../lib/utils";
15import { WorkspacePageContainer, type WorkspacePageWidth } from "../WorkspacePageContainer";19import { WorkspacePageContainer, type WorkspacePageWidth } from "../WorkspacePageContainer";
16import { Button } from "../ui/button";20import { Button } from "../ui/button";
...@@ -159,12 +163,19 @@ export function SettingsSection({...@@ -159,12 +163,19 @@ export function SettingsSection({
159 );163 );
160}164}
161165
166/**
167 * One setting. `serverScoped` marks rows whose value lives in the primary
168 * environment's settings.json; where there is no primary (the hosted app)
169 * the control goes inert with a tooltip instead of showing an editable
170 * default that would never save.
171 */
162export function SettingsRow({172export function SettingsRow({
163 title,173 title,
164 description,174 description,
165 status,175 status,
166 resetAction,176 resetAction,
167 control,177 control,
178 serverScoped = false,
168 children,179 children,
169 className,180 className,
170 ...rowProps181 ...rowProps
...@@ -174,9 +185,36 @@ export function SettingsRow({...@@ -174,9 +185,36 @@ export function SettingsRow({
174 status?: ReactNode;185 status?: ReactNode;
175 resetAction?: ReactNode;186 resetAction?: ReactNode;
176 control?: ReactNode;187 control?: ReactNode;
188 serverScoped?: boolean;
177 children?: ReactNode;189 children?: ReactNode;
178}) {190}) {
179 const targetRef = useSettingsSearchTarget<HTMLDivElement>(rowProps.id);191 const targetRef = useSettingsSearchTarget<HTMLDivElement>(rowProps.id);
192 const primarySettingsAvailable = usePrimarySettingsAvailable();
193 const unavailable = serverScoped && !primarySettingsAvailable;
194 const renderedReset = unavailable ? null : resetAction;
195 const renderedControl =
196 unavailable && control ? (
197 <Tooltip>
198 <TooltipTrigger
199 render={
200 // Focusable so keyboard users can still reach the explanation.
201 <span
202 tabIndex={0}
203 className="flex w-full items-center rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring sm:w-auto"
204 />
205 }
206 >
207 <div inert className="flex w-full items-center gap-2 opacity-50 sm:w-auto">
208 {control}
209 </div>
210 </TooltipTrigger>
211 <TooltipPopup side="top" className="max-w-72">
212 {PRIMARY_SETTINGS_UNAVAILABLE_MESSAGE}
213 </TooltipPopup>
214 </Tooltip>
215 ) : (
216 control
217 );
180218
181 return (219 return (
182 <div220 <div
...@@ -190,7 +228,7 @@ export function SettingsRow({...@@ -190,7 +228,7 @@ export function SettingsRow({
190 <div className="flex min-h-5 items-center gap-1.5">228 <div className="flex min-h-5 items-center gap-1.5">
191 <h3 className="text-sm font-medium tracking-[-0.005em] text-foreground">{title}</h3>229 <h3 className="text-sm font-medium tracking-[-0.005em] text-foreground">{title}</h3>
192 <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center">230 <span className="inline-flex h-5 w-5 shrink-0 items-center justify-center">
193 {resetAction}231 {renderedReset}
194 </span>232 </span>
195 </div>233 </div>
196 {description ? (234 {description ? (
...@@ -200,13 +238,19 @@ export function SettingsRow({...@@ -200,13 +238,19 @@ export function SettingsRow({
200 ) : null}238 ) : null}
201 {status ? <div className="pt-0.5 text-xs text-muted-foreground">{status}</div> : null}239 {status ? <div className="pt-0.5 text-xs text-muted-foreground">{status}</div> : null}
202 </div>240 </div>
203 {control ? (241 {renderedControl ? (
204 <div className="flex w-full shrink-0 items-center gap-2 sm:w-auto sm:justify-end">242 <div className="flex w-full shrink-0 items-center gap-2 sm:w-auto sm:justify-end">
205 {control}243 {renderedControl}
206 </div>244 </div>
207 ) : null}245 ) : null}
208 </div>246 </div>
209 {children}247 {unavailable && children ? (
248 <div inert className="opacity-50">
249 {children}
250 </div>
251 ) : (
252 children
253 )}
210 </div>254 </div>
211 );255 );
212}256}
apps/web/src/hooks/useSettings.ts+23
...@@ -39,6 +39,8 @@ import {...@@ -39,6 +39,8 @@ import {
39 themeAllowsSidebarArtwork,39 themeAllowsSidebarArtwork,
40} from "~/themePalette";40} from "~/themePalette";
41import * as Struct from "effect/Struct";41import * as Struct from "effect/Struct";
42import { toastManager } from "~/components/ui/toast";
43import { isHostedStaticApp } from "~/hostedPairing";
42import { primaryServerSettingsAtom, serverEnvironment } from "~/state/server";44import { primaryServerSettingsAtom, serverEnvironment } from "~/state/server";
43import {45import {
44 type EnvironmentPresentation,46 type EnvironmentPresentation,
...@@ -316,6 +318,20 @@ export function usePrimarySettings<T = UnifiedSettings>(...@@ -316,6 +318,20 @@ export function usePrimarySettings<T = UnifiedSettings>(
316 return useMergedSettings(useAtomValue(primaryServerSettingsAtom), selector);318 return useMergedSettings(useAtomValue(primaryServerSettingsAtom), selector);
317}319}
318320
321export const PRIMARY_SETTINGS_UNAVAILABLE_MESSAGE =
322 "This setting is saved on a server, and the hosted app is not anchored to one. Change it from the desktop app or from the server's own address.";
323
324/**
325 * Whether primary-scoped server settings have a server to live on. The
326 * hosted app connects to every environment as a remote, so it has no primary:
327 * `usePrimarySettings` reads schema defaults there and writes have nowhere
328 * to go. Desktop and server-served web always have one.
329 */
330export function usePrimarySettingsAvailable(): boolean {
331 const primaryEnvironment = usePrimaryEnvironment();
332 return primaryEnvironment !== null || !isHostedStaticApp();
333}
334
319/**335/**
320 * Whether an environment can hold every shared key right now. Gated on the336 * Whether an environment can hold every shared key right now. Gated on the
321 * auto-settlement capability because it is the newest of the shared keys: a337 * auto-settlement capability because it is the newest of the shared keys: a
...@@ -366,6 +382,13 @@ function useUpdateSettingsTarget(environmentId: EnvironmentId | null) {...@@ -366,6 +382,13 @@ function useUpdateSettingsTarget(environmentId: EnvironmentId | null) {
366 environmentId,382 environmentId,
367 input: { patch: localPatch },383 input: { patch: localPatch },
368 });384 });
385 } else {
386 // Dropping the write silently leaves the control looking saved.
387 toastManager.add({
388 type: "warning",
389 title: "Setting not saved",
390 description: PRIMARY_SETTINGS_UNAVAILABLE_MESSAGE,
391 });
369 }392 }
370 if (Object.keys(sharedPatch).length > 0) {393 if (Object.keys(sharedPatch).length > 0) {
371 const targets = new Set(connectedEnvironmentIds);394 const targets = new Set(connectedEnvironmentIds);
apps/web/src/hostedPairing.ts+7-2
...@@ -31,7 +31,7 @@ function originFromUrl(value: string): string | null {...@@ -31,7 +31,7 @@ function originFromUrl(value: string): string | null {
31 }31 }
32}32}
3333
34export function isHostedStaticApp(url: URL = new URL(window.location.href)): boolean {34export function isHostedStaticApp(url?: URL): boolean {
35 if (configuredBackendUrl()) {35 if (configuredBackendUrl()) {
36 return false;36 return false;
37 }37 }
...@@ -40,8 +40,13 @@ export function isHostedStaticApp(url: URL = new URL(window.location.href)): boo...@@ -40,8 +40,13 @@ export function isHostedStaticApp(url: URL = new URL(window.location.href)): boo
40 return true;40 return true;
41 }41 }
4242
43 // No window (tests, static render) means no origin to be hosted at.
44 if (url === undefined && typeof window === "undefined") {
45 return false;
46 }
47
43 const hostedOrigin = originFromUrl(configuredHostedAppUrl());48 const hostedOrigin = originFromUrl(configuredHostedAppUrl());
44 return hostedOrigin !== null && url.origin === hostedOrigin;49 return hostedOrigin !== null && (url ?? new URL(window.location.href)).origin === hostedOrigin;
45}50}
4651
47export function readHostedPairingRequest(url: URL = new URL(window.location.href)) {52export function readHostedPairingRequest(url: URL = new URL(window.location.href)) {