feat: add multi-project support (#110)
* feat: Implement project management store with project path validation and synchronization - Added `useProjectsStore` for managing projects, including adding, removing, renaming, and validating project paths. - Implemented persistence for projects and active project ID using safe storage. - Introduced synchronization from desktop settings to keep project data consistent. - Enhanced session store to manage sessions by directory and added new methods for session management. - Updated todo store to fetch session todos based on the directory context. - Refactored server code to validate and resolve project directories for various API endpoints. - Added project entry validation and sanitization to ensure data integrity. * feat(settings): migrate legacy project settings and update settings loading logic * feat: enhance project management with directory-aware settings and improved agent/command source handling * feat: enhance session and project management with directory-aware settings and improved configuration refresh logic * feat: enhance project management with worktree manager integration and project directory resolution * feat: enhance agent groups store with project directory resolution and loading logic * feat: add heartbeat management and global wrapping for SSE blocks in agent and chat providers * feat: refactor command and project handling in useCommandsStore - Replaced useDirectoryStore with useProjectsStore to manage project paths. - Introduced getRequestDirectory function to determine the active project directory. - Updated command fetching to respect project-level scoping. - Enhanced error handling and logging for command configuration fetching. - Improved command configuration saving and updating to utilize project directory context. feat: enhance project path normalization in useProjectsStore - Added resolveTildePath function to expand paths starting with ~. - Updated normalizeProjectPath to utilize home directory for path expansion. fix: update permission handling in useSessionStore - Changed Permission type to PermissionRequest for clarity. - Updated respondToPermission method to use requestId instead of permissionId. refactor: improve permission utilities - Introduced types for PermissionAction and PermissionRule. - Enhanced getAgentDefinition and resolveConfigStore functions for better type safety. - Added resolvePermissionAction to streamline permission resolution logic. feat: add agent configuration retrieval endpoint - Implemented new API endpoint to fetch agent configuration based on project directory. - Enhanced getAgentPermissionSource to prioritize project-level permissions. chore: update SDK version in package.json files - Bumped @opencode-ai/sdk version to ^1.1.1 across all relevant package.json files. refactor: streamline bridge message handling - Updated handleBridgeMessage to accept directory parameter for agent and command requests. - Improved local API request handling to extract directory from query parameters and headers. feat: enhance project configuration management - Added functions to retrieve and merge project configuration paths. - Improved handling of existing project configuration files for agents and commands. * feat: enhance VSCode integration and session management - Added support for a sticky sidebar header background in light and dark themes. - Introduced functions to read VSCode workspace directory and check if running in VSCode. - Implemented detailed logging for session loading and creation processes. - Enhanced session filtering based on directory structure and canonical paths. - Added a new method to reorder projects and prevent modifications in VSCode workspace. - Improved error handling and logging for app initialization and markdown file parsing. - Updated API checks and health checks to ensure readiness before proceeding. - Refactored code for better readability and maintainability across various modules. * feat: improve agent and branch selection logic, enhance session management, and update multi-run creation response * feat: add worktree management actions in agent group detail and sidebar, including delete and keep only options * fix(ui): share IME guard and cover multi-run * fix(session): reduce maximum visible sessions in group from 7 to 5
This commit is contained in:
committed by
GitHub
parent
8aa379e313
commit
18c5b4c7b5
@@ -11,6 +11,8 @@ import { filterVisibleAgents } from "./useAgentsStore";
|
||||
import { isDesktopRuntime, getDesktopSettings } from "@/lib/desktop";
|
||||
import { getRegisteredRuntimeAPIs } from "@/contexts/runtimeAPIRegistry";
|
||||
import { updateDesktopSettings } from "@/lib/persistence";
|
||||
import { useDirectoryStore } from "@/stores/useDirectoryStore";
|
||||
import { streamDebugEnabled } from "@/stores/utils/streamDebug";
|
||||
|
||||
const MODELS_DEV_API_URL = "https://models.dev/api.json";
|
||||
const MODELS_DEV_PROXY_URL = "/api/openchamber/models-metadata";
|
||||
@@ -271,10 +273,70 @@ const fetchModelsDevMetadata = async (): Promise<Map<string, ModelMetadata>> =>
|
||||
return new Map();
|
||||
};
|
||||
|
||||
let modelsMetadataInFlight: Promise<Map<string, ModelMetadata>> | null = null;
|
||||
|
||||
const ensureModelsMetadataFetch = (
|
||||
getModelsMetadata: () => Map<string, ModelMetadata>,
|
||||
setModelsMetadata: (metadata: Map<string, ModelMetadata>) => void,
|
||||
) => {
|
||||
const existing = getModelsMetadata();
|
||||
if (existing.size > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (modelsMetadataInFlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
modelsMetadataInFlight = fetchModelsDevMetadata()
|
||||
.then((metadata) => {
|
||||
if (metadata.size > 0) {
|
||||
setModelsMetadata(metadata);
|
||||
}
|
||||
return metadata;
|
||||
})
|
||||
.catch(() => new Map<string, ModelMetadata>())
|
||||
.finally(() => {
|
||||
modelsMetadataInFlight = null;
|
||||
});
|
||||
};
|
||||
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const DIRECTORY_KEY_GLOBAL = "__global__";
|
||||
|
||||
const toDirectoryKey = (directory: string | null | undefined): string => {
|
||||
const trimmed = typeof directory === 'string' ? directory.trim() : '';
|
||||
return trimmed.length > 0 ? trimmed : DIRECTORY_KEY_GLOBAL;
|
||||
};
|
||||
|
||||
const fromDirectoryKey = (key: string): string | null => (key === DIRECTORY_KEY_GLOBAL ? null : key);
|
||||
|
||||
const resolveInitialDirectoryKey = (): string => {
|
||||
if (typeof window === 'undefined') {
|
||||
return DIRECTORY_KEY_GLOBAL;
|
||||
}
|
||||
|
||||
const directory = opencodeClient.getDirectory() ?? useDirectoryStore.getState().currentDirectory;
|
||||
return toDirectoryKey(directory);
|
||||
};
|
||||
|
||||
interface DirectoryScopedConfig {
|
||||
providers: ProviderWithModelList[];
|
||||
agents: Agent[];
|
||||
currentProviderId: string;
|
||||
currentModelId: string;
|
||||
currentAgentName: string | undefined;
|
||||
selectedProviderId: string;
|
||||
agentModelSelections: { [agentName: string]: { providerId: string; modelId: string } };
|
||||
defaultProviders: { [key: string]: string };
|
||||
}
|
||||
|
||||
interface ConfigStore {
|
||||
|
||||
activeDirectoryKey: string;
|
||||
directoryScoped: Record<string, DirectoryScopedConfig>;
|
||||
|
||||
providers: ProviderWithModelList[];
|
||||
agents: Agent[];
|
||||
currentProviderId: string;
|
||||
@@ -290,8 +352,10 @@ interface ConfigStore {
|
||||
settingsDefaultModel: string | undefined; // format: "provider/model"
|
||||
settingsDefaultAgent: string | undefined;
|
||||
|
||||
loadProviders: () => Promise<void>;
|
||||
loadAgents: () => Promise<boolean>;
|
||||
activateDirectory: (directory: string | null | undefined) => Promise<void>;
|
||||
|
||||
loadProviders: (options?: { directory?: string | null }) => Promise<void>;
|
||||
loadAgents: (options?: { directory?: string | null }) => Promise<boolean>;
|
||||
setProvider: (providerId: string) => void;
|
||||
setModel: (modelId: string) => void;
|
||||
setAgent: (agentName: string | undefined) => void;
|
||||
@@ -322,6 +386,9 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
|
||||
activeDirectoryKey: resolveInitialDirectoryKey(),
|
||||
directoryScoped: {},
|
||||
|
||||
providers: [],
|
||||
agents: [],
|
||||
currentProviderId: "",
|
||||
@@ -336,15 +403,63 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
settingsDefaultModel: undefined,
|
||||
settingsDefaultAgent: undefined,
|
||||
|
||||
loadProviders: async () => {
|
||||
const previousProviders = get().providers;
|
||||
const previousDefaults = get().defaultProviders;
|
||||
activateDirectory: async (directory) => {
|
||||
const directoryKey = toDirectoryKey(directory);
|
||||
|
||||
set((state) => {
|
||||
const snapshot = state.directoryScoped[directoryKey];
|
||||
if (snapshot) {
|
||||
return {
|
||||
activeDirectoryKey: directoryKey,
|
||||
providers: snapshot.providers,
|
||||
agents: snapshot.agents,
|
||||
currentProviderId: snapshot.currentProviderId,
|
||||
currentModelId: snapshot.currentModelId,
|
||||
currentAgentName: snapshot.currentAgentName,
|
||||
selectedProviderId: snapshot.selectedProviderId,
|
||||
agentModelSelections: snapshot.agentModelSelections,
|
||||
defaultProviders: snapshot.defaultProviders,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
activeDirectoryKey: directoryKey,
|
||||
providers: [],
|
||||
agents: [],
|
||||
currentProviderId: "",
|
||||
currentModelId: "",
|
||||
currentAgentName: undefined,
|
||||
selectedProviderId: "",
|
||||
agentModelSelections: {},
|
||||
defaultProviders: {},
|
||||
};
|
||||
});
|
||||
|
||||
if (!get().isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
await get().loadProviders({ directory: fromDirectoryKey(directoryKey) });
|
||||
await get().loadAgents({ directory: fromDirectoryKey(directoryKey) });
|
||||
},
|
||||
|
||||
loadProviders: async (options) => {
|
||||
const directoryKey = toDirectoryKey(options?.directory ?? fromDirectoryKey(get().activeDirectoryKey));
|
||||
const existingSnapshot = get().directoryScoped[directoryKey];
|
||||
const previousProviders = existingSnapshot?.providers ?? (get().activeDirectoryKey === directoryKey ? get().providers : []);
|
||||
const previousDefaults = existingSnapshot?.defaultProviders ?? (get().activeDirectoryKey === directoryKey ? get().defaultProviders : {});
|
||||
let lastError: unknown = null;
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
const metadataPromise = fetchModelsDevMetadata();
|
||||
const apiResult = await opencodeClient.getProviders();
|
||||
ensureModelsMetadataFetch(
|
||||
() => get().modelsMetadata,
|
||||
(metadata) => set({ modelsMetadata: metadata }),
|
||||
);
|
||||
const apiResult = await opencodeClient.withDirectory(
|
||||
fromDirectoryKey(directoryKey),
|
||||
() => opencodeClient.getProviders()
|
||||
);
|
||||
const providers = Array.isArray(apiResult?.providers) ? apiResult.providers : [];
|
||||
const defaults = apiResult?.default || {};
|
||||
|
||||
@@ -357,16 +472,39 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
};
|
||||
});
|
||||
|
||||
// Only store providers and defaults - model/agent selection handled in loadAgents
|
||||
set({
|
||||
providers: processedProviders,
|
||||
defaultProviders: defaults,
|
||||
set((state) => {
|
||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||
providers: [],
|
||||
agents: [],
|
||||
currentProviderId: "",
|
||||
currentModelId: "",
|
||||
currentAgentName: undefined,
|
||||
selectedProviderId: "",
|
||||
agentModelSelections: {},
|
||||
defaultProviders: {},
|
||||
};
|
||||
|
||||
const nextSnapshot: DirectoryScopedConfig = {
|
||||
...baseSnapshot,
|
||||
providers: processedProviders,
|
||||
defaultProviders: defaults,
|
||||
};
|
||||
|
||||
const nextState: Partial<ConfigStore> = {
|
||||
directoryScoped: {
|
||||
...state.directoryScoped,
|
||||
[directoryKey]: nextSnapshot,
|
||||
},
|
||||
};
|
||||
|
||||
if (state.activeDirectoryKey === directoryKey) {
|
||||
nextState.providers = processedProviders;
|
||||
nextState.defaultProviders = defaults;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
});
|
||||
|
||||
const metadata = await metadataPromise;
|
||||
if (metadata.size > 0) {
|
||||
set({ modelsMetadata: metadata });
|
||||
}
|
||||
return;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
@@ -376,10 +514,38 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
}
|
||||
|
||||
console.error("Failed to load providers:", lastError);
|
||||
// Preserve previous state on failure instead of clearing it
|
||||
set({
|
||||
providers: previousProviders,
|
||||
defaultProviders: previousDefaults,
|
||||
|
||||
set((state) => {
|
||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||
providers: [],
|
||||
agents: [],
|
||||
currentProviderId: "",
|
||||
currentModelId: "",
|
||||
currentAgentName: undefined,
|
||||
selectedProviderId: "",
|
||||
agentModelSelections: {},
|
||||
defaultProviders: {},
|
||||
};
|
||||
|
||||
const nextSnapshot: DirectoryScopedConfig = {
|
||||
...baseSnapshot,
|
||||
providers: previousProviders,
|
||||
defaultProviders: previousDefaults,
|
||||
};
|
||||
|
||||
const nextState: Partial<ConfigStore> = {
|
||||
directoryScoped: {
|
||||
...state.directoryScoped,
|
||||
[directoryKey]: nextSnapshot,
|
||||
},
|
||||
};
|
||||
|
||||
if (state.activeDirectoryKey === directoryKey) {
|
||||
nextState.providers = previousProviders;
|
||||
nextState.defaultProviders = previousDefaults;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -387,34 +553,135 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
const { providers } = get();
|
||||
const provider = providers.find((p) => p.id === providerId);
|
||||
|
||||
if (provider) {
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstModel = provider.models[0];
|
||||
const newModelId = firstModel?.id || "";
|
||||
const firstModel = provider.models[0];
|
||||
const newModelId = firstModel?.id || "";
|
||||
|
||||
set({
|
||||
set((state) => {
|
||||
const directoryKey = state.activeDirectoryKey;
|
||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||
providers: state.providers,
|
||||
agents: state.agents,
|
||||
currentProviderId: state.currentProviderId,
|
||||
currentModelId: state.currentModelId,
|
||||
currentAgentName: state.currentAgentName,
|
||||
selectedProviderId: state.selectedProviderId,
|
||||
agentModelSelections: state.agentModelSelections,
|
||||
defaultProviders: state.defaultProviders,
|
||||
};
|
||||
|
||||
const nextSnapshot: DirectoryScopedConfig = {
|
||||
...baseSnapshot,
|
||||
currentProviderId: providerId,
|
||||
currentModelId: newModelId,
|
||||
selectedProviderId: providerId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
currentProviderId: providerId,
|
||||
currentModelId: newModelId,
|
||||
selectedProviderId: providerId,
|
||||
directoryScoped: {
|
||||
...state.directoryScoped,
|
||||
[directoryKey]: nextSnapshot,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setModel: (modelId: string) => {
|
||||
set({ currentModelId: modelId });
|
||||
set((state) => {
|
||||
const directoryKey = state.activeDirectoryKey;
|
||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||
providers: state.providers,
|
||||
agents: state.agents,
|
||||
currentProviderId: state.currentProviderId,
|
||||
currentModelId: state.currentModelId,
|
||||
currentAgentName: state.currentAgentName,
|
||||
selectedProviderId: state.selectedProviderId,
|
||||
agentModelSelections: state.agentModelSelections,
|
||||
defaultProviders: state.defaultProviders,
|
||||
};
|
||||
|
||||
const nextSnapshot: DirectoryScopedConfig = {
|
||||
...baseSnapshot,
|
||||
currentModelId: modelId,
|
||||
};
|
||||
|
||||
return {
|
||||
currentModelId: modelId,
|
||||
directoryScoped: {
|
||||
...state.directoryScoped,
|
||||
[directoryKey]: nextSnapshot,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setSelectedProvider: (providerId: string) => {
|
||||
set({ selectedProviderId: providerId });
|
||||
set((state) => {
|
||||
const directoryKey = state.activeDirectoryKey;
|
||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||
providers: state.providers,
|
||||
agents: state.agents,
|
||||
currentProviderId: state.currentProviderId,
|
||||
currentModelId: state.currentModelId,
|
||||
currentAgentName: state.currentAgentName,
|
||||
selectedProviderId: state.selectedProviderId,
|
||||
agentModelSelections: state.agentModelSelections,
|
||||
defaultProviders: state.defaultProviders,
|
||||
};
|
||||
|
||||
const nextSnapshot: DirectoryScopedConfig = {
|
||||
...baseSnapshot,
|
||||
selectedProviderId: providerId,
|
||||
};
|
||||
|
||||
return {
|
||||
selectedProviderId: providerId,
|
||||
directoryScoped: {
|
||||
...state.directoryScoped,
|
||||
[directoryKey]: nextSnapshot,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
saveAgentModelSelection: (agentName: string, providerId: string, modelId: string) => {
|
||||
set((state) => ({
|
||||
agentModelSelections: {
|
||||
set((state) => {
|
||||
const directoryKey = state.activeDirectoryKey;
|
||||
const nextSelections = {
|
||||
...state.agentModelSelections,
|
||||
[agentName]: { providerId, modelId },
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||
providers: state.providers,
|
||||
agents: state.agents,
|
||||
currentProviderId: state.currentProviderId,
|
||||
currentModelId: state.currentModelId,
|
||||
currentAgentName: state.currentAgentName,
|
||||
selectedProviderId: state.selectedProviderId,
|
||||
agentModelSelections: state.agentModelSelections,
|
||||
defaultProviders: state.defaultProviders,
|
||||
};
|
||||
|
||||
const nextSnapshot: DirectoryScopedConfig = {
|
||||
...baseSnapshot,
|
||||
agentModelSelections: nextSelections,
|
||||
};
|
||||
|
||||
return {
|
||||
agentModelSelections: nextSelections,
|
||||
directoryScoped: {
|
||||
...state.directoryScoped,
|
||||
[directoryKey]: nextSnapshot,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
getAgentModelSelection: (agentName: string) => {
|
||||
@@ -422,40 +689,97 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
return agentModelSelections[agentName] || null;
|
||||
},
|
||||
|
||||
loadAgents: async () => {
|
||||
const previousAgents = get().agents;
|
||||
loadAgents: async (options) => {
|
||||
const directoryKey = toDirectoryKey(options?.directory ?? fromDirectoryKey(get().activeDirectoryKey));
|
||||
const existingSnapshot = get().directoryScoped[directoryKey];
|
||||
const previousAgents = existingSnapshot?.agents ?? (get().activeDirectoryKey === directoryKey ? get().agents : []);
|
||||
let lastError: unknown = null;
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
// Fetch agents and OpenChamber settings in parallel
|
||||
const [agents, openChamberDefaults] = await Promise.all([
|
||||
opencodeClient.listAgents(),
|
||||
opencodeClient.withDirectory(fromDirectoryKey(directoryKey), () => opencodeClient.listAgents()),
|
||||
fetchOpenChamberDefaults(),
|
||||
]);
|
||||
|
||||
const safeAgents = Array.isArray(agents) ? agents : [];
|
||||
set({
|
||||
agents: safeAgents,
|
||||
// Store settings defaults so setAgent can respect them
|
||||
settingsDefaultModel: openChamberDefaults.defaultModel,
|
||||
settingsDefaultAgent: openChamberDefaults.defaultAgent,
|
||||
|
||||
const providers = get().activeDirectoryKey === directoryKey
|
||||
? get().providers
|
||||
: (get().directoryScoped[directoryKey]?.providers ?? []);
|
||||
|
||||
set((state) => {
|
||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||
providers,
|
||||
agents: previousAgents,
|
||||
currentProviderId: "",
|
||||
currentModelId: "",
|
||||
currentAgentName: undefined,
|
||||
selectedProviderId: "",
|
||||
agentModelSelections: {},
|
||||
defaultProviders: {},
|
||||
};
|
||||
|
||||
const nextSnapshot: DirectoryScopedConfig = {
|
||||
...baseSnapshot,
|
||||
providers,
|
||||
agents: safeAgents,
|
||||
};
|
||||
|
||||
const nextState: Partial<ConfigStore> = {
|
||||
settingsDefaultModel: openChamberDefaults.defaultModel,
|
||||
settingsDefaultAgent: openChamberDefaults.defaultAgent,
|
||||
directoryScoped: {
|
||||
...state.directoryScoped,
|
||||
[directoryKey]: nextSnapshot,
|
||||
},
|
||||
};
|
||||
|
||||
if (state.activeDirectoryKey === directoryKey) {
|
||||
nextState.agents = safeAgents;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
});
|
||||
|
||||
const { providers } = get();
|
||||
|
||||
if (safeAgents.length === 0) {
|
||||
set({ currentAgentName: undefined });
|
||||
set((state) => {
|
||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||
providers,
|
||||
agents: [],
|
||||
currentProviderId: "",
|
||||
currentModelId: "",
|
||||
currentAgentName: undefined,
|
||||
selectedProviderId: "",
|
||||
agentModelSelections: {},
|
||||
defaultProviders: {},
|
||||
};
|
||||
|
||||
const nextSnapshot: DirectoryScopedConfig = {
|
||||
...baseSnapshot,
|
||||
providers,
|
||||
agents: [],
|
||||
currentAgentName: undefined,
|
||||
};
|
||||
|
||||
const nextState: Partial<ConfigStore> = {
|
||||
directoryScoped: {
|
||||
...state.directoryScoped,
|
||||
[directoryKey]: nextSnapshot,
|
||||
},
|
||||
};
|
||||
|
||||
if (state.activeDirectoryKey === directoryKey) {
|
||||
nextState.currentAgentName = undefined;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Agent Selection ---
|
||||
// Priority: settings.defaultAgent → build → first primary → first agent
|
||||
const primaryAgents = safeAgents.filter((agent) => isPrimaryMode(agent.mode));
|
||||
const buildAgent = primaryAgents.find((agent) => agent.name === "build");
|
||||
const fallbackAgent = buildAgent || primaryAgents[0] || safeAgents[0];
|
||||
|
||||
let resolvedAgent: Agent | undefined;
|
||||
|
||||
// Helper to validate model exists in providers
|
||||
const validateModel = (providerId: string, modelId: string): boolean => {
|
||||
const provider = providers.find((p) => p.id === providerId);
|
||||
@@ -463,6 +787,14 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
return provider.models.some((m) => m.id === modelId);
|
||||
};
|
||||
|
||||
// --- Agent Selection ---
|
||||
// Priority: settings.defaultAgent → build → first primary → first agent
|
||||
const primaryAgents = safeAgents.filter((agent) => isPrimaryMode(agent.mode));
|
||||
const buildAgent = primaryAgents.find((agent) => agent.name === "build");
|
||||
const fallbackAgent = buildAgent || primaryAgents[0] || safeAgents[0];
|
||||
|
||||
let resolvedAgent: Agent = fallbackAgent;
|
||||
|
||||
// Track invalid settings to clear
|
||||
const invalidSettings: { defaultModel?: string; defaultAgent?: string } = {};
|
||||
|
||||
@@ -477,13 +809,6 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fall back to default logic
|
||||
if (!resolvedAgent) {
|
||||
resolvedAgent = fallbackAgent;
|
||||
}
|
||||
|
||||
set({ currentAgentName: resolvedAgent.name });
|
||||
|
||||
// --- Model Selection ---
|
||||
// Priority: settings.defaultModel → agent's preferred model → opencode/big-pickle
|
||||
let resolvedProviderId: string | undefined;
|
||||
@@ -524,12 +849,44 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedProviderId && resolvedModelId) {
|
||||
set({
|
||||
currentProviderId: resolvedProviderId,
|
||||
currentModelId: resolvedModelId,
|
||||
});
|
||||
}
|
||||
set((state) => {
|
||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||
providers,
|
||||
agents: safeAgents,
|
||||
currentProviderId: "",
|
||||
currentModelId: "",
|
||||
currentAgentName: undefined,
|
||||
selectedProviderId: "",
|
||||
agentModelSelections: {},
|
||||
defaultProviders: {},
|
||||
};
|
||||
|
||||
const nextSnapshot: DirectoryScopedConfig = {
|
||||
...baseSnapshot,
|
||||
providers,
|
||||
agents: safeAgents,
|
||||
currentAgentName: resolvedAgent.name,
|
||||
currentProviderId: resolvedProviderId ?? baseSnapshot.currentProviderId,
|
||||
currentModelId: resolvedModelId ?? baseSnapshot.currentModelId,
|
||||
};
|
||||
|
||||
const nextState: Partial<ConfigStore> = {
|
||||
directoryScoped: {
|
||||
...state.directoryScoped,
|
||||
[directoryKey]: nextSnapshot,
|
||||
},
|
||||
};
|
||||
|
||||
if (state.activeDirectoryKey === directoryKey) {
|
||||
nextState.currentAgentName = resolvedAgent.name;
|
||||
if (resolvedProviderId && resolvedModelId) {
|
||||
nextState.currentProviderId = resolvedProviderId;
|
||||
nextState.currentModelId = resolvedModelId;
|
||||
}
|
||||
}
|
||||
|
||||
return nextState;
|
||||
});
|
||||
|
||||
// Clear invalid settings from storage (best-effort cleanup)
|
||||
if (Object.keys(invalidSettings).length > 0) {
|
||||
@@ -552,14 +909,75 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
}
|
||||
|
||||
console.error("Failed to load agents:", lastError);
|
||||
set({ agents: previousAgents });
|
||||
|
||||
set((state) => {
|
||||
const providers = state.activeDirectoryKey === directoryKey
|
||||
? state.providers
|
||||
: (state.directoryScoped[directoryKey]?.providers ?? []);
|
||||
|
||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||
providers,
|
||||
agents: [],
|
||||
currentProviderId: "",
|
||||
currentModelId: "",
|
||||
currentAgentName: undefined,
|
||||
selectedProviderId: "",
|
||||
agentModelSelections: {},
|
||||
defaultProviders: {},
|
||||
};
|
||||
|
||||
const nextSnapshot: DirectoryScopedConfig = {
|
||||
...baseSnapshot,
|
||||
providers,
|
||||
agents: previousAgents,
|
||||
};
|
||||
|
||||
const nextState: Partial<ConfigStore> = {
|
||||
directoryScoped: {
|
||||
...state.directoryScoped,
|
||||
[directoryKey]: nextSnapshot,
|
||||
},
|
||||
};
|
||||
|
||||
if (state.activeDirectoryKey === directoryKey) {
|
||||
nextState.agents = previousAgents;
|
||||
}
|
||||
|
||||
return nextState;
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
setAgent: (agentName: string | undefined) => {
|
||||
const { agents, providers, settingsDefaultModel } = get();
|
||||
|
||||
set({ currentAgentName: agentName });
|
||||
set((state) => {
|
||||
const directoryKey = state.activeDirectoryKey;
|
||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||
providers: state.providers,
|
||||
agents: state.agents,
|
||||
currentProviderId: state.currentProviderId,
|
||||
currentModelId: state.currentModelId,
|
||||
currentAgentName: state.currentAgentName,
|
||||
selectedProviderId: state.selectedProviderId,
|
||||
agentModelSelections: state.agentModelSelections,
|
||||
defaultProviders: state.defaultProviders,
|
||||
};
|
||||
|
||||
const nextSnapshot: DirectoryScopedConfig = {
|
||||
...baseSnapshot,
|
||||
currentAgentName: agentName,
|
||||
};
|
||||
|
||||
return {
|
||||
currentAgentName: agentName,
|
||||
directoryScoped: {
|
||||
...state.directoryScoped,
|
||||
[directoryKey]: nextSnapshot,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
if (agentName && typeof window !== "undefined") {
|
||||
|
||||
@@ -608,9 +1026,33 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
if (parsed) {
|
||||
const settingsProvider = providers.find((p) => p.id === parsed.providerId);
|
||||
if (settingsProvider?.models.some((m) => m.id === parsed.modelId)) {
|
||||
set({
|
||||
currentProviderId: parsed.providerId,
|
||||
currentModelId: parsed.modelId,
|
||||
set((state) => {
|
||||
const directoryKey = state.activeDirectoryKey;
|
||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||
providers: state.providers,
|
||||
agents: state.agents,
|
||||
currentProviderId: state.currentProviderId,
|
||||
currentModelId: state.currentModelId,
|
||||
currentAgentName: state.currentAgentName,
|
||||
selectedProviderId: state.selectedProviderId,
|
||||
agentModelSelections: state.agentModelSelections,
|
||||
defaultProviders: state.defaultProviders,
|
||||
};
|
||||
|
||||
const nextSnapshot: DirectoryScopedConfig = {
|
||||
...baseSnapshot,
|
||||
currentProviderId: parsed.providerId,
|
||||
currentModelId: parsed.modelId,
|
||||
};
|
||||
|
||||
return {
|
||||
currentProviderId: parsed.providerId,
|
||||
currentModelId: parsed.modelId,
|
||||
directoryScoped: {
|
||||
...state.directoryScoped,
|
||||
[directoryKey]: nextSnapshot,
|
||||
},
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -625,10 +1067,35 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
const agentModel = agentProvider.models.find((model) => model.id === agent.model!.modelID);
|
||||
|
||||
if (agentModel) {
|
||||
set({
|
||||
currentProviderId: agent.model!.providerID,
|
||||
currentModelId: agent.model!.modelID,
|
||||
selectedProviderId: agent.model!.providerID,
|
||||
set((state) => {
|
||||
const directoryKey = state.activeDirectoryKey;
|
||||
const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? {
|
||||
providers: state.providers,
|
||||
agents: state.agents,
|
||||
currentProviderId: state.currentProviderId,
|
||||
currentModelId: state.currentModelId,
|
||||
currentAgentName: state.currentAgentName,
|
||||
selectedProviderId: state.selectedProviderId,
|
||||
agentModelSelections: state.agentModelSelections,
|
||||
defaultProviders: state.defaultProviders,
|
||||
};
|
||||
|
||||
const nextSnapshot: DirectoryScopedConfig = {
|
||||
...baseSnapshot,
|
||||
currentProviderId: agent.model!.providerID,
|
||||
currentModelId: agent.model!.modelID,
|
||||
selectedProviderId: agent.model!.providerID,
|
||||
};
|
||||
|
||||
return {
|
||||
currentProviderId: agent.model!.providerID,
|
||||
currentModelId: agent.model!.modelID,
|
||||
selectedProviderId: agent.model!.providerID,
|
||||
directoryScoped: {
|
||||
...state.directoryScoped,
|
||||
[directoryKey]: nextSnapshot,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -671,28 +1138,29 @@ export const useConfigStore = create<ConfigStore>()(
|
||||
|
||||
initializeApp: async () => {
|
||||
try {
|
||||
console.log("Starting app initialization...");
|
||||
const debug = streamDebugEnabled();
|
||||
if (debug) console.log("Starting app initialization...");
|
||||
|
||||
const isConnected = await get().checkConnection();
|
||||
console.log("Connection check result:", isConnected);
|
||||
if (debug) console.log("Connection check result:", isConnected);
|
||||
|
||||
if (!isConnected) {
|
||||
console.log("Server not connected");
|
||||
if (debug) console.log("Server not connected");
|
||||
set({ isConnected: false });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Initializing app...");
|
||||
if (debug) console.log("Initializing app...");
|
||||
await opencodeClient.initApp();
|
||||
|
||||
console.log("Loading providers...");
|
||||
if (debug) console.log("Loading providers...");
|
||||
await get().loadProviders();
|
||||
|
||||
console.log("Loading agents...");
|
||||
if (debug) console.log("Loading agents...");
|
||||
await get().loadAgents();
|
||||
|
||||
set({ isInitialized: true, isConnected: true });
|
||||
console.log("App initialized successfully");
|
||||
if (debug) console.log("App initialized successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize app:", error);
|
||||
set({ isInitialized: false, isConnected: false });
|
||||
@@ -770,3 +1238,17 @@ if (!unsubscribeConfigStoreChanges) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let unsubscribeConfigStoreDirectoryChanges: (() => void) | null = null;
|
||||
|
||||
if (typeof window !== "undefined" && !unsubscribeConfigStoreDirectoryChanges) {
|
||||
unsubscribeConfigStoreDirectoryChanges = useDirectoryStore.subscribe((state, prevState) => {
|
||||
const nextKey = toDirectoryKey(state.currentDirectory);
|
||||
const prevKey = toDirectoryKey(prevState.currentDirectory);
|
||||
if (nextKey === prevKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
void useConfigStore.getState().activateDirectory(state.currentDirectory);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user