feat: refactor HomePage to integrate agents store and update related components
feat: add runtime event handling for providers in ProvidersSection feat: update routing to include Channels and Agents pages feat: extend route types and navigation items for Channels and Agents feat: implement agents store for managing agent data and interactions fix: update chat store to utilize agents store for agent-related functionality chore: export agents store from index fix: enhance runtime types for better event handling fix: update Vite config to handle dev server URL correctly
This commit is contained in:
500
electron/utils/agent-config.ts
Normal file
500
electron/utils/agent-config.ts
Normal file
@@ -0,0 +1,500 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import type { ProviderAccount } from '@runtime/lib/providers';
|
||||
import { DEFAULT_AGENT_ID, DEFAULT_MAIN_SESSION_SUFFIX, type AgentSummary, type AgentsSnapshot } from '@runtime/lib/agents';
|
||||
import { buildMainSessionKey, normalizeAgentId } from '@runtime/lib/models';
|
||||
import { getUserDataDir } from './paths';
|
||||
|
||||
interface StoredAgentEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
providerAccountId?: string | null;
|
||||
modelRef?: string | null;
|
||||
workspace?: string | null;
|
||||
agentDir?: string | null;
|
||||
channelTypes?: string[];
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
interface StoredAgentsConfig {
|
||||
agents: StoredAgentEntry[];
|
||||
channelOwners?: Record<string, string>;
|
||||
channelAccountOwners?: Record<string, string>;
|
||||
mainSessionSuffix?: string;
|
||||
}
|
||||
|
||||
const STORE_FILE_NAME = 'agents.json';
|
||||
|
||||
function getStorePath(): string {
|
||||
return path.join(getUserDataDir(), STORE_FILE_NAME);
|
||||
}
|
||||
|
||||
function getAgentsRootDir(): string {
|
||||
return path.join(getUserDataDir(), 'agents');
|
||||
}
|
||||
|
||||
function getMainWorkspacePath(): string {
|
||||
return path.join(getAgentsRootDir(), DEFAULT_AGENT_ID, 'workspace');
|
||||
}
|
||||
|
||||
function getMainAgentDirPath(): string {
|
||||
return path.join(getAgentsRootDir(), DEFAULT_AGENT_ID, 'agent');
|
||||
}
|
||||
|
||||
function getAgentWorkspacePath(agentId: string): string {
|
||||
return path.join(getAgentsRootDir(), agentId, 'workspace');
|
||||
}
|
||||
|
||||
function getAgentDirPath(agentId: string): string {
|
||||
return path.join(getAgentsRootDir(), agentId, 'agent');
|
||||
}
|
||||
|
||||
function ensureDir(dirPath: string | null | undefined): void {
|
||||
if (!dirPath) return;
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
function readStore(): StoredAgentsConfig {
|
||||
try {
|
||||
const filePath = getStorePath();
|
||||
if (fs.existsSync(filePath)) {
|
||||
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Partial<StoredAgentsConfig>;
|
||||
return {
|
||||
agents: Array.isArray(parsed.agents) ? parsed.agents : [],
|
||||
channelOwners: parsed.channelOwners && typeof parsed.channelOwners === 'object' ? parsed.channelOwners : {},
|
||||
channelAccountOwners: parsed.channelAccountOwners && typeof parsed.channelAccountOwners === 'object'
|
||||
? parsed.channelAccountOwners
|
||||
: {},
|
||||
mainSessionSuffix: typeof parsed.mainSessionSuffix === 'string' ? parsed.mainSessionSuffix : DEFAULT_MAIN_SESSION_SUFFIX,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Fall back to an empty store on malformed JSON.
|
||||
}
|
||||
|
||||
return {
|
||||
agents: [],
|
||||
channelOwners: {},
|
||||
channelAccountOwners: {},
|
||||
mainSessionSuffix: DEFAULT_MAIN_SESSION_SUFFIX,
|
||||
};
|
||||
}
|
||||
|
||||
function writeStore(store: StoredAgentsConfig): void {
|
||||
const filePath = getStorePath();
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, JSON.stringify(store, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
function formatModelDisplay(modelRef: string | null | undefined, fallbackLabel: string): string {
|
||||
const trimmed = String(modelRef ?? '').trim();
|
||||
if (!trimmed) return fallbackLabel;
|
||||
|
||||
const parts = trimmed.split('/');
|
||||
return parts[parts.length - 1] || trimmed;
|
||||
}
|
||||
|
||||
function normalizeAgentName(name: string): string {
|
||||
return name.trim() || 'Agent';
|
||||
}
|
||||
|
||||
function slugifyAgentId(name: string): string {
|
||||
const normalized = name
|
||||
.normalize('NFKD')
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[_\s]+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
|
||||
if (!normalized || /^\d+$/.test(normalized)) return 'agent';
|
||||
if (normalized === DEFAULT_AGENT_ID) return 'agent';
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function buildUniqueAgentId(existingIds: Set<string>, name: string): string {
|
||||
const baseId = slugifyAgentId(name);
|
||||
let nextId = baseId;
|
||||
let index = 2;
|
||||
|
||||
while (existingIds.has(nextId)) {
|
||||
nextId = `${baseId}-${index}`;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return nextId;
|
||||
}
|
||||
|
||||
function getDefaultAccount(accounts: ProviderAccount[], defaultAccountId: string | null): ProviderAccount | null {
|
||||
return accounts.find((account) => account.id === defaultAccountId) ?? accounts[0] ?? null;
|
||||
}
|
||||
|
||||
function buildMainAgent(defaultAccount: ProviderAccount | null, mainSessionSuffix: string): AgentSummary {
|
||||
ensureDir(getMainWorkspacePath());
|
||||
ensureDir(getMainAgentDirPath());
|
||||
|
||||
return {
|
||||
id: DEFAULT_AGENT_ID,
|
||||
name: 'Main Agent',
|
||||
isDefault: true,
|
||||
providerAccountId: defaultAccount?.id ?? null,
|
||||
modelRef: defaultAccount?.model ?? null,
|
||||
modelDisplay: formatModelDisplay(defaultAccount?.model, defaultAccount?.label || 'Unassigned'),
|
||||
mainSessionKey: buildMainSessionKey(DEFAULT_AGENT_ID, mainSessionSuffix),
|
||||
vendorId: defaultAccount?.vendorId ?? null,
|
||||
source: 'synthetic-main',
|
||||
overrideModelRef: null,
|
||||
inheritedModel: true,
|
||||
workspace: getMainWorkspacePath(),
|
||||
agentDir: getMainAgentDirPath(),
|
||||
channelTypes: [],
|
||||
};
|
||||
}
|
||||
|
||||
function collectChannelOwners(
|
||||
store: StoredAgentsConfig,
|
||||
entries: StoredAgentEntry[],
|
||||
): { channelOwners: Record<string, string>; channelAccountOwners: Record<string, string> } {
|
||||
const explicitChannelOwners = store.channelOwners && typeof store.channelOwners === 'object'
|
||||
? { ...store.channelOwners }
|
||||
: {};
|
||||
const explicitChannelAccountOwners = store.channelAccountOwners && typeof store.channelAccountOwners === 'object'
|
||||
? { ...store.channelAccountOwners }
|
||||
: {};
|
||||
|
||||
for (const entry of entries) {
|
||||
const normalizedId = normalizeAgentId(entry.id);
|
||||
const channels = Array.isArray(entry.channelTypes) ? entry.channelTypes.filter(Boolean) : [];
|
||||
for (const channelType of channels) {
|
||||
if (!explicitChannelOwners[channelType]) {
|
||||
explicitChannelOwners[channelType] = normalizedId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
channelOwners: explicitChannelOwners,
|
||||
channelAccountOwners: explicitChannelAccountOwners,
|
||||
};
|
||||
}
|
||||
|
||||
function mapStoredAgentToSummary(
|
||||
entry: StoredAgentEntry,
|
||||
accounts: ProviderAccount[],
|
||||
defaultAccount: ProviderAccount | null,
|
||||
mainSessionSuffix: string,
|
||||
): AgentSummary {
|
||||
const normalizedId = normalizeAgentId(entry.id);
|
||||
const configuredAccount = entry.providerAccountId
|
||||
? accounts.find((account) => account.id === entry.providerAccountId) ?? null
|
||||
: null;
|
||||
const effectiveAccount = configuredAccount ?? defaultAccount;
|
||||
const effectiveModelRef = entry.modelRef ?? effectiveAccount?.model ?? defaultAccount?.model ?? null;
|
||||
const workspace = entry.workspace || getAgentWorkspacePath(normalizedId);
|
||||
const agentDir = entry.agentDir || getAgentDirPath(normalizedId);
|
||||
|
||||
ensureDir(agentDir);
|
||||
ensureDir(workspace);
|
||||
|
||||
return {
|
||||
id: normalizedId,
|
||||
name: normalizeAgentName(entry.name),
|
||||
isDefault: false,
|
||||
providerAccountId: entry.providerAccountId ?? null,
|
||||
modelRef: effectiveModelRef,
|
||||
modelDisplay: formatModelDisplay(effectiveModelRef, normalizeAgentName(entry.name)),
|
||||
mainSessionKey: buildMainSessionKey(normalizedId, mainSessionSuffix),
|
||||
vendorId: configuredAccount?.vendorId ?? effectiveAccount?.vendorId ?? null,
|
||||
source: 'agent-config',
|
||||
overrideModelRef: entry.modelRef ?? null,
|
||||
inheritedModel: !entry.modelRef,
|
||||
workspace,
|
||||
agentDir,
|
||||
channelTypes: Array.isArray(entry.channelTypes) ? entry.channelTypes.filter(Boolean) : [],
|
||||
};
|
||||
}
|
||||
|
||||
function buildSnapshotFromStore(
|
||||
store: StoredAgentsConfig,
|
||||
accounts: ProviderAccount[],
|
||||
defaultAccountId: string | null,
|
||||
): AgentsSnapshot {
|
||||
const defaultAccount = getDefaultAccount(accounts, defaultAccountId);
|
||||
const mainSessionSuffix = store.mainSessionSuffix || DEFAULT_MAIN_SESSION_SUFFIX;
|
||||
const normalizedEntries = Array.from(
|
||||
new Map(
|
||||
store.agents
|
||||
.filter((entry) => Boolean(entry) && typeof entry.id === 'string' && entry.id.trim().length > 0)
|
||||
.map((entry) => [normalizeAgentId(entry.id), { ...entry, id: normalizeAgentId(entry.id) }]),
|
||||
).values(),
|
||||
).filter((entry) => entry.id !== DEFAULT_AGENT_ID);
|
||||
|
||||
const { channelOwners, channelAccountOwners } = collectChannelOwners(store, normalizedEntries);
|
||||
const agents = [
|
||||
buildMainAgent(defaultAccount, mainSessionSuffix),
|
||||
...normalizedEntries.map((entry) => mapStoredAgentToSummary(entry, accounts, defaultAccount, mainSessionSuffix)),
|
||||
];
|
||||
|
||||
const configuredChannelTypes = Array.from(new Set([
|
||||
...Object.keys(channelOwners),
|
||||
...Object.keys(channelAccountOwners).map((key) => key.split(':')[0]).filter(Boolean),
|
||||
]));
|
||||
|
||||
return {
|
||||
agents,
|
||||
models: agents,
|
||||
defaultAgentId: DEFAULT_AGENT_ID,
|
||||
defaultProviderAccountId: defaultAccount?.id ?? null,
|
||||
defaultModelRef: defaultAccount?.model ?? null,
|
||||
mainSessionSuffix,
|
||||
configuredChannelTypes,
|
||||
channelOwners,
|
||||
channelAccountOwners,
|
||||
};
|
||||
}
|
||||
|
||||
function syncAgentChannelMembership(store: StoredAgentsConfig, channelType: string): void {
|
||||
const normalizedChannelType = String(channelType ?? '').trim();
|
||||
if (!normalizedChannelType) return;
|
||||
|
||||
const ownerIds = new Set<string>();
|
||||
const channelOwnerId = store.channelOwners?.[normalizedChannelType];
|
||||
if (channelOwnerId) {
|
||||
ownerIds.add(normalizeAgentId(channelOwnerId));
|
||||
}
|
||||
|
||||
for (const [key, ownerId] of Object.entries(store.channelAccountOwners ?? {})) {
|
||||
if (!key.startsWith(`${normalizedChannelType}:`) || !ownerId) continue;
|
||||
ownerIds.add(normalizeAgentId(ownerId));
|
||||
}
|
||||
|
||||
store.agents = store.agents.map((entry) => {
|
||||
const normalizedId = normalizeAgentId(entry.id);
|
||||
const currentChannelTypes = Array.isArray(entry.channelTypes) ? entry.channelTypes.filter(Boolean) : [];
|
||||
const hasChannel = currentChannelTypes.includes(normalizedChannelType);
|
||||
const shouldHaveChannel = ownerIds.has(normalizedId);
|
||||
|
||||
if (hasChannel === shouldHaveChannel) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
return {
|
||||
...entry,
|
||||
channelTypes: shouldHaveChannel
|
||||
? [...currentChannelTypes, normalizedChannelType]
|
||||
: currentChannelTypes.filter((value) => value !== normalizedChannelType),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function listAgentsSnapshot(
|
||||
accounts: ProviderAccount[],
|
||||
defaultAccountId: string | null,
|
||||
): AgentsSnapshot {
|
||||
return buildSnapshotFromStore(readStore(), accounts, defaultAccountId);
|
||||
}
|
||||
|
||||
export function createAgentConfig(
|
||||
name: string,
|
||||
options: { inheritWorkspace?: boolean } | undefined,
|
||||
accounts: ProviderAccount[],
|
||||
defaultAccountId: string | null,
|
||||
): AgentsSnapshot {
|
||||
const trimmedName = normalizeAgentName(name);
|
||||
const store = readStore();
|
||||
const existingIds = new Set(store.agents.map((entry) => normalizeAgentId(entry.id)));
|
||||
const agentId = buildUniqueAgentId(existingIds, trimmedName);
|
||||
const now = new Date().toISOString();
|
||||
const workspace = options?.inheritWorkspace ? getMainWorkspacePath() : getAgentWorkspacePath(agentId);
|
||||
const agentDir = getAgentDirPath(agentId);
|
||||
|
||||
ensureDir(workspace);
|
||||
ensureDir(agentDir);
|
||||
|
||||
store.agents = [
|
||||
...store.agents,
|
||||
{
|
||||
id: agentId,
|
||||
name: trimmedName,
|
||||
providerAccountId: null,
|
||||
modelRef: null,
|
||||
workspace,
|
||||
agentDir,
|
||||
channelTypes: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
},
|
||||
];
|
||||
|
||||
writeStore(store);
|
||||
return buildSnapshotFromStore(store, accounts, defaultAccountId);
|
||||
}
|
||||
|
||||
export function updateAgentName(
|
||||
agentId: string,
|
||||
name: string,
|
||||
accounts: ProviderAccount[],
|
||||
defaultAccountId: string | null,
|
||||
): AgentsSnapshot {
|
||||
const normalizedId = normalizeAgentId(agentId);
|
||||
if (normalizedId === DEFAULT_AGENT_ID) {
|
||||
throw new Error('Main Agent cannot be renamed');
|
||||
}
|
||||
|
||||
const store = readStore();
|
||||
const index = store.agents.findIndex((entry) => normalizeAgentId(entry.id) === normalizedId);
|
||||
if (index === -1) {
|
||||
throw new Error(`Agent "${normalizedId}" not found`);
|
||||
}
|
||||
|
||||
store.agents[index] = {
|
||||
...store.agents[index],
|
||||
name: normalizeAgentName(name),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
writeStore(store);
|
||||
return buildSnapshotFromStore(store, accounts, defaultAccountId);
|
||||
}
|
||||
|
||||
export function updateAgentModelConfig(
|
||||
agentId: string,
|
||||
modelRef: string | null,
|
||||
providerAccountId: string | null | undefined,
|
||||
accounts: ProviderAccount[],
|
||||
defaultAccountId: string | null,
|
||||
): AgentsSnapshot {
|
||||
const normalizedId = normalizeAgentId(agentId);
|
||||
if (normalizedId === DEFAULT_AGENT_ID) {
|
||||
throw new Error('Main Agent model is managed from Models');
|
||||
}
|
||||
|
||||
const store = readStore();
|
||||
const index = store.agents.findIndex((entry) => normalizeAgentId(entry.id) === normalizedId);
|
||||
if (index === -1) {
|
||||
throw new Error(`Agent "${normalizedId}" not found`);
|
||||
}
|
||||
|
||||
const trimmedModelRef = typeof modelRef === 'string' ? modelRef.trim() : '';
|
||||
const inferredAccountId = providerAccountId
|
||||
?? (
|
||||
trimmedModelRef
|
||||
? accounts.find((account) => account.model === trimmedModelRef)?.id ?? store.agents[index].providerAccountId ?? null
|
||||
: null
|
||||
);
|
||||
|
||||
store.agents[index] = {
|
||||
...store.agents[index],
|
||||
providerAccountId: inferredAccountId ?? null,
|
||||
modelRef: trimmedModelRef || null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
writeStore(store);
|
||||
return buildSnapshotFromStore(store, accounts, defaultAccountId);
|
||||
}
|
||||
|
||||
export function deleteAgentConfig(
|
||||
agentId: string,
|
||||
accounts: ProviderAccount[],
|
||||
defaultAccountId: string | null,
|
||||
): AgentsSnapshot {
|
||||
const normalizedId = normalizeAgentId(agentId);
|
||||
if (normalizedId === DEFAULT_AGENT_ID) {
|
||||
throw new Error('Main Agent cannot be deleted');
|
||||
}
|
||||
|
||||
const store = readStore();
|
||||
const existingAgent = store.agents.find((entry) => normalizeAgentId(entry.id) === normalizedId);
|
||||
if (!existingAgent) {
|
||||
throw new Error(`Agent "${normalizedId}" not found`);
|
||||
}
|
||||
|
||||
store.agents = store.agents.filter((entry) => normalizeAgentId(entry.id) !== normalizedId);
|
||||
store.channelOwners = Object.fromEntries(
|
||||
Object.entries(store.channelOwners ?? {}).filter(([, ownerId]) => normalizeAgentId(ownerId) !== normalizedId),
|
||||
);
|
||||
store.channelAccountOwners = Object.fromEntries(
|
||||
Object.entries(store.channelAccountOwners ?? {}).filter(([, ownerId]) => normalizeAgentId(ownerId) !== normalizedId),
|
||||
);
|
||||
writeStore(store);
|
||||
|
||||
const agentRootDir = path.join(getAgentsRootDir(), normalizedId);
|
||||
try {
|
||||
fs.rmSync(agentRootDir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Best effort cleanup only.
|
||||
}
|
||||
|
||||
return buildSnapshotFromStore(store, accounts, defaultAccountId);
|
||||
}
|
||||
|
||||
export function assignChannelToAgent(
|
||||
agentId: string,
|
||||
channelType: string,
|
||||
accountId: string | null | undefined,
|
||||
accounts: ProviderAccount[],
|
||||
defaultAccountId: string | null,
|
||||
): AgentsSnapshot {
|
||||
const normalizedId = normalizeAgentId(agentId);
|
||||
const normalizedChannelType = String(channelType ?? '').trim();
|
||||
if (!normalizedChannelType) {
|
||||
throw new Error('channelType is required');
|
||||
}
|
||||
|
||||
const store = readStore();
|
||||
if (normalizedId !== DEFAULT_AGENT_ID && !store.agents.some((entry) => normalizeAgentId(entry.id) === normalizedId)) {
|
||||
throw new Error(`Agent "${normalizedId}" not found`);
|
||||
}
|
||||
|
||||
store.channelOwners = {
|
||||
...(store.channelOwners ?? {}),
|
||||
...(accountId ? {} : { [normalizedChannelType]: normalizedId }),
|
||||
};
|
||||
|
||||
if (accountId) {
|
||||
store.channelAccountOwners = {
|
||||
...(store.channelAccountOwners ?? {}),
|
||||
[`${normalizedChannelType}:${accountId}`]: normalizedId,
|
||||
};
|
||||
}
|
||||
|
||||
syncAgentChannelMembership(store, normalizedChannelType);
|
||||
|
||||
writeStore(store);
|
||||
return buildSnapshotFromStore(store, accounts, defaultAccountId);
|
||||
}
|
||||
|
||||
export function clearChannelBinding(
|
||||
channelType: string,
|
||||
accountId: string | null | undefined,
|
||||
accounts: ProviderAccount[],
|
||||
defaultAccountId: string | null,
|
||||
): AgentsSnapshot {
|
||||
const normalizedChannelType = String(channelType ?? '').trim();
|
||||
if (!normalizedChannelType) {
|
||||
throw new Error('channelType is required');
|
||||
}
|
||||
|
||||
const store = readStore();
|
||||
const ownerId = accountId
|
||||
? store.channelAccountOwners?.[`${normalizedChannelType}:${accountId}`]
|
||||
: store.channelOwners?.[normalizedChannelType];
|
||||
|
||||
if (accountId) {
|
||||
const nextAccountOwners = { ...(store.channelAccountOwners ?? {}) };
|
||||
delete nextAccountOwners[`${normalizedChannelType}:${accountId}`];
|
||||
store.channelAccountOwners = nextAccountOwners;
|
||||
} else {
|
||||
const nextChannelOwners = { ...(store.channelOwners ?? {}) };
|
||||
delete nextChannelOwners[normalizedChannelType];
|
||||
store.channelOwners = nextChannelOwners;
|
||||
}
|
||||
|
||||
if (ownerId) {
|
||||
syncAgentChannelMembership(store, normalizedChannelType);
|
||||
}
|
||||
|
||||
writeStore(store);
|
||||
return buildSnapshotFromStore(store, accounts, defaultAccountId);
|
||||
}
|
||||
Reference in New Issue
Block a user