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:
@@ -4,9 +4,9 @@ import { providerApiService } from '@electron/service/provider-api-service';
|
||||
import type { HostApiContext } from './context';
|
||||
import type { HostApiRequest } from './route-utils';
|
||||
import { normalizeRequest } from './route-utils';
|
||||
import { handleAgentRoutes } from './routes/agents';
|
||||
import { handleFileRoutes } from './routes/files';
|
||||
import { handleGatewayRoutes } from './routes/gateway';
|
||||
import { handleModelRoutes } from './routes/models';
|
||||
import { handleProviderRoutes } from './routes/providers';
|
||||
import { handleSessionRoutes } from './routes/sessions';
|
||||
|
||||
@@ -17,7 +17,7 @@ type RouteHandler = (
|
||||
|
||||
const routeHandlers: RouteHandler[] = [
|
||||
handleProviderRoutes,
|
||||
handleAgentRoutes,
|
||||
handleModelRoutes,
|
||||
handleGatewayRoutes,
|
||||
handleFileRoutes,
|
||||
handleSessionRoutes,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
buildMainSessionKey,
|
||||
normalizeAgentId,
|
||||
type AgentSummary,
|
||||
} from '@runtime/lib/agents';
|
||||
} from '@runtime/lib/models';
|
||||
import type { HostApiContext } from '../context';
|
||||
import type { NormalizedHostApiRequest } 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;
|
||||
}
|
||||
|
||||
function buildMainAgent(defaultAccount: ProviderAccount | null): AgentSummary {
|
||||
function buildMainModel(defaultAccount: ProviderAccount | null): AgentSummary {
|
||||
return {
|
||||
id: DEFAULT_AGENT_ID,
|
||||
name: 'Main Agent',
|
||||
name: 'Main Model',
|
||||
isDefault: true,
|
||||
providerAccountId: defaultAccount?.id ?? 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 summaries: AgentSummary[] = [];
|
||||
|
||||
@@ -57,12 +57,12 @@ function buildProviderBackedAgents(accounts: ProviderAccount[]): AgentSummary[]
|
||||
return summaries;
|
||||
}
|
||||
|
||||
export async function handleAgentRoutes(
|
||||
export async function handleModelRoutes(
|
||||
request: NormalizedHostApiRequest,
|
||||
ctx: HostApiContext,
|
||||
) {
|
||||
const { pathname, method } = request;
|
||||
if (pathname !== '/api/agents' || method !== 'GET') {
|
||||
if ((pathname !== '/api/models' && pathname !== '/api/agents') || method !== 'GET') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -72,14 +72,15 @@ export async function handleAgentRoutes(
|
||||
const defaultAccountId = ctx.providerApiService.getDefault().accountId;
|
||||
const defaultAccount = accounts.find((account) => account.id === defaultAccountId) ?? accounts[0] ?? null;
|
||||
|
||||
const agents = [
|
||||
buildMainAgent(defaultAccount),
|
||||
...buildProviderBackedAgents(accounts),
|
||||
const models = [
|
||||
buildMainModel(defaultAccount),
|
||||
...buildProviderBackedModels(accounts),
|
||||
];
|
||||
|
||||
return ok({
|
||||
success: true,
|
||||
agents,
|
||||
models,
|
||||
agents: models,
|
||||
defaultAgentId: DEFAULT_AGENT_ID,
|
||||
defaultProviderAccountId: defaultAccount?.id ?? null,
|
||||
defaultModelRef: defaultAccount?.model ?? null,
|
||||
@@ -1,6 +1,9 @@
|
||||
import { sessionStore } from '@electron/gateway/session-store';
|
||||
import { getTranscriptFilePath } from '@electron/utils/token-usage-writer';
|
||||
import { buildAgentSessionKey, normalizeAgentSessionKey, parseSessionKey } from '@runtime/lib/agents';
|
||||
import {
|
||||
getTranscriptFilePath,
|
||||
getTranscriptPathCandidates,
|
||||
} from '@electron/utils/token-usage-writer';
|
||||
import { buildAgentSessionKey, normalizeAgentSessionKey, parseSessionKey } from '@runtime/lib/models';
|
||||
import type { HostApiContext } from '../context';
|
||||
import type { NormalizedHostApiRequest } from '../route-utils';
|
||||
import { fail, ok, parseJsonBody } from '../route-utils';
|
||||
@@ -41,20 +44,45 @@ export async function handleSessionRoutes(
|
||||
|
||||
try {
|
||||
const fsP = await import('node:fs/promises');
|
||||
const transcriptPath = getTranscriptFilePath(identity.sessionKey);
|
||||
let raw: string;
|
||||
let transcriptPath = getTranscriptFilePath(identity.sessionKey);
|
||||
let raw: string | null = null;
|
||||
let lastError: unknown = null;
|
||||
|
||||
try {
|
||||
raw = await fsP.readFile(transcriptPath, 'utf8');
|
||||
} catch (error: any) {
|
||||
const requestedSessionKey = request.url.searchParams.get('sessionKey')?.trim() || '';
|
||||
if (error?.code === 'ENOENT' && requestedSessionKey && requestedSessionKey !== identity.sessionKey) {
|
||||
raw = await fsP.readFile(getTranscriptFilePath(requestedSessionKey), 'utf8');
|
||||
} else {
|
||||
throw error;
|
||||
for (const candidatePath of getTranscriptPathCandidates(identity.sessionKey)) {
|
||||
try {
|
||||
raw = await fsP.readFile(candidatePath, 'utf8');
|
||||
transcriptPath = candidatePath;
|
||||
break;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
if ((error as { code?: string } | null)?.code !== 'ENOENT') {
|
||||
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 messages = lines.flatMap((line) => {
|
||||
try {
|
||||
@@ -90,19 +118,18 @@ export async function handleSessionRoutes(
|
||||
|
||||
sessionStore.deleteSession(sessionKey);
|
||||
|
||||
const transcriptPath = getTranscriptFilePath(sessionKey);
|
||||
try {
|
||||
const fsP = await import('node:fs/promises');
|
||||
await fsP.rename(transcriptPath, transcriptPath.replace(/\.jsonl$/, '.deleted.jsonl'));
|
||||
} catch {
|
||||
if (rawSessionKey && rawSessionKey !== sessionKey) {
|
||||
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.
|
||||
}
|
||||
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'));
|
||||
break;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user