diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 51e92e80..852c5ddf 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -45,6 +45,7 @@ import { getTerminalService } from "./services/terminal-service.js"; import { SettingsService } from "./services/settings-service.js"; import { createSpecRegenerationRoutes } from "./routes/app-spec/index.js"; import { createClaudeRoutes } from "./routes/claude/index.js"; +import { ClaudeUsageService } from "./services/claude-usage-service.js"; // Load environment variables dotenv.config(); @@ -112,6 +113,7 @@ const agentService = new AgentService(DATA_DIR, events); const featureLoader = new FeatureLoader(); const autoModeService = new AutoModeService(events); const settingsService = new SettingsService(DATA_DIR); +const claudeUsageService = new ClaudeUsageService(); // Initialize services (async () => { @@ -142,7 +144,7 @@ app.use("/api/workspace", createWorkspaceRoutes()); app.use("/api/templates", createTemplatesRoutes()); app.use("/api/terminal", createTerminalRoutes()); app.use("/api/settings", createSettingsRoutes(settingsService)); -app.use("/api/claude", createClaudeRoutes()); +app.use("/api/claude", createClaudeRoutes(claudeUsageService)); // Create HTTP server const server = createServer(app); diff --git a/apps/server/src/routes/claude/index.ts b/apps/server/src/routes/claude/index.ts index 8f359d5f..f951aa34 100644 --- a/apps/server/src/routes/claude/index.ts +++ b/apps/server/src/routes/claude/index.ts @@ -1,9 +1,8 @@ import { Router, Request, Response } from "express"; import { ClaudeUsageService } from "../../services/claude-usage-service.js"; -export function createClaudeRoutes(): Router { +export function createClaudeRoutes(service: ClaudeUsageService): Router { const router = Router(); - const service = new ClaudeUsageService(); // Get current usage (fetches from Claude CLI) router.get("/usage", async (req: Request, res: Response) => { diff --git a/apps/ui/src/components/claude-usage-popover.tsx b/apps/ui/src/components/claude-usage-popover.tsx index 84131e8d..3288e5f1 100644 --- a/apps/ui/src/components/claude-usage-popover.tsx +++ b/apps/ui/src/components/claude-usage-popover.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect, useMemo, useCallback } from "react"; import { Popover, PopoverContent, @@ -29,7 +29,7 @@ export function ClaudeUsagePopover() { return !claudeUsageLastUpdated || Date.now() - claudeUsageLastUpdated > 2 * 60 * 1000; }, [claudeUsageLastUpdated]); - const fetchUsage = async (isAutoRefresh = false) => { + const fetchUsage = useCallback(async (isAutoRefresh = false) => { if (!isAutoRefresh) setLoading(true); setError(null); try { @@ -38,7 +38,7 @@ export function ClaudeUsagePopover() { throw new Error("Claude API not available"); } const data = await api.claude.getUsage(); - if (data.error) { + if ("error" in data) { throw new Error(data.message || data.error); } setClaudeUsage(data); @@ -47,26 +47,20 @@ export function ClaudeUsagePopover() { } finally { if (!isAutoRefresh) setLoading(false); } - }; + }, [setClaudeUsage]); // Auto-fetch on mount if data is stale useEffect(() => { if (isStale) { fetchUsage(true); } - }, []); + }, [isStale, fetchUsage]); useEffect(() => { // Initial fetch when opened if (open) { - if (!claudeUsage) { + if (!claudeUsage || isStale) { fetchUsage(); - } else { - const now = Date.now(); - const stale = !claudeUsageLastUpdated || now - claudeUsageLastUpdated > 2 * 60 * 1000; - if (stale) { - fetchUsage(false); - } } } @@ -81,7 +75,7 @@ export function ClaudeUsagePopover() { return () => { if (intervalId) clearInterval(intervalId); }; - }, [open]); + }, [open, claudeUsage, isStale, claudeRefreshInterval, fetchUsage]); // Derived status color/icon helper const getStatusInfo = (percentage: number) => { diff --git a/apps/ui/src/lib/electron.ts b/apps/ui/src/lib/electron.ts index 5e01b492..bdb09748 100644 --- a/apps/ui/src/lib/electron.ts +++ b/apps/ui/src/lib/electron.ts @@ -1,5 +1,6 @@ // Type definitions for Electron IPC API import type { SessionListItem, Message } from "@/types/electron"; +import type { ClaudeUsageResponse } from "@/store/app-store"; import { getJSON, setJSON, removeItem } from "./storage"; export interface FileEntry { @@ -483,7 +484,7 @@ export interface ElectronAPI { ) => Promise<{ success: boolean; error?: string }>; }; claude?: { - getUsage: () => Promise; + getUsage: () => Promise; }; } diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index b78e8596..b713472a 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -24,7 +24,7 @@ import type { SuggestionType, } from "./electron"; import type { Message, SessionListItem } from "@/types/electron"; -import type { Feature } from "@/store/app-store"; +import type { Feature, ClaudeUsageResponse } from "@/store/app-store"; import type { WorktreeAPI, GitAPI, @@ -1019,7 +1019,7 @@ export class HttpApiClient implements ElectronAPI { // Claude API claude = { - getUsage: (): Promise => this.get("/api/claude/usage"), + getUsage: (): Promise => this.get("/api/claude/usage"), }; } diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index 365962c9..4afafc2c 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -515,7 +515,7 @@ export interface AppState { } // Claude Usage interface matching the server response -export interface ClaudeUsage { +export type ClaudeUsage = { sessionTokensUsed: number; sessionLimit: number; sessionPercentage: number; @@ -535,7 +535,15 @@ export interface ClaudeUsage { costUsed: number | null; costLimit: number | null; costCurrency: string | null; -} + + lastUpdated: string; + userTimezone: string; +}; + +// Response type for Claude usage API (can be success or error) +export type ClaudeUsageResponse = + | ClaudeUsage + | { error: string; message?: string }; /** * Check if Claude usage is at its limit (any of: session >= 100%, weekly >= 100%, OR cost >= limit)