refactor(settings): scope the settings project selector to settings
Picking a project in Settings called setActiveProject, which relocates the chat, the session list, the file tree and the Git surface. Reading another project's MCP servers or agents moved the user's whole app. It had to, because the configuration stores resolved the directory themselves from the active project and held one flat list. Each of them now takes an explicit directory — omitted still means the active project, so every caller outside Settings is unchanged — and keys loaded data by directory next to a flat mirror of the active project. Chat, autocompletes and pickers keep reading that mirror; a load for another directory writes only the map. A failed load restores that directory's previous list. Settings resolves its own directory through useSettingsDirectory, backed by a session-local settingsProjectPath that follows the active project until the user picks something else.
This commit is contained in:
@@ -4,7 +4,8 @@ import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLay
|
||||
import { SettingsSection, SETTINGS_CUSTOM_TRIGGER_CLASS } from '@/components/sections/shared/SettingsSection';
|
||||
import { SettingsInfoHint } from '@/components/sections/shared/SettingsInfoHint';
|
||||
import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { selectProvidersForDirectory, useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useSettingsDirectory } from '@/hooks/useSettingsDirectory';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
@@ -144,7 +145,10 @@ const parseProvidersPayload = (payload: unknown): ProviderOption[] => {
|
||||
|
||||
export const ProvidersPage: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
const providers = useConfigStore((state) => state.providers);
|
||||
// Settings browses whichever project its own selector points at; the app
|
||||
// stays where it is.
|
||||
const settingsDirectory = useSettingsDirectory();
|
||||
const providers = useConfigStore((state) => selectProvidersForDirectory(state, settingsDirectory));
|
||||
const selectedProviderId = useConfigStore((state) => state.selectedProviderId);
|
||||
const setSelectedProvider = useConfigStore((state) => state.setSelectedProvider);
|
||||
const getModelMetadata = useConfigStore((state) => state.getModelMetadata);
|
||||
@@ -341,7 +345,8 @@ export const ProvidersPage: React.FC = () => {
|
||||
try {
|
||||
// OpenChamber-only metadata endpoint: the SDK exposes provider data but
|
||||
// not local auth/source-file provenance used by this settings UI.
|
||||
const response = await runtimeFetch(`/api/provider/${encodeURIComponent(selectedProviderId)}/source`, {
|
||||
const query = settingsDirectory ? `?directory=${encodeURIComponent(settingsDirectory)}` : '';
|
||||
const response = await runtimeFetch(`/api/provider/${encodeURIComponent(selectedProviderId)}/source${query}`, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
@@ -370,7 +375,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [selectedProviderId, t]);
|
||||
}, [selectedProviderId, settingsDirectory, t]);
|
||||
|
||||
const selectedProvider = providers.find((provider) => provider.id === selectedProviderId);
|
||||
const selectedSources = selectedProviderId ? providerSources[selectedProviderId] : undefined;
|
||||
@@ -431,7 +436,7 @@ export const ProvidersPage: React.FC = () => {
|
||||
? (editingCustomScope ?? resolveProviderConfigScope(providerSources[editingCustomProviderId]))
|
||||
: 'user',
|
||||
});
|
||||
const response = await runtimeFetch('/api/provider', {
|
||||
const response = await runtimeFetch(`/api/provider${settingsDirectory ? `?directory=${encodeURIComponent(settingsDirectory)}` : ''}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
@@ -484,10 +489,13 @@ export const ProvidersPage: React.FC = () => {
|
||||
setAuthBusyKey(busyKey);
|
||||
|
||||
try {
|
||||
const response = await runtimeFetch(`/api/provider/${encodeURIComponent(providerId)}/auth?scope=all`, {
|
||||
method: 'DELETE',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
const response = await runtimeFetch(
|
||||
`/api/provider/${encodeURIComponent(providerId)}/auth?scope=all${settingsDirectory ? `&directory=${encodeURIComponent(settingsDirectory)}` : ''}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: { Accept: 'application/json' },
|
||||
},
|
||||
);
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -2,7 +2,8 @@ import React from 'react';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { selectProvidersForDirectory, useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useSettingsDirectory } from '@/hooks/useSettingsDirectory';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SettingsProjectSelector } from '@/components/sections/shared/SettingsProjectSelector';
|
||||
@@ -40,16 +41,28 @@ interface ProvidersSidebarProps {
|
||||
|
||||
export const ProvidersSidebar: React.FC<ProvidersSidebarProps> = ({ onItemSelect }) => {
|
||||
const { t } = useI18n();
|
||||
const providers = useConfigStore((state) => state.providers);
|
||||
// Settings browses whichever project its own selector points at; the app
|
||||
// stays where it is.
|
||||
const settingsDirectory = useSettingsDirectory();
|
||||
const providers = useConfigStore((state) => selectProvidersForDirectory(state, settingsDirectory));
|
||||
const selectedProviderId = useConfigStore((state) => state.selectedProviderId);
|
||||
const setSelectedProvider = useConfigStore((state) => state.setSelectedProvider);
|
||||
const activeProjectId = useProjectsStore((s) => s.activeProjectId);
|
||||
const [sourcesByProvider, setSourcesByProvider] = React.useState<Record<string, ProviderSources>>({});
|
||||
const directory = React.useMemo(() => {
|
||||
if (settingsDirectory) return settingsDirectory;
|
||||
// tie refresh to active project changes (directory is stored in the client)
|
||||
void activeProjectId;
|
||||
return getCurrentDirectory();
|
||||
}, [activeProjectId]);
|
||||
}, [activeProjectId, settingsDirectory]);
|
||||
|
||||
// The app only loads providers for the project it is on; Settings has to ask
|
||||
// for the one it is looking at.
|
||||
const loadProviders = useConfigStore((state) => state.loadProviders);
|
||||
React.useEffect(() => {
|
||||
if (!settingsDirectory) return;
|
||||
void loadProviders({ directory: settingsDirectory, source: 'settings:providers' });
|
||||
}, [loadProviders, settingsDirectory]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (providers.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user