style: clear the lint and prettier findings so the gates pass
- prettier formatting across routes, components and tests (no behavior change) - unused imports and ternary-as-statement replaced with explicit branches - eslint: scripts/**/*.js are CommonJS, allow require() there; prettier ignores pnpm-lock.yaml
This commit is contained in:
1 parent
42466c274b
commit
8ad3eef53d
21 files changed
+365
-333
No files matched your search
@@ -10,3 +10,4 @@ openspec/
|
|||||||
.agent/
|
.agent/
|
||||||
.claude/
|
.claude/
|
||||||
.codex
|
.codex
|
||||||
|
pnpm-lock.yaml
|
||||||
@@ -50,4 +50,11 @@ export default defineConfig([
|
|||||||
'prefer-const': 'off',
|
'prefer-const': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Node maintenance scripts are CommonJS; require() is the native API there.
|
||||||
|
files: ['scripts/**/*.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-require-imports': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
@@ -417,7 +417,11 @@ export function CloudAccountList() {
|
|||||||
const setSelectionState = (id: string, selected: boolean) =>
|
const setSelectionState = (id: string, selected: boolean) =>
|
||||||
setSelectedIds((prev) => {
|
setSelectedIds((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
selected ? next.add(id) : next.delete(id);
|
if (selected) {
|
||||||
|
next.add(id);
|
||||||
|
} else {
|
||||||
|
next.delete(id);
|
||||||
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
const toggleSelectAllAccounts = () =>
|
const toggleSelectAllAccounts = () =>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { calculateCost } from '@/constants/pricing';
|
|
||||||
|
|
||||||
export interface ModelCostEntry {
|
export interface ModelCostEntry {
|
||||||
model: string;
|
model: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { Loader2, RefreshCw, RotateCcw } from 'lucide-react';
|
import { Loader2, RefreshCw, RotateCcw } from 'lucide-react';
|
||||||
import { MODEL_PRICING } from '@/constants/pricing';
|
import { MODEL_PRICING } from '@/constants/pricing';
|
||||||
|
|
||||||
@@ -35,8 +34,16 @@ export const ModelPricingDialog: React.FC<ModelPricingDialogProps> = ({
|
|||||||
Record<string, { input: number; output: number; source: string }>
|
Record<string, { input: number; output: number; source: string }>
|
||||||
>({});
|
>({});
|
||||||
const [isFetching, setIsFetching] = useState(false);
|
const [isFetching, setIsFetching] = useState(false);
|
||||||
|
const [pricingSource, setPricingSource] = useState<{
|
||||||
|
open: boolean;
|
||||||
|
customPricing: typeof customPricing;
|
||||||
|
}>({ open, customPricing });
|
||||||
|
|
||||||
useEffect(() => {
|
// Re-seed the editable copy when the dialog opens or the source pricing changes.
|
||||||
|
// Adjusting state during render replaces the previous synchronous setState inside
|
||||||
|
// an effect (https://react.dev/learn/you-might-not-need-an-effect).
|
||||||
|
if (pricingSource.open !== open || pricingSource.customPricing !== customPricing) {
|
||||||
|
setPricingSource({ open, customPricing });
|
||||||
if (open) {
|
if (open) {
|
||||||
// Initialize local state with custom pricing or empty if default
|
// Initialize local state with custom pricing or empty if default
|
||||||
const initial: Record<string, { input: number; output: number; source: string }> = {};
|
const initial: Record<string, { input: number; output: number; source: string }> = {};
|
||||||
@@ -49,7 +56,7 @@ export const ModelPricingDialog: React.FC<ModelPricingDialogProps> = ({
|
|||||||
});
|
});
|
||||||
setLocalPricing(initial);
|
setLocalPricing(initial);
|
||||||
}
|
}
|
||||||
}, [open, customPricing]);
|
}
|
||||||
|
|
||||||
const handlePriceChange = (model: string, type: 'input' | 'output', value: string) => {
|
const handlePriceChange = (model: string, type: 'input' | 'output', value: string) => {
|
||||||
const numValue = parseFloat(value);
|
const numValue = parseFloat(value);
|
||||||
|
|||||||
@@ -58,9 +58,8 @@ export const MonitorTab: React.FC = () => {
|
|||||||
tokensPrompt: log.metadata?.tokensPrompt as number | undefined,
|
tokensPrompt: log.metadata?.tokensPrompt as number | undefined,
|
||||||
tokensCompletion: log.metadata?.tokensCompletion as number | undefined,
|
tokensCompletion: log.metadata?.tokensCompletion as number | undefined,
|
||||||
tokensTotal:
|
tokensTotal:
|
||||||
(((log.metadata?.tokensPrompt as number) || 0) +
|
((log.metadata?.tokensPrompt as number) || 0) +
|
||||||
((log.metadata?.tokensCompletion as number) || 0)) ||
|
((log.metadata?.tokensCompletion as number) || 0) || undefined,
|
||||||
undefined,
|
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
const stats = statsData?.success ? statsData.data : undefined;
|
const stats = statsData?.success ? statsData.data : undefined;
|
||||||
|
|||||||
@@ -26,7 +26,16 @@ interface TrafficLogTableProps {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortKey = 'timestamp' | 'direction' | 'endpoint' | 'model' | 'method' | 'status' | 'tokens' | 'cost' | 'duration';
|
type SortKey =
|
||||||
|
| 'timestamp'
|
||||||
|
| 'direction'
|
||||||
|
| 'endpoint'
|
||||||
|
| 'model'
|
||||||
|
| 'method'
|
||||||
|
| 'status'
|
||||||
|
| 'tokens'
|
||||||
|
| 'cost'
|
||||||
|
| 'duration';
|
||||||
type SortDir = 'asc' | 'desc';
|
type SortDir = 'asc' | 'desc';
|
||||||
|
|
||||||
function SortIcon({
|
function SortIcon({
|
||||||
@@ -79,13 +88,10 @@ function getBodyPreview(body: unknown): string | null {
|
|||||||
|
|
||||||
// Request body: extract first user message
|
// Request body: extract first user message
|
||||||
if (Array.isArray(obj.messages)) {
|
if (Array.isArray(obj.messages)) {
|
||||||
const userMsg = obj.messages.find(
|
const userMsg = obj.messages.find((m: Record<string, unknown>) => m.role === 'user');
|
||||||
(m: Record<string, unknown>) => m.role === 'user',
|
|
||||||
);
|
|
||||||
if (userMsg) {
|
if (userMsg) {
|
||||||
const content = typeof userMsg.content === 'string'
|
const content =
|
||||||
? userMsg.content
|
typeof userMsg.content === 'string' ? userMsg.content : JSON.stringify(userMsg.content);
|
||||||
: JSON.stringify(userMsg.content);
|
|
||||||
return content.length > 120 ? content.slice(0, 120) + '…' : content;
|
return content.length > 120 ? content.slice(0, 120) + '…' : content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,9 +101,7 @@ function getBodyPreview(body: unknown): string | null {
|
|||||||
const firstChoice = obj.choices[0] as Record<string, unknown> | undefined;
|
const firstChoice = obj.choices[0] as Record<string, unknown> | undefined;
|
||||||
const message = firstChoice?.message as Record<string, unknown> | undefined;
|
const message = firstChoice?.message as Record<string, unknown> | undefined;
|
||||||
if (message?.content && typeof message.content === 'string') {
|
if (message?.content && typeof message.content === 'string') {
|
||||||
return message.content.length > 120
|
return message.content.length > 120 ? message.content.slice(0, 120) + '…' : message.content;
|
||||||
? message.content.slice(0, 120) + '…'
|
|
||||||
: message.content;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,18 +258,16 @@ export const TrafficLogTable: React.FC<TrafficLogTableProps> = ({ logs, isLoadin
|
|||||||
<React.Fragment key={logKey}>
|
<React.Fragment key={logKey}>
|
||||||
<tr
|
<tr
|
||||||
className={`cursor-pointer border-b border-white/[0.04] transition-colors last:border-0 ${
|
className={`cursor-pointer border-b border-white/[0.04] transition-colors last:border-0 ${
|
||||||
isExpanded
|
isExpanded ? 'bg-white/[0.03]' : 'hover:bg-white/[0.02]'
|
||||||
? 'bg-white/[0.03]'
|
|
||||||
: 'hover:bg-white/[0.02]'
|
|
||||||
}`}
|
}`}
|
||||||
onClick={() => toggleExpand(logKey)}
|
onClick={() => toggleExpand(logKey)}
|
||||||
>
|
>
|
||||||
{/* Expand chevron */}
|
{/* Expand chevron */}
|
||||||
<td className="py-2 pr-1">
|
<td className="py-2 pr-1">
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
|
<ChevronDown className="text-muted-foreground h-3.5 w-3.5" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="h-3.5 w-3.5 text-muted-foreground/50" />
|
<ChevronRight className="text-muted-foreground/50 h-3.5 w-3.5" />
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-muted-foreground py-2 pr-4 font-mono text-xs">
|
<td className="text-muted-foreground py-2 pr-4 font-mono text-xs">
|
||||||
@@ -300,11 +302,9 @@ export const TrafficLogTable: React.FC<TrafficLogTableProps> = ({ logs, isLoadin
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 pr-4 text-xs">
|
<td className="py-2 pr-4 text-xs">
|
||||||
{log.tokensTotal ? (
|
{log.tokensTotal
|
||||||
`${formatTokens(log.tokensPrompt)} / ${formatTokens(log.tokensCompletion)}`
|
? `${formatTokens(log.tokensPrompt)} / ${formatTokens(log.tokensCompletion)}`
|
||||||
) : (
|
: '—'}
|
||||||
'—'
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 pr-4 text-xs">
|
<td className="py-2 pr-4 text-xs">
|
||||||
{log.durationMs != null ? `${log.durationMs}ms` : '—'}
|
{log.durationMs != null ? `${log.durationMs}ms` : '—'}
|
||||||
@@ -323,7 +323,7 @@ export const TrafficLogTable: React.FC<TrafficLogTableProps> = ({ logs, isLoadin
|
|||||||
<div className="border-t border-white/[0.06] bg-white/[0.01] px-6 py-4">
|
<div className="border-t border-white/[0.06] bg-white/[0.01] px-6 py-4">
|
||||||
{/* Preview line */}
|
{/* Preview line */}
|
||||||
{preview && (
|
{preview && (
|
||||||
<div className="mb-3 rounded-lg bg-white/[0.03] px-3 py-2 text-xs text-muted-foreground italic break-words">
|
<div className="text-muted-foreground mb-3 rounded-lg bg-white/[0.03] px-3 py-2 text-xs break-words italic">
|
||||||
"{preview}"
|
"{preview}"
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -339,18 +339,32 @@ export const TrafficLogTable: React.FC<TrafficLogTableProps> = ({ logs, isLoadin
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{log.tokensPrompt != null && (
|
{log.tokensPrompt != null && (
|
||||||
<Badge variant="outline" className="border-blue-500/20 text-blue-400 text-[10px]">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-blue-500/20 text-[10px] text-blue-400"
|
||||||
|
>
|
||||||
Input: {log.tokensPrompt.toLocaleString()} tokens
|
Input: {log.tokensPrompt.toLocaleString()} tokens
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{log.tokensCompletion != null && (
|
{log.tokensCompletion != null && (
|
||||||
<Badge variant="outline" className="border-emerald-500/20 text-emerald-400 text-[10px]">
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="border-emerald-500/20 text-[10px] text-emerald-400"
|
||||||
|
>
|
||||||
Output: {log.tokensCompletion.toLocaleString()} tokens
|
Output: {log.tokensCompletion.toLocaleString()} tokens
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{log.tokensTotal != null && log.model && (
|
{log.tokensTotal != null && log.model && (
|
||||||
<Badge variant="outline" className="border-amber-500/20 text-amber-400 text-[10px]">
|
<Badge
|
||||||
Cost: ${calculateCost(log.model, log.tokensPrompt || 0, log.tokensCompletion || 0).toFixed(6)}
|
variant="outline"
|
||||||
|
className="border-amber-500/20 text-[10px] text-amber-400"
|
||||||
|
>
|
||||||
|
Cost: $
|
||||||
|
{calculateCost(
|
||||||
|
log.model,
|
||||||
|
log.tokensPrompt || 0,
|
||||||
|
log.tokensCompletion || 0,
|
||||||
|
).toFixed(6)}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{log.durationMs != null && (
|
{log.durationMs != null && (
|
||||||
@@ -363,11 +377,15 @@ export const TrafficLogTable: React.FC<TrafficLogTableProps> = ({ logs, isLoadin
|
|||||||
{/* Body content */}
|
{/* Body content */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-muted-foreground text-[10px] font-medium uppercase tracking-wider">
|
<span className="text-muted-foreground text-[10px] font-medium tracking-wider uppercase">
|
||||||
{log.direction === 'inbound' ? 'Request Body' : log.direction === 'outbound' ? 'Response Body' : 'Payload'}
|
{log.direction === 'inbound'
|
||||||
|
? 'Request Body'
|
||||||
|
: log.direction === 'outbound'
|
||||||
|
? 'Response Body'
|
||||||
|
: 'Payload'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<pre className="custom-scrollbar max-h-[400px] overflow-auto rounded-lg bg-black/30 p-3 font-mono text-[11px] leading-relaxed text-foreground/80 whitespace-pre-wrap break-all">
|
<pre className="custom-scrollbar text-foreground/80 max-h-[400px] overflow-auto rounded-lg bg-black/30 p-3 font-mono text-[11px] leading-relaxed break-all whitespace-pre-wrap">
|
||||||
{formatBody(log.body)}
|
{formatBody(log.body)}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import { getServiceOrThrow, getServiceOptional } from './service-registry';
|
import { getServiceOrThrow, getServiceOptional } from './service-registry';
|
||||||
import type {
|
import type { IdeConfigResult } from '../../server/modules/proxy/proxy-ide-config.service';
|
||||||
IdeConfigResult,
|
|
||||||
SupportedIde,
|
|
||||||
} from '../../server/modules/proxy/proxy-ide-config.service';
|
|
||||||
import { TrafficLogsRepo } from '../database/proxyMetricsHandler';
|
import { TrafficLogsRepo } from '../database/proxyMetricsHandler';
|
||||||
import { CloudAccountRepo } from '../database/cloudHandler';
|
import { CloudAccountRepo } from '../database/cloudHandler';
|
||||||
import { ConfigManager } from '../config/manager';
|
import { ConfigManager } from '../config/manager';
|
||||||
@@ -257,9 +254,7 @@ export async function getRecentRequests(): Promise<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function replayRequest(
|
export async function replayRequest(requestId: string): Promise<{
|
||||||
requestId: string,
|
|
||||||
): Promise<{
|
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: { original: unknown; newResponse: unknown };
|
data?: { original: unknown; newResponse: unknown };
|
||||||
error?: string;
|
error?: string;
|
||||||
|
|||||||
+276
-274
@@ -406,7 +406,7 @@ function UsagePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs defaultValue="analytics" className="flex-1 flex flex-col">
|
<Tabs defaultValue="analytics" className="flex flex-1 flex-col">
|
||||||
<div className="px-6 pt-5">
|
<div className="px-6 pt-5">
|
||||||
<TabsList className="grid w-full max-w-md grid-cols-2">
|
<TabsList className="grid w-full max-w-md grid-cols-2">
|
||||||
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
||||||
@@ -415,299 +415,301 @@ function UsagePage() {
|
|||||||
</div>
|
</div>
|
||||||
<TabsContent value="analytics" className="flex-1 space-y-4 px-6 py-5">
|
<TabsContent value="analytics" className="flex-1 space-y-4 px-6 py-5">
|
||||||
{hasQuotaWarning && (
|
{hasQuotaWarning && (
|
||||||
<Alert
|
<Alert
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
className="border-amber-500/50 bg-amber-500/10 text-amber-500"
|
className="border-amber-500/50 bg-amber-500/10 text-amber-500"
|
||||||
>
|
>
|
||||||
<AlertTriangle className="h-4 w-4" />
|
<AlertTriangle className="h-4 w-4" />
|
||||||
<AlertTitle>Quota Warning</AlertTitle>
|
<AlertTitle>Quota Warning</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
One or more models have exceeded 80% of their allocated usage limit.
|
One or more models have exceeded 80% of their allocated usage limit.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* ─── Stat cards ─── */}
|
{/* ─── Stat cards ─── */}
|
||||||
<TooltipProvider delayDuration={300}>
|
<TooltipProvider delayDuration={300}>
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-5">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-5">
|
||||||
<StatCard
|
|
||||||
label="Total Tokens"
|
|
||||||
value={formatNumber(totals.totalTokens)}
|
|
||||||
icon={Zap}
|
|
||||||
isLoading={dailyLoading}
|
|
||||||
accent="amber"
|
|
||||||
trend={
|
|
||||||
prevDailyData ? calcTrend(totals.totalTokens, prevTotals.totalTokens) : undefined
|
|
||||||
}
|
|
||||||
sparkline={sparklineTotal}
|
|
||||||
/>
|
|
||||||
<div className="group relative">
|
|
||||||
<StatCard
|
<StatCard
|
||||||
label={
|
label="Total Tokens"
|
||||||
<div className="flex cursor-help items-center gap-1.5">
|
value={formatNumber(totals.totalTokens)}
|
||||||
Input Tokens
|
icon={Zap}
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Info className="text-muted-foreground/70 h-3.5 w-3.5" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Tokens sent to the model (prompt, context, files).</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
value={formatNumber(totals.promptTokens)}
|
|
||||||
icon={MessageSquare}
|
|
||||||
isLoading={dailyLoading}
|
isLoading={dailyLoading}
|
||||||
accent="blue"
|
accent="amber"
|
||||||
trend={
|
trend={
|
||||||
prevDailyData
|
prevDailyData ? calcTrend(totals.totalTokens, prevTotals.totalTokens) : undefined
|
||||||
? calcTrend(totals.promptTokens, prevTotals.promptTokens)
|
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
sparkline={sparklinePrompt}
|
sparkline={sparklineTotal}
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="group relative">
|
||||||
<div className="group relative">
|
<StatCard
|
||||||
<StatCard
|
label={
|
||||||
label={
|
|
||||||
<div className="flex cursor-help items-center gap-1.5">
|
|
||||||
Output Tokens
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Info className="text-muted-foreground/70 h-3.5 w-3.5" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>Tokens generated by the model in response.</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
value={formatNumber(totals.completionTokens)}
|
|
||||||
icon={Activity}
|
|
||||||
isLoading={dailyLoading}
|
|
||||||
accent="green"
|
|
||||||
trend={
|
|
||||||
prevDailyData
|
|
||||||
? calcTrend(totals.completionTokens, prevTotals.completionTokens)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
sparkline={sparklineCompletion}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<StatCard
|
|
||||||
label="Total Requests"
|
|
||||||
value={totals.requests.toLocaleString()}
|
|
||||||
icon={Layers}
|
|
||||||
isLoading={dailyLoading}
|
|
||||||
accent="slate"
|
|
||||||
trend={prevDailyData ? calcTrend(totals.requests, prevTotals.requests) : undefined}
|
|
||||||
sparkline={sparklineRequests}
|
|
||||||
/>
|
|
||||||
<div className="group relative">
|
|
||||||
<StatCard
|
|
||||||
label={
|
|
||||||
<div className="flex w-full items-center justify-between">
|
|
||||||
<div className="flex cursor-help items-center gap-1.5">
|
<div className="flex cursor-help items-center gap-1.5">
|
||||||
Estimated Cost
|
Input Tokens
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Info className="text-muted-foreground/70 h-3.5 w-3.5" />
|
<Info className="text-muted-foreground/70 h-3.5 w-3.5" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>Approximate cost based on configured pricing.</p>
|
<p>Tokens sent to the model (prompt, context, files).</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
}
|
||||||
variant="ghost"
|
value={formatNumber(totals.promptTokens)}
|
||||||
size="icon"
|
icon={MessageSquare}
|
||||||
className="ml-2 h-5 w-5 hover:bg-white/10"
|
isLoading={dailyLoading}
|
||||||
onClick={() => setIsPricingDialogOpen(true)}
|
accent="blue"
|
||||||
>
|
trend={
|
||||||
<Settings className="text-muted-foreground h-3 w-3" />
|
prevDailyData
|
||||||
</Button>
|
? calcTrend(totals.promptTokens, prevTotals.promptTokens)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
sparkline={sparklinePrompt}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="group relative">
|
||||||
|
<StatCard
|
||||||
|
label={
|
||||||
|
<div className="flex cursor-help items-center gap-1.5">
|
||||||
|
Output Tokens
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Info className="text-muted-foreground/70 h-3.5 w-3.5" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Tokens generated by the model in response.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
value={formatNumber(totals.completionTokens)}
|
||||||
|
icon={Activity}
|
||||||
|
isLoading={dailyLoading}
|
||||||
|
accent="green"
|
||||||
|
trend={
|
||||||
|
prevDailyData
|
||||||
|
? calcTrend(totals.completionTokens, prevTotals.completionTokens)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
sparkline={sparklineCompletion}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<StatCard
|
||||||
|
label="Total Requests"
|
||||||
|
value={totals.requests.toLocaleString()}
|
||||||
|
icon={Layers}
|
||||||
|
isLoading={dailyLoading}
|
||||||
|
accent="slate"
|
||||||
|
trend={prevDailyData ? calcTrend(totals.requests, prevTotals.requests) : undefined}
|
||||||
|
sparkline={sparklineRequests}
|
||||||
|
/>
|
||||||
|
<div className="group relative">
|
||||||
|
<StatCard
|
||||||
|
label={
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<div className="flex cursor-help items-center gap-1.5">
|
||||||
|
Estimated Cost
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Info className="text-muted-foreground/70 h-3.5 w-3.5" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Approximate cost based on configured pricing.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="ml-2 h-5 w-5 hover:bg-white/10"
|
||||||
|
onClick={() => setIsPricingDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<Settings className="text-muted-foreground h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
value={`$${totalCost.toFixed(2)}`}
|
||||||
|
icon={DollarSign}
|
||||||
|
isLoading={modelLoading}
|
||||||
|
accent="purple"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
{/* ─── Prompt/Completion ratio bar + top models ─── */}
|
||||||
|
{!dailyLoading && totals.totalTokens > 0 && (
|
||||||
|
<div className="grid grid-cols-1 gap-3 lg:grid-cols-3">
|
||||||
|
<div className="bg-card col-span-1 flex flex-col gap-3 rounded-xl border border-white/[0.06] p-4 lg:col-span-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-muted-foreground text-[11px] font-medium tracking-wider uppercase">
|
||||||
|
Input / Output
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground font-mono text-xs">
|
||||||
|
{formatNumber(totals.totalTokens)} total
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex h-2.5 w-full overflow-hidden rounded-full bg-white/[0.04]">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-l-full transition-all duration-500"
|
||||||
|
style={{
|
||||||
|
width: `${promptRatio}%`,
|
||||||
|
backgroundColor: COLOR_PROMPT,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="h-full rounded-r-full transition-all duration-500"
|
||||||
|
style={{
|
||||||
|
width: `${completionRatio}%`,
|
||||||
|
backgroundColor: COLOR_COMPLETION,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
className="h-2 w-2 rounded-full"
|
||||||
|
style={{ backgroundColor: COLOR_PROMPT }}
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground">Input</span>
|
||||||
|
<span className="text-foreground font-mono font-semibold">
|
||||||
|
{promptRatio}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
className="h-2 w-2 rounded-full"
|
||||||
|
style={{ backgroundColor: COLOR_COMPLETION }}
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground">Output</span>
|
||||||
|
<span className="text-foreground font-mono font-semibold">
|
||||||
|
{completionRatio}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
<span className="text-muted-foreground">
|
||||||
value={`$${totalCost.toFixed(2)}`}
|
Avg/req:{' '}
|
||||||
icon={DollarSign}
|
<span className="text-foreground font-mono font-medium">
|
||||||
|
{formatNumber(avgReqSize)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ForecastCard hourlyData={hourlyData} dailyData={dailyData} range={range} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ─── Charts — Area + Model ranking ─── */}
|
||||||
|
<div className="grid grid-cols-1 gap-3 xl:grid-cols-5">
|
||||||
|
<div className="xl:col-span-3">
|
||||||
|
<ChartCard
|
||||||
|
title={range === '24h' ? 'Hourly Usage' : 'Daily Usage'}
|
||||||
|
isLoading={areaLoading}
|
||||||
|
error={areaError}
|
||||||
|
isEmpty={isAreaEmpty}
|
||||||
|
onRetry={() => (range === '24h' ? refetchHourly() : refetchDaily())}
|
||||||
|
emptyTitle="No usage data"
|
||||||
|
emptyDescription="Data will appear here once proxy requests are recorded."
|
||||||
|
errorMessage="Failed to load usage data"
|
||||||
|
retryLabel="Retry"
|
||||||
|
>
|
||||||
|
<div className="h-[320px]">
|
||||||
|
<ResponsiveLine
|
||||||
|
data={areaChartData}
|
||||||
|
theme={nivoTheme}
|
||||||
|
colors={[COLOR_PROMPT, COLOR_COMPLETION]}
|
||||||
|
margin={{ top: 10, right: 10, bottom: 40, left: 50 }}
|
||||||
|
xScale={{ type: 'point' }}
|
||||||
|
yScale={{ type: 'linear', min: 0, stacked: false }}
|
||||||
|
curve="monotoneX"
|
||||||
|
enableArea={true}
|
||||||
|
areaOpacity={0.08}
|
||||||
|
areaBlendMode="normal"
|
||||||
|
enablePoints={true}
|
||||||
|
pointSize={4}
|
||||||
|
pointColor={{ theme: 'background' }}
|
||||||
|
pointBorderWidth={2}
|
||||||
|
pointBorderColor={{ from: 'serieColor' }}
|
||||||
|
enableGridX={false}
|
||||||
|
enableGridY={true}
|
||||||
|
gridYValues={5}
|
||||||
|
axisBottom={{
|
||||||
|
tickSize: 0,
|
||||||
|
tickPadding: 8,
|
||||||
|
tickRotation: 0,
|
||||||
|
}}
|
||||||
|
axisLeft={{
|
||||||
|
tickSize: 0,
|
||||||
|
tickPadding: 8,
|
||||||
|
tickRotation: 0,
|
||||||
|
format: (v: number) => (v >= 1000 ? `${(v / 1000).toFixed(1)}k` : `${v}`),
|
||||||
|
}}
|
||||||
|
tooltip={LineTooltip}
|
||||||
|
legends={[
|
||||||
|
{
|
||||||
|
anchor: 'bottom',
|
||||||
|
direction: 'row',
|
||||||
|
justify: false,
|
||||||
|
translateX: 0,
|
||||||
|
translateY: 36,
|
||||||
|
itemsSpacing: 16,
|
||||||
|
itemWidth: 80,
|
||||||
|
itemHeight: 16,
|
||||||
|
itemDirection: 'left-to-right',
|
||||||
|
symbolSize: 8,
|
||||||
|
symbolShape: 'circle',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
animate={true}
|
||||||
|
motionConfig="gentle"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ChartCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="xl:col-span-2">
|
||||||
|
<ChartCard
|
||||||
|
title="Model Distribution"
|
||||||
|
isLoading={modelLoading}
|
||||||
|
error={modelError}
|
||||||
|
isEmpty={isModelEmpty}
|
||||||
|
onRetry={() => refetchModel()}
|
||||||
|
emptyTitle="No model data"
|
||||||
|
emptyDescription="Model distribution will appear after proxy usage."
|
||||||
|
errorMessage="Failed to load model data"
|
||||||
|
retryLabel="Retry"
|
||||||
|
>
|
||||||
|
<div className="custom-scrollbar h-[320px] overflow-y-auto pr-2">
|
||||||
|
{topModelsData.map((m) => (
|
||||||
|
<ModelQuotaBar
|
||||||
|
key={m.model}
|
||||||
|
modelName={m.model}
|
||||||
|
usedTokens={m.tokens}
|
||||||
|
limitTokens={m.limit}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ChartCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ─── Cost Report ─── */}
|
||||||
|
{!dailyLoading && modelData && modelData.length > 0 && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<CostReportCard
|
||||||
|
models={costReport}
|
||||||
|
totalInputCost={costReport.reduce((acc, r) => acc + r.inputCost, 0)}
|
||||||
|
totalOutputCost={costReport.reduce((acc, r) => acc + r.outputCost, 0)}
|
||||||
|
totalCost={totalCost}
|
||||||
isLoading={modelLoading}
|
isLoading={modelLoading}
|
||||||
accent="purple"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
{/* ─── Prompt/Completion ratio bar + top models ─── */}
|
{/* ─── Modals ─── */}
|
||||||
{!dailyLoading && totals.totalTokens > 0 && (
|
|
||||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-3">
|
|
||||||
<div className="bg-card col-span-1 flex flex-col gap-3 rounded-xl border border-white/[0.06] p-4 lg:col-span-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-muted-foreground text-[11px] font-medium tracking-wider uppercase">
|
|
||||||
Input / Output
|
|
||||||
</span>
|
|
||||||
<span className="text-muted-foreground font-mono text-xs">
|
|
||||||
{formatNumber(totals.totalTokens)} total
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex h-2.5 w-full overflow-hidden rounded-full bg-white/[0.04]">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-l-full transition-all duration-500"
|
|
||||||
style={{
|
|
||||||
width: `${promptRatio}%`,
|
|
||||||
backgroundColor: COLOR_PROMPT,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="h-full rounded-r-full transition-all duration-500"
|
|
||||||
style={{
|
|
||||||
width: `${completionRatio}%`,
|
|
||||||
backgroundColor: COLOR_COMPLETION,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between text-xs">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<span
|
|
||||||
className="h-2 w-2 rounded-full"
|
|
||||||
style={{ backgroundColor: COLOR_PROMPT }}
|
|
||||||
/>
|
|
||||||
<span className="text-muted-foreground">Input</span>
|
|
||||||
<span className="text-foreground font-mono font-semibold">{promptRatio}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<span
|
|
||||||
className="h-2 w-2 rounded-full"
|
|
||||||
style={{ backgroundColor: COLOR_COMPLETION }}
|
|
||||||
/>
|
|
||||||
<span className="text-muted-foreground">Output</span>
|
|
||||||
<span className="text-foreground font-mono font-semibold">
|
|
||||||
{completionRatio}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Avg/req:{' '}
|
|
||||||
<span className="text-foreground font-mono font-medium">
|
|
||||||
{formatNumber(avgReqSize)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ForecastCard hourlyData={hourlyData} dailyData={dailyData} range={range} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ─── Charts — Area + Model ranking ─── */}
|
|
||||||
<div className="grid grid-cols-1 gap-3 xl:grid-cols-5">
|
|
||||||
<div className="xl:col-span-3">
|
|
||||||
<ChartCard
|
|
||||||
title={range === '24h' ? 'Hourly Usage' : 'Daily Usage'}
|
|
||||||
isLoading={areaLoading}
|
|
||||||
error={areaError}
|
|
||||||
isEmpty={isAreaEmpty}
|
|
||||||
onRetry={() => (range === '24h' ? refetchHourly() : refetchDaily())}
|
|
||||||
emptyTitle="No usage data"
|
|
||||||
emptyDescription="Data will appear here once proxy requests are recorded."
|
|
||||||
errorMessage="Failed to load usage data"
|
|
||||||
retryLabel="Retry"
|
|
||||||
>
|
|
||||||
<div className="h-[320px]">
|
|
||||||
<ResponsiveLine
|
|
||||||
data={areaChartData}
|
|
||||||
theme={nivoTheme}
|
|
||||||
colors={[COLOR_PROMPT, COLOR_COMPLETION]}
|
|
||||||
margin={{ top: 10, right: 10, bottom: 40, left: 50 }}
|
|
||||||
xScale={{ type: 'point' }}
|
|
||||||
yScale={{ type: 'linear', min: 0, stacked: false }}
|
|
||||||
curve="monotoneX"
|
|
||||||
enableArea={true}
|
|
||||||
areaOpacity={0.08}
|
|
||||||
areaBlendMode="normal"
|
|
||||||
enablePoints={true}
|
|
||||||
pointSize={4}
|
|
||||||
pointColor={{ theme: 'background' }}
|
|
||||||
pointBorderWidth={2}
|
|
||||||
pointBorderColor={{ from: 'serieColor' }}
|
|
||||||
enableGridX={false}
|
|
||||||
enableGridY={true}
|
|
||||||
gridYValues={5}
|
|
||||||
axisBottom={{
|
|
||||||
tickSize: 0,
|
|
||||||
tickPadding: 8,
|
|
||||||
tickRotation: 0,
|
|
||||||
}}
|
|
||||||
axisLeft={{
|
|
||||||
tickSize: 0,
|
|
||||||
tickPadding: 8,
|
|
||||||
tickRotation: 0,
|
|
||||||
format: (v: number) => (v >= 1000 ? `${(v / 1000).toFixed(1)}k` : `${v}`),
|
|
||||||
}}
|
|
||||||
tooltip={LineTooltip}
|
|
||||||
legends={[
|
|
||||||
{
|
|
||||||
anchor: 'bottom',
|
|
||||||
direction: 'row',
|
|
||||||
justify: false,
|
|
||||||
translateX: 0,
|
|
||||||
translateY: 36,
|
|
||||||
itemsSpacing: 16,
|
|
||||||
itemWidth: 80,
|
|
||||||
itemHeight: 16,
|
|
||||||
itemDirection: 'left-to-right',
|
|
||||||
symbolSize: 8,
|
|
||||||
symbolShape: 'circle',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
animate={true}
|
|
||||||
motionConfig="gentle"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ChartCard>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="xl:col-span-2">
|
|
||||||
<ChartCard
|
|
||||||
title="Model Distribution"
|
|
||||||
isLoading={modelLoading}
|
|
||||||
error={modelError}
|
|
||||||
isEmpty={isModelEmpty}
|
|
||||||
onRetry={() => refetchModel()}
|
|
||||||
emptyTitle="No model data"
|
|
||||||
emptyDescription="Model distribution will appear after proxy usage."
|
|
||||||
errorMessage="Failed to load model data"
|
|
||||||
retryLabel="Retry"
|
|
||||||
>
|
|
||||||
<div className="custom-scrollbar h-[320px] overflow-y-auto pr-2">
|
|
||||||
{topModelsData.map((m) => (
|
|
||||||
<ModelQuotaBar
|
|
||||||
key={m.model}
|
|
||||||
modelName={m.model}
|
|
||||||
usedTokens={m.tokens}
|
|
||||||
limitTokens={m.limit}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</ChartCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ─── Cost Report ─── */}
|
|
||||||
{!dailyLoading && modelData && modelData.length > 0 && (
|
|
||||||
<div className="mt-6">
|
|
||||||
<CostReportCard
|
|
||||||
models={costReport}
|
|
||||||
totalInputCost={costReport.reduce((acc, r) => acc + r.inputCost, 0)}
|
|
||||||
totalOutputCost={costReport.reduce((acc, r) => acc + r.outputCost, 0)}
|
|
||||||
totalCost={totalCost}
|
|
||||||
isLoading={modelLoading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ─── Modals ─── */}
|
|
||||||
<ModelPricingDialog
|
<ModelPricingDialog
|
||||||
open={isPricingDialogOpen}
|
open={isPricingDialogOpen}
|
||||||
onOpenChange={setIsPricingDialogOpen}
|
onOpenChange={setIsPricingDialogOpen}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import { getSwitchGuardSnapshot } from '../../ipc/switchGuard';
|
|||||||
vi.mock('../../utils/paths', async () => {
|
vi.mock('../../utils/paths', async () => {
|
||||||
const path = await import('path');
|
const path = await import('path');
|
||||||
const agentDir = path.join(process.cwd(), 'temp_test_agent');
|
const agentDir = path.join(process.cwd(), 'temp_test_agent');
|
||||||
return {
|
return {
|
||||||
IS_DEV_ENVIRONMENT: false,
|
IS_DEV_ENVIRONMENT: false,
|
||||||
getAgentDir: vi.fn(() => agentDir),
|
getAgentDir: vi.fn(() => agentDir),
|
||||||
getAccountsFilePath: vi.fn(() => path.join(agentDir, 'accounts.json')),
|
getAccountsFilePath: vi.fn(() => path.join(agentDir, 'accounts.json')),
|
||||||
getBackupsDir: vi.fn(() => path.join(agentDir, 'backups')),
|
getBackupsDir: vi.fn(() => path.join(agentDir, 'backups')),
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ vi.mock('../../ipc/database/dbConnection', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../utils/paths', () => ({
|
vi.mock('../../utils/paths', () => ({
|
||||||
IS_DEV_ENVIRONMENT: false,
|
IS_DEV_ENVIRONMENT: false,
|
||||||
getGeminiNexusDbPaths: () => ['mock-db'],
|
getGeminiNexusDbPaths: () => ['mock-db'],
|
||||||
getCloudAccountsDbPath: () => 'mock-cloud-db',
|
getCloudAccountsDbPath: () => 'mock-cloud-db',
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ vi.mock('../../ipc/database/dbConnection', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../utils/paths', () => ({
|
vi.mock('../../utils/paths', () => ({
|
||||||
IS_DEV_ENVIRONMENT: false,
|
IS_DEV_ENVIRONMENT: false,
|
||||||
getGeminiNexusDbPaths: () => ['mock-db'],
|
getGeminiNexusDbPaths: () => ['mock-db'],
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ vi.mock('../../utils/logger', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../utils/paths', () => ({
|
vi.mock('../../utils/paths', () => ({
|
||||||
IS_DEV_ENVIRONMENT: false,
|
IS_DEV_ENVIRONMENT: false,
|
||||||
getGeminiNexusDbPaths: vi.fn().mockReturnValue(['/tmp/test.vscdb']),
|
getGeminiNexusDbPaths: vi.fn().mockReturnValue(['/tmp/test.vscdb']),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ vi.mock('../../../ipc/database/dbConnection', () => ({
|
|||||||
configureDatabase: vi.fn(),
|
configureDatabase: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../utils/paths', () => ({
|
vi.mock('../../../utils/paths', () => ({
|
||||||
IS_DEV_ENVIRONMENT: false,
|
IS_DEV_ENVIRONMENT: false,
|
||||||
getCloudAccountsDbPath: vi.fn(() => '/mock/db/path.sqlite'),
|
getCloudAccountsDbPath: vi.fn(() => '/mock/db/path.sqlite'),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ vi.mock('../../../utils/logger', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../utils/paths', () => ({
|
vi.mock('../../../utils/paths', () => ({
|
||||||
IS_DEV_ENVIRONMENT: false,
|
IS_DEV_ENVIRONMENT: false,
|
||||||
getGeminiNexusDbPaths: vi.fn().mockReturnValue(['/tmp/test.vscdb']),
|
getGeminiNexusDbPaths: vi.fn().mockReturnValue(['/tmp/test.vscdb']),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ vi.mock('../../../utils/logger', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../utils/paths', () => ({
|
vi.mock('../../../utils/paths', () => ({
|
||||||
IS_DEV_ENVIRONMENT: false,
|
IS_DEV_ENVIRONMENT: false,
|
||||||
getGeminiNexusStoragePaths: vi.fn().mockReturnValue(['/tmp/storage.json']),
|
getGeminiNexusStoragePaths: vi.fn().mockReturnValue(['/tmp/storage.json']),
|
||||||
getGeminiNexusDbPaths: vi.fn().mockReturnValue(['/tmp/state.vscdb']),
|
getGeminiNexusDbPaths: vi.fn().mockReturnValue(['/tmp/state.vscdb']),
|
||||||
|
|||||||
@@ -460,5 +460,5 @@ describe('Proxy Advanced Handler', () => {
|
|||||||
expect(result.data?.proxySetting).toBe('OPENAI_BASE_URL');
|
expect(result.data?.proxySetting).toBe('OPENAI_BASE_URL');
|
||||||
vi.mocked(proxyAdvancedRegistry).proxyIdeConfig = null;
|
vi.mocked(proxyAdvancedRegistry).proxyIdeConfig = null;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -24,7 +24,7 @@ vi.mock('../../../ipc/database/cloudHandler', () => ({
|
|||||||
ensureDatabaseInitialized: vi.fn(),
|
ensureDatabaseInitialized: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../utils/paths', () => ({
|
vi.mock('../../../utils/paths', () => ({
|
||||||
IS_DEV_ENVIRONMENT: false,
|
IS_DEV_ENVIRONMENT: false,
|
||||||
getCloudAccountsDbPath: vi.fn(() => '/mock/db/path.sqlite'),
|
getCloudAccountsDbPath: vi.fn(() => '/mock/db/path.sqlite'),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import path from 'path';
|
|||||||
|
|
||||||
vi.mock('../../utils/paths', async () => {
|
vi.mock('../../utils/paths', async () => {
|
||||||
const path = await import('path');
|
const path = await import('path');
|
||||||
return {
|
return {
|
||||||
IS_DEV_ENVIRONMENT: false,
|
IS_DEV_ENVIRONMENT: false,
|
||||||
getAgentDir: vi.fn(() => path.join(process.cwd(), 'temp_test_logs')),
|
getAgentDir: vi.fn(() => path.join(process.cwd(), 'temp_test_logs')),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -104,8 +104,8 @@ vi.mock('electron', () => {
|
|||||||
session: {
|
session: {
|
||||||
webRequest: {
|
webRequest: {
|
||||||
onBeforeSendHeaders: vi.fn(),
|
onBeforeSendHeaders: vi.fn(),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
on: vi.fn(),
|
on: vi.fn(),
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ vi.mock('../../ipc/database/cloudHandler', () => ({
|
|||||||
ensureDatabaseInitialized: vi.fn(),
|
ensureDatabaseInitialized: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../utils/paths', () => ({
|
vi.mock('../../utils/paths', () => ({
|
||||||
IS_DEV_ENVIRONMENT: false,
|
IS_DEV_ENVIRONMENT: false,
|
||||||
getCloudAccountsDbPath: vi.fn(() => '/mock/db/path.sqlite'),
|
getCloudAccountsDbPath: vi.fn(() => '/mock/db/path.sqlite'),
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in new issue
Block a user