feat: add models management and usage history components

- Introduced RequestContentDialog for displaying request content details.
- Added UsageBarChart for visualizing token usage data.
- Implemented UsageHistorySection to manage and display usage history with filtering and pagination.
- Created provider-types for managing provider-related types.
- Developed ModelsPage to encapsulate models configuration, providers, and usage history.
- Defined usage-history types and utility functions for managing usage data.
- Updated routing to include models page and redirect agents to models.
- Refactored chat store to integrate models instead of agents.
- Established models store for managing model-related state and data fetching.
This commit is contained in:
duanshuwen
2026-04-18 09:41:59 +08:00
parent 1205a96661
commit 85d92b937f
28 changed files with 343 additions and 258 deletions

View File

@@ -1,2 +1,7 @@
require('bytenode') "use strict";
require('./main.jsc') require("electron");
require("./main-Bp9J8VEe.js");
require("electron-squirrel-startup");
require("electron-log");
require("bytenode");
require("axios");

View File

@@ -4,9 +4,9 @@ import { providerApiService } from '@electron/service/provider-api-service';
import type { HostApiContext } from './context'; import type { HostApiContext } from './context';
import type { HostApiRequest } from './route-utils'; import type { HostApiRequest } from './route-utils';
import { normalizeRequest } from './route-utils'; import { normalizeRequest } from './route-utils';
import { handleAgentRoutes } from './routes/agents';
import { handleFileRoutes } from './routes/files'; import { handleFileRoutes } from './routes/files';
import { handleGatewayRoutes } from './routes/gateway'; import { handleGatewayRoutes } from './routes/gateway';
import { handleModelRoutes } from './routes/models';
import { handleProviderRoutes } from './routes/providers'; import { handleProviderRoutes } from './routes/providers';
import { handleSessionRoutes } from './routes/sessions'; import { handleSessionRoutes } from './routes/sessions';
@@ -17,7 +17,7 @@ type RouteHandler = (
const routeHandlers: RouteHandler[] = [ const routeHandlers: RouteHandler[] = [
handleProviderRoutes, handleProviderRoutes,
handleAgentRoutes, handleModelRoutes,
handleGatewayRoutes, handleGatewayRoutes,
handleFileRoutes, handleFileRoutes,
handleSessionRoutes, handleSessionRoutes,

View File

@@ -5,7 +5,7 @@ import {
buildMainSessionKey, buildMainSessionKey,
normalizeAgentId, normalizeAgentId,
type AgentSummary, type AgentSummary,
} from '@runtime/lib/agents'; } from '@runtime/lib/models';
import type { HostApiContext } from '../context'; import type { HostApiContext } from '../context';
import type { NormalizedHostApiRequest } from '../route-utils'; import type { NormalizedHostApiRequest } from '../route-utils';
import { ok } from '../route-utils'; import { ok } from '../route-utils';
@@ -18,10 +18,10 @@ function formatModelDisplay(modelRef: string | null | undefined, fallbackLabel:
return parts[parts.length - 1] || trimmed; return parts[parts.length - 1] || trimmed;
} }
function buildMainAgent(defaultAccount: ProviderAccount | null): AgentSummary { function buildMainModel(defaultAccount: ProviderAccount | null): AgentSummary {
return { return {
id: DEFAULT_AGENT_ID, id: DEFAULT_AGENT_ID,
name: 'Main Agent', name: 'Main Model',
isDefault: true, isDefault: true,
providerAccountId: defaultAccount?.id ?? null, providerAccountId: defaultAccount?.id ?? null,
modelRef: defaultAccount?.model ?? null, modelRef: defaultAccount?.model ?? null,
@@ -32,7 +32,7 @@ function buildMainAgent(defaultAccount: ProviderAccount | null): AgentSummary {
}; };
} }
function buildProviderBackedAgents(accounts: ProviderAccount[]): AgentSummary[] { function buildProviderBackedModels(accounts: ProviderAccount[]): AgentSummary[] {
const seen = new Set<string>(); const seen = new Set<string>();
const summaries: AgentSummary[] = []; const summaries: AgentSummary[] = [];
@@ -57,12 +57,12 @@ function buildProviderBackedAgents(accounts: ProviderAccount[]): AgentSummary[]
return summaries; return summaries;
} }
export async function handleAgentRoutes( export async function handleModelRoutes(
request: NormalizedHostApiRequest, request: NormalizedHostApiRequest,
ctx: HostApiContext, ctx: HostApiContext,
) { ) {
const { pathname, method } = request; const { pathname, method } = request;
if (pathname !== '/api/agents' || method !== 'GET') { if ((pathname !== '/api/models' && pathname !== '/api/agents') || method !== 'GET') {
return null; return null;
} }
@@ -72,14 +72,15 @@ export async function handleAgentRoutes(
const defaultAccountId = ctx.providerApiService.getDefault().accountId; const defaultAccountId = ctx.providerApiService.getDefault().accountId;
const defaultAccount = accounts.find((account) => account.id === defaultAccountId) ?? accounts[0] ?? null; const defaultAccount = accounts.find((account) => account.id === defaultAccountId) ?? accounts[0] ?? null;
const agents = [ const models = [
buildMainAgent(defaultAccount), buildMainModel(defaultAccount),
...buildProviderBackedAgents(accounts), ...buildProviderBackedModels(accounts),
]; ];
return ok({ return ok({
success: true, success: true,
agents, models,
agents: models,
defaultAgentId: DEFAULT_AGENT_ID, defaultAgentId: DEFAULT_AGENT_ID,
defaultProviderAccountId: defaultAccount?.id ?? null, defaultProviderAccountId: defaultAccount?.id ?? null,
defaultModelRef: defaultAccount?.model ?? null, defaultModelRef: defaultAccount?.model ?? null,

View File

@@ -1,6 +1,9 @@
import { sessionStore } from '@electron/gateway/session-store'; import { sessionStore } from '@electron/gateway/session-store';
import { getTranscriptFilePath } from '@electron/utils/token-usage-writer'; import {
import { buildAgentSessionKey, normalizeAgentSessionKey, parseSessionKey } from '@runtime/lib/agents'; getTranscriptFilePath,
getTranscriptPathCandidates,
} from '@electron/utils/token-usage-writer';
import { buildAgentSessionKey, normalizeAgentSessionKey, parseSessionKey } from '@runtime/lib/models';
import type { HostApiContext } from '../context'; import type { HostApiContext } from '../context';
import type { NormalizedHostApiRequest } from '../route-utils'; import type { NormalizedHostApiRequest } from '../route-utils';
import { fail, ok, parseJsonBody } from '../route-utils'; import { fail, ok, parseJsonBody } from '../route-utils';
@@ -41,19 +44,44 @@ export async function handleSessionRoutes(
try { try {
const fsP = await import('node:fs/promises'); const fsP = await import('node:fs/promises');
const transcriptPath = getTranscriptFilePath(identity.sessionKey); let transcriptPath = getTranscriptFilePath(identity.sessionKey);
let raw: string; let raw: string | null = null;
let lastError: unknown = null;
for (const candidatePath of getTranscriptPathCandidates(identity.sessionKey)) {
try { try {
raw = await fsP.readFile(transcriptPath, 'utf8'); raw = await fsP.readFile(candidatePath, 'utf8');
} catch (error: any) { transcriptPath = candidatePath;
const requestedSessionKey = request.url.searchParams.get('sessionKey')?.trim() || ''; break;
if (error?.code === 'ENOENT' && requestedSessionKey && requestedSessionKey !== identity.sessionKey) { } catch (error) {
raw = await fsP.readFile(getTranscriptFilePath(requestedSessionKey), 'utf8'); lastError = error;
} else { if ((error as { code?: string } | null)?.code !== 'ENOENT') {
throw error; throw error;
} }
} }
}
if (raw === null) {
const requestedSessionKey = request.url.searchParams.get('sessionKey')?.trim() || '';
if (requestedSessionKey && requestedSessionKey !== identity.sessionKey) {
for (const candidatePath of getTranscriptPathCandidates(requestedSessionKey)) {
try {
raw = await fsP.readFile(candidatePath, 'utf8');
transcriptPath = candidatePath;
break;
} catch (candidateError) {
lastError = candidateError;
if ((candidateError as { code?: string } | null)?.code !== 'ENOENT') {
throw candidateError;
}
}
}
}
}
if (raw === null) {
throw lastError ?? Object.assign(new Error('Transcript not found'), { code: 'ENOENT' });
}
const lines = raw.split(/\r?\n/).filter(Boolean); const lines = raw.split(/\r?\n/).filter(Boolean);
const messages = lines.flatMap((line) => { const messages = lines.flatMap((line) => {
@@ -90,19 +118,18 @@ export async function handleSessionRoutes(
sessionStore.deleteSession(sessionKey); sessionStore.deleteSession(sessionKey);
const transcriptPath = getTranscriptFilePath(sessionKey);
try {
const fsP = await import('node:fs/promises'); const fsP = await import('node:fs/promises');
const transcriptPathCandidates = Array.from(new Set([
...getTranscriptPathCandidates(sessionKey),
...(rawSessionKey && rawSessionKey !== sessionKey ? getTranscriptPathCandidates(rawSessionKey) : []),
]));
for (const transcriptPath of transcriptPathCandidates) {
try {
await fsP.rename(transcriptPath, transcriptPath.replace(/\.jsonl$/, '.deleted.jsonl')); await fsP.rename(transcriptPath, transcriptPath.replace(/\.jsonl$/, '.deleted.jsonl'));
break;
} catch { } catch {
if (rawSessionKey && rawSessionKey !== sessionKey) { continue;
try {
const fsP = await import('node:fs/promises');
const legacyTranscriptPath = getTranscriptFilePath(rawSessionKey);
await fsP.rename(legacyTranscriptPath, legacyTranscriptPath.replace(/\.jsonl$/, '.deleted.jsonl'));
} catch {
// Best effort: transcript may not exist yet.
}
} }
} }

View File

@@ -3,7 +3,7 @@ import { createProvider } from '@electron/providers';
import type { BaseProvider } from '@electron/providers/BaseProvider'; import type { BaseProvider } from '@electron/providers/BaseProvider';
import { providerApiService } from '@electron/service/provider-api-service'; import { providerApiService } from '@electron/service/provider-api-service';
import logManager from '@electron/service/logger'; import logManager from '@electron/service/logger';
import { normalizeAgentSessionKey } from '@runtime/lib/agents'; import { normalizeAgentSessionKey } from '@runtime/lib/models';
import type { RawMessage } from '@runtime/shared/chat-model'; import type { RawMessage } from '@runtime/shared/chat-model';
import { sessionStore } from '../session-store'; import { sessionStore } from '../session-store';
import type { GatewayEvent, GatewayRpcParams, GatewayRpcReturns } from '../types'; import type { GatewayEvent, GatewayRpcParams, GatewayRpcReturns } from '../types';

View File

@@ -2,7 +2,7 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { app } from 'electron'; import { app } from 'electron';
import logManager from '@electron/service/logger'; import logManager from '@electron/service/logger';
import { normalizeAgentSessionKey } from '@runtime/lib/agents'; import { normalizeAgentSessionKey } from '@runtime/lib/models';
import type { RawMessage } from '@runtime/shared/chat-model'; import type { RawMessage } from '@runtime/shared/chat-model';
let sessionsFilePath: string | null = null; let sessionsFilePath: string | null = null;

View File

@@ -1,21 +1,39 @@
import { app } from 'electron'; import { app } from 'electron';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { parseSessionKey } from '@runtime/lib/agents'; import { parseSessionKey } from '@runtime/lib/models';
export function getTranscriptFilePath(sessionKey: string): string { const PRIMARY_TRANSCRIPT_ROOT_DIR = 'models';
const LEGACY_TRANSCRIPT_ROOT_DIR = 'agents';
function buildTranscriptFilePath(sessionKey: string, rootDirName: string): string {
const parsed = parseSessionKey(sessionKey); const parsed = parseSessionKey(sessionKey);
let agentId = parsed.isAgentSession ? parsed.agentId : 'default'; const agentId = parsed.isAgentSession ? parsed.agentId : 'default';
let sessionId = parsed.isAgentSession ? parsed.sessionId : sessionKey; let sessionId = parsed.isAgentSession ? parsed.sessionId : sessionKey;
if (!sessionId) { if (!sessionId) {
sessionId = 'unknown'; sessionId = 'unknown';
} }
const baseDir = path.join(app.getPath('userData'), 'agents', agentId, 'sessions'); const baseDir = path.join(app.getPath('userData'), rootDirName, agentId, 'sessions');
return path.join(baseDir, `${sessionId}.jsonl`); return path.join(baseDir, `${sessionId}.jsonl`);
} }
export function getTranscriptFilePath(sessionKey: string): string {
return buildTranscriptFilePath(sessionKey, PRIMARY_TRANSCRIPT_ROOT_DIR);
}
export function getLegacyTranscriptFilePath(sessionKey: string): string {
return buildTranscriptFilePath(sessionKey, LEGACY_TRANSCRIPT_ROOT_DIR);
}
export function getTranscriptPathCandidates(sessionKey: string): string[] {
return Array.from(new Set([
getTranscriptFilePath(sessionKey),
getLegacyTranscriptFilePath(sessionKey),
]));
}
export function appendTranscriptLine(sessionKey: string, lineObject: any): void { export function appendTranscriptLine(sessionKey: string, lineObject: any): void {
const filePath = getTranscriptFilePath(sessionKey); const filePath = getTranscriptFilePath(sessionKey);
fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.mkdirSync(path.dirname(filePath), { recursive: true });

View File

@@ -14,12 +14,14 @@ export {
type TokenUsageHistoryEntry, type TokenUsageHistoryEntry,
} from './token-usage-core'; } from './token-usage-core';
async function listAgentIdsWithSessionDirs(): Promise<string[]> { const TRANSCRIPT_ROOT_DIR_NAMES = ['models', 'agents'] as const;
const agentsDir = join(app.getPath('userData'), 'agents');
async function listAgentIdsWithSessionDirs(rootDirName: string): Promise<string[]> {
const rootDir = join(app.getPath('userData'), rootDirName);
const agentIds = new Set<string>(); const agentIds = new Set<string>();
try { try {
const agentEntries = await readdir(agentsDir, { withFileTypes: true }); const agentEntries = await readdir(rootDir, { withFileTypes: true });
for (const entry of agentEntries) { for (const entry of agentEntries) {
if (entry.isDirectory()) { if (entry.isDirectory()) {
const normalized = entry.name.trim(); const normalized = entry.name.trim();
@@ -36,14 +38,14 @@ async function listAgentIdsWithSessionDirs(): Promise<string[]> {
} }
async function listRecentSessionFiles(): Promise<Array<{ filePath: string; sessionId: string; agentId: string; mtimeMs: number }>> { async function listRecentSessionFiles(): Promise<Array<{ filePath: string; sessionId: string; agentId: string; mtimeMs: number }>> {
const agentsDir = join(app.getPath('userData'), 'agents'); const filesBySession = new Map<string, { filePath: string; sessionId: string; agentId: string; mtimeMs: number }>();
try { try {
const agentEntries = await listAgentIdsWithSessionDirs(); for (const rootDirName of TRANSCRIPT_ROOT_DIR_NAMES) {
const files: Array<{ filePath: string; sessionId: string; agentId: string; mtimeMs: number }> = []; const agentEntries = await listAgentIdsWithSessionDirs(rootDirName);
for (const agentId of agentEntries) { for (const agentId of agentEntries) {
const sessionsDir = join(agentsDir, agentId, 'sessions'); const sessionsDir = join(app.getPath('userData'), rootDirName, agentId, 'sessions');
try { try {
const sessionEntries = await readdir(sessionsDir); const sessionEntries = await readdir(sessionsDir);
@@ -53,12 +55,17 @@ async function listRecentSessionFiles(): Promise<Array<{ filePath: string; sessi
const filePath = join(sessionsDir, fileName); const filePath = join(sessionsDir, fileName);
try { try {
const fileStat = await stat(filePath); const fileStat = await stat(filePath);
files.push({ const sessionKey = `${agentId}:${sessionId}`;
const existing = filesBySession.get(sessionKey);
if (!existing || fileStat.mtimeMs > existing.mtimeMs) {
filesBySession.set(sessionKey, {
filePath, filePath,
sessionId, sessionId,
agentId, agentId,
mtimeMs: fileStat.mtimeMs, mtimeMs: fileStat.mtimeMs,
}); });
}
} catch { } catch {
continue; continue;
} }
@@ -67,7 +74,9 @@ async function listRecentSessionFiles(): Promise<Array<{ filePath: string; sessi
continue; continue;
} }
} }
}
const files = [...filesBySession.values()];
files.sort((a, b) => b.mtimeMs - a.mtimeMs); files.sort((a, b) => b.mtimeMs - a.mtimeMs);
return files; return files;
} catch { } catch {

View File

@@ -1,92 +1 @@
export const DEFAULT_AGENT_ID = 'main'; export * from './models';
export const DEFAULT_MAIN_SESSION_SUFFIX = 'main';
export interface AgentSummary {
id: string;
name: string;
isDefault: boolean;
providerAccountId: string | null;
modelRef: string | null;
modelDisplay: string;
mainSessionKey: string;
vendorId?: string | null;
source?: 'synthetic-main' | 'provider-account';
}
export interface AgentsSnapshot {
agents: AgentSummary[];
defaultAgentId: string;
defaultProviderAccountId: string | null;
defaultModelRef: string | null;
mainSessionSuffix: string;
configuredChannelTypes: string[];
channelOwners: Record<string, string>;
channelAccountOwners: Record<string, string>;
}
export interface ParsedSessionKey {
sessionKey: string;
agentId: string;
sessionId: string;
isAgentSession: boolean;
}
export function normalizeAgentId(value: string | null | undefined): string {
const normalized = String(value ?? '').trim().toLowerCase();
return normalized || DEFAULT_AGENT_ID;
}
export function normalizeSessionSuffix(value: string | null | undefined): string {
const normalized = String(value ?? '').trim().toLowerCase();
return normalized || DEFAULT_MAIN_SESSION_SUFFIX;
}
export function buildAgentSessionKey(agentId: string, sessionId: string): string {
return `agent:${normalizeAgentId(agentId)}:${normalizeSessionSuffix(sessionId)}`;
}
export function buildMainSessionKey(
agentId: string,
sessionId = DEFAULT_MAIN_SESSION_SUFFIX,
): string {
return buildAgentSessionKey(agentId, sessionId);
}
export function parseSessionKey(sessionKey: string): ParsedSessionKey {
const trimmed = String(sessionKey ?? '').trim();
if (trimmed.startsWith('agent:')) {
const parts = trimmed.split(':');
const agentId = normalizeAgentId(parts[1]);
const sessionId = normalizeSessionSuffix(parts.slice(2).join(':'));
return {
sessionKey: buildAgentSessionKey(agentId, sessionId),
agentId,
sessionId,
isAgentSession: true,
};
}
if (trimmed.startsWith('local:')) {
const parts = trimmed.split(':');
const agentId = normalizeAgentId(parts[1]);
const sessionId = normalizeSessionSuffix(parts.slice(2).join(':'));
return {
sessionKey: buildAgentSessionKey(agentId, sessionId),
agentId,
sessionId,
isAgentSession: true,
};
}
return {
sessionKey: trimmed,
agentId: DEFAULT_AGENT_ID,
sessionId: normalizeSessionSuffix(trimmed),
isAgentSession: false,
};
}
export function normalizeAgentSessionKey(sessionKey: string): string {
return parseSessionKey(sessionKey).sessionKey;
}

View File

@@ -0,0 +1,112 @@
export const DEFAULT_AGENT_ID = 'main';
export const DEFAULT_MAIN_SESSION_SUFFIX = 'main';
export const DEFAULT_MODEL_ID = DEFAULT_AGENT_ID;
export interface AgentSummary {
id: string;
name: string;
isDefault: boolean;
providerAccountId: string | null;
modelRef: string | null;
modelDisplay: string;
mainSessionKey: string;
vendorId?: string | null;
source?: 'synthetic-main' | 'provider-account';
}
export type ModelSummary = AgentSummary;
export interface ModelsSnapshot {
models: ModelSummary[];
agents?: ModelSummary[];
defaultAgentId: string;
defaultProviderAccountId: string | null;
defaultModelRef: string | null;
mainSessionSuffix: string;
configuredChannelTypes: string[];
channelOwners: Record<string, string>;
channelAccountOwners: Record<string, string>;
}
export interface AgentsSnapshot {
agents: AgentSummary[];
models?: AgentSummary[];
defaultAgentId: string;
defaultProviderAccountId: string | null;
defaultModelRef: string | null;
mainSessionSuffix: string;
configuredChannelTypes: string[];
channelOwners: Record<string, string>;
channelAccountOwners: Record<string, string>;
}
export interface ParsedSessionKey {
sessionKey: string;
agentId: string;
sessionId: string;
isAgentSession: boolean;
}
export function normalizeAgentId(value: string | null | undefined): string {
const normalized = String(value ?? '').trim().toLowerCase();
return normalized || DEFAULT_AGENT_ID;
}
export function normalizeSessionSuffix(value: string | null | undefined): string {
const normalized = String(value ?? '').trim().toLowerCase();
return normalized || DEFAULT_MAIN_SESSION_SUFFIX;
}
export function buildAgentSessionKey(agentId: string, sessionId: string): string {
return `agent:${normalizeAgentId(agentId)}:${normalizeSessionSuffix(sessionId)}`;
}
export function buildMainSessionKey(
agentId: string,
sessionId = DEFAULT_MAIN_SESSION_SUFFIX,
): string {
return buildAgentSessionKey(agentId, sessionId);
}
export function parseSessionKey(sessionKey: string): ParsedSessionKey {
const trimmed = String(sessionKey ?? '').trim();
if (trimmed.startsWith('agent:')) {
const parts = trimmed.split(':');
const agentId = normalizeAgentId(parts[1]);
const sessionId = normalizeSessionSuffix(parts.slice(2).join(':'));
return {
sessionKey: buildAgentSessionKey(agentId, sessionId),
agentId,
sessionId,
isAgentSession: true,
};
}
if (trimmed.startsWith('local:')) {
const parts = trimmed.split(':');
const agentId = normalizeAgentId(parts[1]);
const sessionId = normalizeSessionSuffix(parts.slice(2).join(':'));
return {
sessionKey: buildAgentSessionKey(agentId, sessionId),
agentId,
sessionId,
isAgentSession: true,
};
}
return {
sessionKey: trimmed,
agentId: DEFAULT_AGENT_ID,
sessionId: normalizeSessionSuffix(trimmed),
isAgentSession: false,
};
}
export function normalizeAgentSessionKey(sessionKey: string): string {
return parseSessionKey(sessionKey).sessionKey;
}
export const normalizeModelId = normalizeAgentId;
export const buildModelSessionKey = buildAgentSessionKey;
export const normalizeModelSessionKey = normalizeAgentSessionKey;

View File

@@ -8,7 +8,7 @@ import blueLogo from '../../assets/images/login/blue_logo.png';
const MENU_MARKS: Record<string, typeof House> = { const MENU_MARKS: Record<string, typeof House> = {
'/home': House, '/home': House,
'/knowledge': Book, '/knowledge': Book,
'/agents': Cpu, '/models': Cpu,
'/skills': Puzzle, '/skills': Puzzle,
'/cron': Clock, '/cron': Clock,
'/scripts': Code, '/scripts': Code,

View File

@@ -15,15 +15,15 @@ import {
import { IPC_EVENTS } from '../../lib/constants'; import { IPC_EVENTS } from '../../lib/constants';
import { invokeIpc } from '../../lib/host-api'; import { invokeIpc } from '../../lib/host-api';
import { import {
agentsStore,
channelStore, channelStore,
chatStore, chatStore,
getCompletedTasks, getCompletedTasks,
getPendingTasks, getPendingTasks,
modelsStore,
taskStore, taskStore,
useAgentsStore,
useChannelStore, useChannelStore,
useChatStore, useChatStore,
useModelsStore,
useTaskStore, useTaskStore,
type StagedAttachment, type StagedAttachment,
} from '../../stores'; } from '../../stores';
@@ -149,7 +149,7 @@ function mapMessages(messages: RawMessage[], streamingMessage: RawMessage | null
} }
export default function HomePage() { export default function HomePage() {
const agentsState = useAgentsStore(); const modelsState = useModelsStore();
const chat = useChatStore(); const chat = useChatStore();
const taskState = useTaskStore(); const taskState = useTaskStore();
const channelState = useChannelStore(); const channelState = useChannelStore();
@@ -161,7 +161,7 @@ export default function HomePage() {
const [addChannelDialogOpen, setAddChannelDialogOpen] = useState(false); const [addChannelDialogOpen, setAddChannelDialogOpen] = useState(false);
useEffect(() => { useEffect(() => {
void agentsStore.init(); void modelsStore.init();
void chatStore.init(); void chatStore.init();
void taskStore.init(); void taskStore.init();
void channelStore.init(); void channelStore.init();
@@ -212,10 +212,10 @@ export default function HomePage() {
const latestTask = currentTaskSource[0]; const latestTask = currentTaskSource[0];
const visibleMessages = mapMessages(chat.messages, chat.streamingMessage); const visibleMessages = mapMessages(chat.messages, chat.streamingMessage);
const selectedAgentId = agentsState.agents.some((agent) => agent.id === chat.currentAgentId) const selectedModelId = modelsState.models.some((model) => model.id === chat.currentAgentId)
? chat.currentAgentId ? chat.currentAgentId
: agentsState.defaultAgentId; : modelsState.defaultAgentId;
const currentAgent = agentsState.agents.find((agent) => agent.id === selectedAgentId) || null; const currentModel = modelsState.models.find((model) => model.id === selectedModelId) || null;
async function handleSendMessage(): Promise<void> { async function handleSendMessage(): Promise<void> {
const sent = await chatStore.sendMessage(inputMessage, attachments); const sent = await chatStore.sendMessage(inputMessage, attachments);
@@ -288,7 +288,7 @@ export default function HomePage() {
loading={!chat.initialized} loading={!chat.initialized}
selectedConversationId={chat.currentSessionKey} selectedConversationId={chat.currentSessionKey}
onNewChat={() => { onNewChat={() => {
void chatStore.newSession(selectedAgentId || undefined); void chatStore.newSession(selectedModelId || undefined);
}} }}
onSelectConversation={(conversationId) => { onSelectConversation={(conversationId) => {
chatStore.switchSession(conversationId); chatStore.switchSession(conversationId);
@@ -316,23 +316,23 @@ export default function HomePage() {
<h2 className="text-base font-semibold text-[#171717] dark:text-gray-100"></h2> <h2 className="text-base font-semibold text-[#171717] dark:text-gray-100"></h2>
<div className="mt-1 text-xs text-[#99A0AE] dark:text-gray-500"> <div className="mt-1 text-xs text-[#99A0AE] dark:text-gray-500">
{chat.gatewayStatus === 'connected' ? '已连接' : chat.gatewayStatus === 'reconnecting' ? '重连中' : '未连接'} {chat.gatewayStatus === 'connected' ? '已连接' : chat.gatewayStatus === 'reconnecting' ? '重连中' : '未连接'}
{currentAgent ? ` · 当前代理${currentAgent.name}` : ''} {currentModel ? ` · 当前模型${currentModel.name}` : ''}
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<label className="flex items-center gap-2 text-xs text-[#525866] dark:text-gray-300"> <label className="flex items-center gap-2 text-xs text-[#525866] dark:text-gray-300">
<span></span> <span></span>
<select <select
className="rounded-full border border-[#E5E8EE] bg-white px-3 py-1.5 text-xs text-[#525866] outline-none transition-colors hover:border-[#2B7FFF] dark:border-[#2a2a2d] dark:bg-[#232327] dark:text-gray-300" className="rounded-full border border-[#E5E8EE] bg-white px-3 py-1.5 text-xs text-[#525866] outline-none transition-colors hover:border-[#2B7FFF] dark:border-[#2a2a2d] dark:bg-[#232327] dark:text-gray-300"
disabled={agentsState.loading || agentsState.agents.length === 0} disabled={modelsState.loading || modelsState.models.length === 0}
value={selectedAgentId} value={selectedModelId}
onChange={(event) => { onChange={(event) => {
chatStore.selectAgent(event.target.value); chatStore.selectAgent(event.target.value);
}} }}
> >
{agentsState.agents.map((agent) => ( {modelsState.models.map((model) => (
<option key={agent.id} value={agent.id}> <option key={model.id} value={model.id}>
{agent.name} {model.name}
</option> </option>
))} ))}
</select> </select>
@@ -341,7 +341,7 @@ export default function HomePage() {
type="button" type="button"
className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-xs text-[#525866] transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:text-gray-300" className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-xs text-[#525866] transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:text-gray-300"
onClick={() => { onClick={() => {
void agentsStore.load(); void modelsStore.load();
void chatStore.loadSessions(); void chatStore.loadSessions();
void chatStore.loadHistory(); void chatStore.loadHistory();
}} }}

View File

@@ -1,16 +1,16 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { agentsStore, useAgentsStore } from '../../../stores'; import { modelsStore, useModelsStore } from '../../../stores';
const CHIP_CLASS_NAME = [ const CHIP_CLASS_NAME = [
'rounded-full border px-2.5 py-1 text-[11px] leading-none', 'rounded-full border px-2.5 py-1 text-[11px] leading-none',
'border-[#E5E8EE] text-[#525866] dark:border-[#2a2a2d] dark:text-gray-300', 'border-[#E5E8EE] text-[#525866] dark:border-[#2a2a2d] dark:text-gray-300',
].join(' '); ].join(' ');
export default function AgentsSection() { export default function ModelsSection() {
const agentState = useAgentsStore(); const modelsState = useModelsStore();
useEffect(() => { useEffect(() => {
void agentsStore.init(); void modelsStore.init();
}, []); }, []);
return ( return (
@@ -18,43 +18,43 @@ export default function AgentsSection() {
<div className="flex items-end justify-between gap-4"> <div className="flex items-end justify-between gap-4">
<div> <div>
<h3 className="text-[18px] font-semibold leading-[24px] text-[#171717] dark:text-gray-100"> <h3 className="text-[18px] font-semibold leading-[24px] text-[#171717] dark:text-gray-100">
Agents Snapshot Models Snapshot
</h3> </h3>
<p className="mt-1 text-[13px] leading-[20px] text-[#99A0AE] dark:text-gray-500"> <p className="mt-1 text-[13px] leading-[20px] text-[#99A0AE] dark:text-gray-500">
`agents` `mainSessionKey` `/api/models` `mainSessionKey`
</p> </p>
</div> </div>
<button <button
type="button" type="button"
className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-[12px] text-[#525866] transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:text-gray-300" className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-[12px] text-[#525866] transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:text-gray-300"
onClick={() => { onClick={() => {
void agentsStore.load(); void modelsStore.load();
}} }}
> >
Agents Models
</button> </button>
</div> </div>
{agentState.error ? ( {modelsState.error ? (
<div className="rounded-[14px] border border-[#F1D4D4] bg-[#FFF5F5] px-4 py-3 text-sm text-[#A53A3A] dark:border-[#4b2a2a] dark:bg-[#2a1f1f] dark:text-[#f7b8b8]"> <div className="rounded-[14px] border border-[#F1D4D4] bg-[#FFF5F5] px-4 py-3 text-sm text-[#A53A3A] dark:border-[#4b2a2a] dark:bg-[#2a1f1f] dark:text-[#f7b8b8]">
{agentState.error} {modelsState.error}
</div> </div>
) : null} ) : null}
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
{agentState.agents.map((agent) => ( {modelsState.models.map((model) => (
<article <article
key={agent.id} key={model.id}
className="rounded-[16px] border border-[#E5E8EE] bg-[#FAFBFC] p-4 dark:border-[#2a2a2d] dark:bg-[#202024]" className="rounded-[16px] border border-[#E5E8EE] bg-[#FAFBFC] p-4 dark:border-[#2a2a2d] dark:bg-[#202024]"
> >
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<div className="truncate text-[15px] font-semibold text-[#171717] dark:text-gray-100"> <div className="truncate text-[15px] font-semibold text-[#171717] dark:text-gray-100">
{agent.name} {model.name}
</div> </div>
<div className="mt-1 text-[12px] text-[#99A0AE] dark:text-gray-500">{agent.id}</div> <div className="mt-1 text-[12px] text-[#99A0AE] dark:text-gray-500">{model.id}</div>
</div> </div>
{agent.isDefault ? ( {model.isDefault ? (
<span className="rounded-full bg-[#EFF6FF] px-2.5 py-1 text-[11px] font-medium text-[#2B7FFF] dark:bg-[#1d2633]"> <span className="rounded-full bg-[#EFF6FF] px-2.5 py-1 text-[11px] font-medium text-[#2B7FFF] dark:bg-[#1d2633]">
</span> </span>
@@ -62,20 +62,20 @@ export default function AgentsSection() {
</div> </div>
<div className="mt-4 flex flex-wrap gap-2"> <div className="mt-4 flex flex-wrap gap-2">
<span className={CHIP_CLASS_NAME}>Provider: {agent.providerAccountId || '--'}</span> <span className={CHIP_CLASS_NAME}>Provider: {model.providerAccountId || '--'}</span>
<span className={CHIP_CLASS_NAME}>Model: {agent.modelDisplay || '--'}</span> <span className={CHIP_CLASS_NAME}>Model: {model.modelDisplay || '--'}</span>
</div> </div>
<div className="mt-4 rounded-[12px] border border-dashed border-[#DCE5F1] bg-white px-3 py-2 text-[12px] leading-[18px] text-[#525866] dark:border-[#2a2a2d] dark:bg-[#17171a] dark:text-gray-300"> <div className="mt-4 rounded-[12px] border border-dashed border-[#DCE5F1] bg-white px-3 py-2 text-[12px] leading-[18px] text-[#525866] dark:border-[#2a2a2d] dark:bg-[#17171a] dark:text-gray-300">
<div className="font-medium text-[#171717] dark:text-gray-100">mainSessionKey</div> <div className="font-medium text-[#171717] dark:text-gray-100">mainSessionKey</div>
<div className="mt-1 break-all">{agent.mainSessionKey}</div> <div className="mt-1 break-all">{model.mainSessionKey}</div>
</div> </div>
</article> </article>
))} ))}
{!agentState.loading && agentState.agents.length === 0 ? ( {!modelsState.loading && modelsState.models.length === 0 ? (
<div className="rounded-[16px] border border-dashed border-[#DCE5F1] bg-[#FAFBFC] px-4 py-6 text-sm text-[#525866] dark:border-[#2a2a2d] dark:bg-[#202024] dark:text-gray-300"> <div className="rounded-[16px] border border-dashed border-[#DCE5F1] bg-[#FAFBFC] px-4 py-6 text-sm text-[#525866] dark:border-[#2a2a2d] dark:bg-[#202024] dark:text-gray-300">
agent provider agent snapshot provider model snapshot
</div> </div>
) : null} ) : null}
</div> </div>

View File

@@ -1,8 +1,8 @@
import AgentsSection from './components/AgentsSection'; import ModelsSection from './components/ModelsSection';
import ProvidersSection from './components/ProvidersSection'; import ProvidersSection from './components/ProvidersSection';
import UsageHistorySection from './components/UsageHistorySection'; import UsageHistorySection from './components/UsageHistorySection';
export default function AgentsPage() { export default function ModelsPage() {
return ( return (
<section className="h-full w-full min-h-0"> <section className="h-full w-full min-h-0">
<div className="flex h-full w-full min-h-0 flex-col rounded-[16px] bg-white p-[20px] dark:bg-[#1b1b1d]"> <div className="flex h-full w-full min-h-0 flex-col rounded-[16px] bg-white p-[20px] dark:bg-[#1b1b1d]">
@@ -18,7 +18,7 @@ export default function AgentsPage() {
</div> </div>
<div className="min-h-0 flex-1 space-y-12 overflow-y-auto pb-10 pr-2"> <div className="min-h-0 flex-1 space-y-12 overflow-y-auto pb-10 pr-2">
<AgentsSection /> <ModelsSection />
<ProvidersSection /> <ProvidersSection />
<UsageHistorySection /> <UsageHistorySection />
</div> </div>

View File

@@ -3,7 +3,7 @@ import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-
import MainLayout from '../components/layout/MainLayout'; import MainLayout from '../components/layout/MainLayout';
import HomePage from '../pages/Home'; import HomePage from '../pages/Home';
import LoginPage from '../pages/Login'; import LoginPage from '../pages/Login';
import AgentsPage from '../pages/agents'; import ModelsPage from '../pages/Models';
import SkillsPage from '../pages/Skills'; import SkillsPage from '../pages/Skills';
import CronPage from '../pages/Cron'; import CronPage from '../pages/Cron';
import ScriptsPage from '../pages/Scripts'; import ScriptsPage from '../pages/Scripts';
@@ -45,7 +45,8 @@ export function AppRouter() {
<Route element={<RequireAuth />}> <Route element={<RequireAuth />}>
<Route element={<MainLayout />}> <Route element={<MainLayout />}>
<Route path="/home" element={<HomePage />} /> <Route path="/home" element={<HomePage />} />
<Route path="/agents" element={<AgentsPage />} /> <Route path="/models" element={<ModelsPage />} />
<Route path="/agents" element={<Navigate to="/models" replace />} />
<Route path="/skills" element={<SkillsPage />} /> <Route path="/skills" element={<SkillsPage />} />
<Route path="/cron" element={<CronPage />} /> <Route path="/cron" element={<CronPage />} />
<Route path="/scripts" element={<ScriptsPage />} /> <Route path="/scripts" element={<ScriptsPage />} />

View File

@@ -1,6 +1,6 @@
export type AppPath = export type AppPath =
| '/home' | '/home'
| '/agents' | '/models'
| '/skills' | '/skills'
| '/cron' | '/cron'
| '/scripts' | '/scripts'
@@ -27,7 +27,7 @@ export const DEFAULT_PATH: WorkspacePath = '/home';
export const NAV_ITEMS: NavItem[] = [ export const NAV_ITEMS: NavItem[] = [
{ path: '/home', labelKey: 'sidebar.home' }, { path: '/home', labelKey: 'sidebar.home' },
{ path: '/knowledge', labelKey: 'sidebar.knowledge' }, { path: '/knowledge', labelKey: 'sidebar.knowledge' },
{ path: '/agents', labelKey: 'sidebar.models' }, { path: '/models', labelKey: 'sidebar.models' },
{ path: '/skills', labelKey: 'sidebar.skills' }, { path: '/skills', labelKey: 'sidebar.skills' },
{ path: '/cron', labelKey: 'sidebar.cron' }, { path: '/cron', labelKey: 'sidebar.cron' },
{ path: '/scripts', labelKey: 'sidebar.scripts' }, { path: '/scripts', labelKey: 'sidebar.scripts' },
@@ -37,12 +37,13 @@ export const NAV_ITEMS: NavItem[] = [
export function normalizeWorkspacePath(pathname: string): WorkspacePath { export function normalizeWorkspacePath(pathname: string): WorkspacePath {
switch (pathname) { switch (pathname) {
case '/knowledge': case '/knowledge':
case '/models':
case '/agents': case '/agents':
case '/skills': case '/skills':
case '/cron': case '/cron':
case '/scripts': case '/scripts':
case '/setting': case '/setting':
return pathname; return pathname === '/agents' ? '/models' : pathname;
case '/home': case '/home':
default: default:
return DEFAULT_PATH; return DEFAULT_PATH;

View File

@@ -7,13 +7,13 @@ import {
normalizeAgentId, normalizeAgentId,
normalizeAgentSessionKey, normalizeAgentSessionKey,
parseSessionKey, parseSessionKey,
} from '@runtime/lib/agents'; } from '@runtime/lib/models';
import type { ChatSession, RawMessage, ToolStatus } from '../shared/chat-model'; import type { ChatSession, RawMessage, ToolStatus } from '../shared/chat-model';
import { extractText, isToolOnlyMessage } from '../shared/chat-model'; import { extractText, isToolOnlyMessage } from '../shared/chat-model';
import { gatewayRpc, onGatewayEvent } from '../lib/gateway-client'; import { gatewayRpc, onGatewayEvent } from '../lib/gateway-client';
import { hostApiFetch } from '../lib/host-api'; import { hostApiFetch } from '../lib/host-api';
import type { GatewayEvent } from '../types/runtime'; import type { GatewayEvent } from '../types/runtime';
import { agentsStore } from './agents'; import { modelsStore } from './models';
const SESSION_LOAD_MIN_INTERVAL_MS = 1200; const SESSION_LOAD_MIN_INTERVAL_MS = 1200;
const HISTORY_LOAD_MIN_INTERVAL_MS = 800; const HISTORY_LOAD_MIN_INTERVAL_MS = 800;
@@ -101,19 +101,19 @@ function patchState(patch: Partial<ChatStoreState>): ChatStoreState {
function getAgentIdFromSessionKey(sessionKey: string): string { function getAgentIdFromSessionKey(sessionKey: string): string {
const parsed = parseSessionKey(normalizeAgentSessionKey(sessionKey)); const parsed = parseSessionKey(normalizeAgentSessionKey(sessionKey));
if (parsed.isAgentSession) return parsed.agentId; if (parsed.isAgentSession) return parsed.agentId;
return agentsStore.getState().defaultAgentId || FALLBACK_AGENT_ID; return modelsStore.getState().defaultAgentId || FALLBACK_AGENT_ID;
} }
function getDefaultAgentId(): string { function getDefaultAgentId(): string {
return agentsStore.getState().defaultAgentId || FALLBACK_AGENT_ID; return modelsStore.getState().defaultAgentId || FALLBACK_AGENT_ID;
} }
function getDefaultMainSessionKey(): string { function getDefaultMainSessionKey(): string {
return agentsStore.resolveMainSessionKey(getDefaultAgentId()) || FALLBACK_MAIN_SESSION_KEY; return modelsStore.resolveMainSessionKey(getDefaultAgentId()) || FALLBACK_MAIN_SESSION_KEY;
} }
function resolveMainSessionKeyForAgent(agentId: string | null | undefined): string { function resolveMainSessionKeyForAgent(agentId: string | null | undefined): string {
return agentsStore.resolveMainSessionKey(agentId || getDefaultAgentId()) || getDefaultMainSessionKey(); return modelsStore.resolveMainSessionKey(agentId || getDefaultAgentId()) || getDefaultMainSessionKey();
} }
function buildNewSessionKey(agentId: string | null | undefined): string { function buildNewSessionKey(agentId: string | null | undefined): string {
@@ -208,7 +208,7 @@ async function resolveDefaultAccountId(): Promise<string | null> {
} }
async function resolveProviderAccountIdForAgent(agentId: string | null | undefined): Promise<string | null> { async function resolveProviderAccountIdForAgent(agentId: string | null | undefined): Promise<string | null> {
const mappedAccountId = agentsStore.resolveProviderAccountId(agentId); const mappedAccountId = modelsStore.resolveProviderAccountId(agentId);
if (mappedAccountId) { if (mappedAccountId) {
return mappedAccountId; return mappedAccountId;
} }
@@ -283,7 +283,7 @@ async function subscribeToGateway(): Promise<void> {
} }
async function loadSessions(): Promise<void> { async function loadSessions(): Promise<void> {
await agentsStore.init(); await modelsStore.init();
const now = Date.now(); const now = Date.now();
if (loadSessionsInFlight) { if (loadSessionsInFlight) {
await loadSessionsInFlight; await loadSessionsInFlight;
@@ -850,7 +850,7 @@ function clearError(): void {
} }
async function initChatStore(): Promise<void> { async function initChatStore(): Promise<void> {
await agentsStore.init(); await modelsStore.init();
await subscribeToGateway(); await subscribeToGateway();
await loadSessions(); await loadSessions();
} }

View File

@@ -1,5 +1,5 @@
export * from './settings'; export * from './settings';
export * from './agents'; export * from './models';
export * from './chat'; export * from './chat';
export * from './task'; export * from './task';
export * from './channel'; export * from './channel';

View File

@@ -5,15 +5,15 @@ import {
buildMainSessionKey, buildMainSessionKey,
normalizeAgentId, normalizeAgentId,
type AgentSummary, type AgentSummary,
type AgentsSnapshot, type ModelsSnapshot,
} from '@runtime/lib/agents'; } from '@runtime/lib/models';
import { hostApiFetch } from '../lib/host-api'; import { hostApiFetch } from '../lib/host-api';
export interface AgentsStoreState { export interface ModelsStoreState {
initialized: boolean; initialized: boolean;
loading: boolean; loading: boolean;
error: string | null; error: string | null;
agents: AgentSummary[]; models: AgentSummary[];
defaultAgentId: string; defaultAgentId: string;
defaultProviderAccountId: string | null; defaultProviderAccountId: string | null;
defaultModelRef: string | null; defaultModelRef: string | null;
@@ -22,12 +22,12 @@ export interface AgentsStoreState {
const listeners = new Set<() => void>(); const listeners = new Set<() => void>();
let loadAgentsInFlight: Promise<void> | null = null; let loadModelsInFlight: Promise<void> | null = null;
let state: AgentsStoreState = { let state: ModelsStoreState = {
initialized: false, initialized: false,
loading: false, loading: false,
error: null, error: null,
agents: [], models: [],
defaultAgentId: DEFAULT_AGENT_ID, defaultAgentId: DEFAULT_AGENT_ID,
defaultProviderAccountId: null, defaultProviderAccountId: null,
defaultModelRef: null, defaultModelRef: null,
@@ -40,49 +40,51 @@ function emit(): void {
} }
} }
function patchState(patch: Partial<AgentsStoreState>): AgentsStoreState { function patchState(patch: Partial<ModelsStoreState>): ModelsStoreState {
state = { ...state, ...patch }; state = { ...state, ...patch };
emit(); emit();
return state; return state;
} }
function sanitizeAgent(agent: AgentSummary): AgentSummary { function sanitizeModel(model: AgentSummary): AgentSummary {
const normalizedId = normalizeAgentId(agent.id); const normalizedId = normalizeAgentId(model.id);
const normalizedMainSessionKey = agent.mainSessionKey || buildMainSessionKey(normalizedId); const normalizedMainSessionKey = model.mainSessionKey || buildMainSessionKey(normalizedId);
return { return {
id: normalizedId, id: normalizedId,
name: agent.name || normalizedId, name: model.name || normalizedId,
isDefault: Boolean(agent.isDefault), isDefault: Boolean(model.isDefault),
providerAccountId: agent.providerAccountId ?? null, providerAccountId: model.providerAccountId ?? null,
modelRef: agent.modelRef ?? null, modelRef: model.modelRef ?? null,
modelDisplay: agent.modelDisplay || agent.modelRef || agent.name || normalizedId, modelDisplay: model.modelDisplay || model.modelRef || model.name || normalizedId,
mainSessionKey: normalizedMainSessionKey, mainSessionKey: normalizedMainSessionKey,
vendorId: agent.vendorId ?? null, vendorId: model.vendorId ?? null,
source: agent.source, source: model.source,
}; };
} }
async function loadAgents(): Promise<void> { async function loadModels(): Promise<void> {
if (loadAgentsInFlight) { if (loadModelsInFlight) {
await loadAgentsInFlight; await loadModelsInFlight;
return; return;
} }
loadAgentsInFlight = (async () => { loadModelsInFlight = (async () => {
patchState({ loading: true, error: null }); patchState({ loading: true, error: null });
try { try {
const snapshot = await hostApiFetch<AgentsSnapshot & { success?: boolean }>('/api/agents'); const snapshot = await hostApiFetch<ModelsSnapshot & { success?: boolean }>('/api/models');
const agents = Array.isArray(snapshot?.agents) const models = Array.isArray(snapshot?.models)
? snapshot.agents.map((agent) => sanitizeAgent(agent)) ? snapshot.models.map((model) => sanitizeModel(model))
: Array.isArray(snapshot?.agents)
? snapshot.agents.map((model) => sanitizeModel(model))
: []; : [];
patchState({ patchState({
initialized: true, initialized: true,
loading: false, loading: false,
error: null, error: null,
agents, models,
defaultAgentId: snapshot?.defaultAgentId ? normalizeAgentId(snapshot.defaultAgentId) : DEFAULT_AGENT_ID, defaultAgentId: snapshot?.defaultAgentId ? normalizeAgentId(snapshot.defaultAgentId) : DEFAULT_AGENT_ID,
defaultProviderAccountId: snapshot?.defaultProviderAccountId ?? null, defaultProviderAccountId: snapshot?.defaultProviderAccountId ?? null,
defaultModelRef: snapshot?.defaultModelRef ?? null, defaultModelRef: snapshot?.defaultModelRef ?? null,
@@ -98,9 +100,9 @@ async function loadAgents(): Promise<void> {
})(); })();
try { try {
await loadAgentsInFlight; await loadModelsInFlight;
} finally { } finally {
loadAgentsInFlight = null; loadModelsInFlight = null;
} }
} }
@@ -109,36 +111,36 @@ function subscribe(listener: () => void): () => void {
return () => listeners.delete(listener); return () => listeners.delete(listener);
} }
function getSnapshot(): AgentsStoreState { function getSnapshot(): ModelsStoreState {
return state; return state;
} }
function getAgentById(agentId: string | null | undefined): AgentSummary | undefined { function getModelById(modelId: string | null | undefined): AgentSummary | undefined {
const normalizedId = normalizeAgentId(agentId); const normalizedId = normalizeAgentId(modelId);
return state.agents.find((agent) => agent.id === normalizedId); return state.models.find((model) => model.id === normalizedId);
} }
function resolveMainSessionKey(agentId: string | null | undefined): string { function resolveMainSessionKey(agentId: string | null | undefined): string {
const normalizedId = normalizeAgentId(agentId || state.defaultAgentId); const normalizedId = normalizeAgentId(agentId || state.defaultAgentId);
return getAgentById(normalizedId)?.mainSessionKey || buildMainSessionKey(normalizedId, state.mainSessionSuffix); return getModelById(normalizedId)?.mainSessionKey || buildMainSessionKey(normalizedId, state.mainSessionSuffix);
} }
function resolveProviderAccountId(agentId: string | null | undefined): string | null { function resolveProviderAccountId(agentId: string | null | undefined): string | null {
const normalizedId = normalizeAgentId(agentId || state.defaultAgentId); const normalizedId = normalizeAgentId(agentId || state.defaultAgentId);
return getAgentById(normalizedId)?.providerAccountId ?? state.defaultProviderAccountId; return getModelById(normalizedId)?.providerAccountId ?? state.defaultProviderAccountId;
} }
export const agentsStore = { export const modelsStore = {
subscribe, subscribe,
getSnapshot, getSnapshot,
getState: () => state, getState: () => state,
init: loadAgents, init: loadModels,
load: loadAgents, load: loadModels,
getAgentById, getModelById,
resolveMainSessionKey, resolveMainSessionKey,
resolveProviderAccountId, resolveProviderAccountId,
}; };
export function useAgentsStore(): AgentsStoreState { export function useModelsStore(): ModelsStoreState {
return useSyncExternalStore(agentsStore.subscribe, agentsStore.getSnapshot, agentsStore.getSnapshot); return useSyncExternalStore(modelsStore.subscribe, modelsStore.getSnapshot, modelsStore.getSnapshot);
} }