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:
@@ -40,6 +40,19 @@ const getCurrentDirectory = (): string | null => {
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Directory a call operates on. Settings can browse another project without
|
||||
* moving the app, so every entry point takes one; omitting it means the project
|
||||
* the app is currently on.
|
||||
*/
|
||||
const resolveDirectory = (directory?: string | null): string | null => {
|
||||
if (directory !== undefined) {
|
||||
const trimmed = directory?.trim();
|
||||
return trimmed ? trimmed : null;
|
||||
}
|
||||
return getConfigDirectory();
|
||||
};
|
||||
|
||||
export const getConfigDirectory = (): string | null => {
|
||||
try {
|
||||
const projectsStore = useProjectsStore.getState();
|
||||
@@ -257,19 +270,22 @@ export interface AgentDraft {
|
||||
interface AgentsStore {
|
||||
|
||||
selectedAgentName: string | null;
|
||||
/** Agents of the project the app is on. Chat and pickers read this one. */
|
||||
agents: Agent[];
|
||||
/** Every directory loaded so far, including the ambient one. */
|
||||
agentsByDirectory: Record<string, Agent[]>;
|
||||
isLoading: boolean;
|
||||
agentDraft: AgentDraft | null;
|
||||
|
||||
setSelectedAgent: (name: string | null) => void;
|
||||
setAgentDraft: (draft: AgentDraft | null) => void;
|
||||
loadAgents: () => Promise<boolean>;
|
||||
createAgent: (config: AgentConfig) => Promise<AgentMutationResult>;
|
||||
updateAgent: (name: string, config: Partial<AgentConfig>) => Promise<AgentMutationResult>;
|
||||
deleteAgent: (name: string, scope?: AgentScope) => Promise<AgentMutationResult>;
|
||||
getAgentByName: (name: string) => Agent | undefined;
|
||||
loadAgents: (directory?: string | null) => Promise<boolean>;
|
||||
createAgent: (config: AgentConfig, directory?: string | null) => Promise<AgentMutationResult>;
|
||||
updateAgent: (name: string, config: Partial<AgentConfig>, directory?: string | null) => Promise<AgentMutationResult>;
|
||||
deleteAgent: (name: string, scope?: AgentScope, directory?: string | null) => Promise<AgentMutationResult>;
|
||||
getAgentByName: (name: string, directory?: string | null) => Agent | undefined;
|
||||
// Returns only visible agents (excludes hidden internal agents)
|
||||
getVisibleAgents: () => Agent[];
|
||||
getVisibleAgents: (directory?: string | null) => Agent[];
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -278,6 +294,20 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
const EMPTY_AGENTS: Agent[] = [];
|
||||
|
||||
/**
|
||||
* Agents of one project. Returns a stored array so components can select it
|
||||
* directly; an omitted directory means the project the app is on.
|
||||
*/
|
||||
export const selectAgentsForDirectory = (
|
||||
state: Pick<AgentsStore, 'agentsByDirectory'>,
|
||||
directory?: string | null,
|
||||
): Agent[] => {
|
||||
const cacheKey = getAgentsCacheKey(resolveDirectory(directory));
|
||||
return state.agentsByDirectory[cacheKey] ?? EMPTY_AGENTS;
|
||||
};
|
||||
|
||||
export const useAgentsStore = create<AgentsStore>()(
|
||||
devtools(
|
||||
persist(
|
||||
@@ -285,6 +315,7 @@ export const useAgentsStore = create<AgentsStore>()(
|
||||
|
||||
selectedAgentName: null,
|
||||
agents: [],
|
||||
agentsByDirectory: {},
|
||||
isLoading: false,
|
||||
agentDraft: null,
|
||||
|
||||
@@ -296,12 +327,13 @@ export const useAgentsStore = create<AgentsStore>()(
|
||||
set({ agentDraft: draft });
|
||||
},
|
||||
|
||||
loadAgents: async () => {
|
||||
const configDirectory = getConfigDirectory();
|
||||
loadAgents: async (requestedDirectory?: string | null) => {
|
||||
const configDirectory = resolveDirectory(requestedDirectory);
|
||||
const cacheKey = getAgentsCacheKey(configDirectory);
|
||||
const isAmbient = cacheKey === getAgentsCacheKey(getConfigDirectory());
|
||||
const now = Date.now();
|
||||
const loadedAt = agentsLastLoadedAt.get(cacheKey) ?? 0;
|
||||
const hasCachedAgents = get().agents.length > 0;
|
||||
const hasCachedAgents = (get().agentsByDirectory[cacheKey] ?? (isAmbient ? get().agents : [])).length > 0;
|
||||
|
||||
if (hasCachedAgents && now - loadedAt < AGENTS_LOAD_CACHE_TTL_MS) {
|
||||
return true;
|
||||
@@ -314,7 +346,9 @@ export const useAgentsStore = create<AgentsStore>()(
|
||||
|
||||
const request = (async () => {
|
||||
set({ isLoading: true });
|
||||
const previousAgents = get().agents;
|
||||
// Failure must never look like an empty project. The mirror is the
|
||||
// fallback so a directory loaded before this map existed still counts.
|
||||
const previousAgents = get().agentsByDirectory[cacheKey] ?? (isAmbient ? get().agents : []);
|
||||
const previousSignature = buildAgentsSignature(previousAgents);
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
@@ -372,7 +406,14 @@ export const useAgentsStore = create<AgentsStore>()(
|
||||
|
||||
const nextSignature = buildAgentsSignature(agentsWithScope);
|
||||
if (previousSignature !== nextSignature) {
|
||||
set({ agents: agentsWithScope, isLoading: false });
|
||||
set((state) => {
|
||||
const next: Partial<AgentsStore> = {
|
||||
agentsByDirectory: { ...state.agentsByDirectory, [cacheKey]: agentsWithScope },
|
||||
isLoading: false,
|
||||
};
|
||||
if (isAmbient) next.agents = agentsWithScope;
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
set({ isLoading: false });
|
||||
}
|
||||
@@ -395,7 +436,7 @@ export const useAgentsStore = create<AgentsStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
createAgent: async (config: AgentConfig) => {
|
||||
createAgent: async (config: AgentConfig, requestedDirectory?: string | null) => {
|
||||
try {
|
||||
console.log('[AgentsStore] Creating agent:', config.name);
|
||||
|
||||
@@ -415,7 +456,7 @@ export const useAgentsStore = create<AgentsStore>()(
|
||||
|
||||
console.log('[AgentsStore] Agent config to save:', agentConfig);
|
||||
|
||||
const configDirectory = getConfigDirectory();
|
||||
const configDirectory = resolveDirectory(requestedDirectory);
|
||||
const queryParams = configDirectory ? `?directory=${encodeURIComponent(configDirectory)}` : '';
|
||||
|
||||
const response = await runtimeFetch(`/api/config/agents/${encodeURIComponent(config.name)}${queryParams}`, {
|
||||
@@ -458,7 +499,7 @@ export const useAgentsStore = create<AgentsStore>()(
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const loaded = await get().loadAgents();
|
||||
const loaded = await get().loadAgents(configDirectory);
|
||||
if (loaded) {
|
||||
emitConfigChange("agents", { source: CONFIG_EVENT_SOURCE });
|
||||
}
|
||||
@@ -471,7 +512,7 @@ export const useAgentsStore = create<AgentsStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
updateAgent: async (name: string, config: Partial<AgentConfig>) => {
|
||||
updateAgent: async (name: string, config: Partial<AgentConfig>, requestedDirectory?: string | null) => {
|
||||
try {
|
||||
const agentConfig: Record<string, unknown> = {};
|
||||
|
||||
@@ -485,7 +526,7 @@ export const useAgentsStore = create<AgentsStore>()(
|
||||
if (config.permission !== undefined) agentConfig.permission = config.permission;
|
||||
if (config.disable !== undefined) agentConfig.disable = config.disable;
|
||||
|
||||
const configDirectory = getConfigDirectory();
|
||||
const configDirectory = resolveDirectory(requestedDirectory);
|
||||
const queryParams = configDirectory ? `?directory=${encodeURIComponent(configDirectory)}` : '';
|
||||
|
||||
const response = await runtimeFetch(`/api/config/agents/${encodeURIComponent(name)}${queryParams}`, {
|
||||
@@ -528,7 +569,7 @@ export const useAgentsStore = create<AgentsStore>()(
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const loaded = await get().loadAgents();
|
||||
const loaded = await get().loadAgents(configDirectory);
|
||||
if (loaded) {
|
||||
emitConfigChange("agents", { source: CONFIG_EVENT_SOURCE });
|
||||
}
|
||||
@@ -541,9 +582,9 @@ export const useAgentsStore = create<AgentsStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
deleteAgent: async (name: string, scope?: AgentScope) => {
|
||||
deleteAgent: async (name: string, scope?: AgentScope, requestedDirectory?: string | null) => {
|
||||
try {
|
||||
const configDirectory = getConfigDirectory();
|
||||
const configDirectory = resolveDirectory(requestedDirectory);
|
||||
const queryParams = configDirectory ? `?directory=${encodeURIComponent(configDirectory)}` : '';
|
||||
|
||||
const response = await runtimeFetch(`/api/config/agents/${encodeURIComponent(name)}${queryParams}`, {
|
||||
@@ -594,7 +635,7 @@ export const useAgentsStore = create<AgentsStore>()(
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const loaded = await get().loadAgents();
|
||||
const loaded = await get().loadAgents(configDirectory);
|
||||
if (loaded) {
|
||||
emitConfigChange("agents", { source: CONFIG_EVENT_SOURCE });
|
||||
}
|
||||
@@ -609,14 +650,12 @@ export const useAgentsStore = create<AgentsStore>()(
|
||||
},
|
||||
|
||||
|
||||
getAgentByName: (name: string) => {
|
||||
const { agents } = get();
|
||||
return agents.find((a) => a.name === name);
|
||||
getAgentByName: (name: string, requestedDirectory?: string | null) => {
|
||||
return selectAgentsForDirectory(get(), requestedDirectory).find((agent) => agent.name === name);
|
||||
},
|
||||
|
||||
getVisibleAgents: () => {
|
||||
const { agents } = get();
|
||||
return filterVisibleAgents(agents);
|
||||
getVisibleAgents: (requestedDirectory?: string | null) => {
|
||||
return filterVisibleAgents(selectAgentsForDirectory(get(), requestedDirectory));
|
||||
},
|
||||
}),
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user