From e5d5402ddadf6a0f96a9a026cebcd696b42d6aea Mon Sep 17 00:00:00 2001 From: Mehdi Date: Fri, 20 Jun 2025 06:49:00 +0000 Subject: [PATCH 1/3] Fixed caching --- assets/README.md | 27 --- jest.config.js | 18 +- package.json | 3 +- src/components/App.tsx | 5 +- src/components/panels/ChatPanel.tsx | 18 ++ src/providers/ClaudeRunnerPanel.ts | 10 +- src/services/ClaudeCodeService.ts | 7 +- src/services/UsageReportService.ts | 250 ++++++++++++++++++++++------ 8 files changed, 245 insertions(+), 93 deletions(-) delete mode 100644 assets/README.md diff --git a/assets/README.md b/assets/README.md deleted file mode 100644 index 1e0f589..0000000 --- a/assets/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Icon Requirements - -This directory should contain: - -1. **icon.png** (128x128px) - Main extension icon - - - Should represent Claude/AI + Terminal/Command concepts - - Use colors that work well in both light and dark themes - - Suggested: Terminal window with AI/brain symbol - -2. **icon-32.png** (32x32px) - Activity bar icon - - Smaller version of the main icon - - Should be clearly visible at small sizes - - High contrast for visibility - -## Icon Design Suggestions - -- **Color scheme**: Use blue/purple for AI, combined with terminal green -- **Symbolism**: - - Terminal/command prompt symbol - - AI/brain/chat bubble - - Play button (for execution) -- **Style**: Modern, flat design that matches VS Code aesthetic - -## Placeholder Usage - -Until proper icons are created, VS Code will use default icons with the specified theme icons in package.json (terminal, play, run, gear, etc.). diff --git a/jest.config.js b/jest.config.js index 4c0a64d..3978d1a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,7 +7,15 @@ module.exports = { "**/?(*.)+(spec|test).+(ts|tsx|js)", ], transform: { - "^.+\\.(ts|tsx)$": "ts-jest", + "^.+\\.(ts|tsx)$": [ + "ts-jest", + { + useESM: false, + tsconfig: { + types: ["jest", "node"], + }, + }, + ], }, moduleNameMapper: { "\\.(css|less|scss|sass)$": "identity-obj-proxy", @@ -15,12 +23,4 @@ module.exports = { }, setupFilesAfterEnv: ["/src/test/setup.ts"], collectCoverageFrom: ["src/**/*.{ts,tsx}", "!src/**/*.d.ts", "!src/test/**"], - globals: { - "ts-jest": { - useESM: false, - tsconfig: { - types: ["jest", "node"], - }, - }, - }, }; diff --git a/package.json b/package.json index 5513972..214fcbd 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,7 @@ }, "categories": [ "Other", - "AI", - "Development Tools" + "Machine Learning" ], "keywords": [ "claude", diff --git a/src/components/App.tsx b/src/components/App.tsx index b7041bb..287c386 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -196,7 +196,9 @@ const App: React.FC = ({ > Pipeline - {showAdvancedTabs && ( + {(showAdvancedTabs || + activeTab === "usage" || + activeTab === "logs") && ( <> + + ); }; diff --git a/src/providers/ClaudeRunnerPanel.ts b/src/providers/ClaudeRunnerPanel.ts index 6ef9d45..fc70606 100644 --- a/src/providers/ClaudeRunnerPanel.ts +++ b/src/providers/ClaudeRunnerPanel.ts @@ -965,7 +965,10 @@ export class ClaudeRunnerPanel implements vscode.WebviewViewProvider { await ClaudeDetectionService.detectClaude(preferredShell); // Update ALL Claude-related state consistently - this._uiState.claudeInstalled = detectionResult.isInstalled; + // Only upgrade, never downgrade the installation status + if (detectionResult.isInstalled || !this._uiState.claudeInstalled) { + this._uiState.claudeInstalled = detectionResult.isInstalled; + } this._uiState.claudeVersion = detectionResult.version ?? "Not Available"; this._uiState.claudeVersionAvailable = detectionResult.isInstalled; this._uiState.claudeVersionError = detectionResult.error; @@ -980,7 +983,10 @@ export class ClaudeRunnerPanel implements vscode.WebviewViewProvider { "ClaudeRunnerPanel: Claude installation recheck failed:", error, ); - this._uiState.claudeInstalled = false; + // Only downgrade if we never had a successful detection + if (!this._uiState.claudeInstalled) { + this._uiState.claudeInstalled = false; + } this._uiState.claudeVersionAvailable = false; this._uiState.claudeVersionError = error instanceof Error ? error.message : "Recheck failed"; diff --git a/src/services/ClaudeCodeService.ts b/src/services/ClaudeCodeService.ts index 31ebd92..b71fd92 100644 --- a/src/services/ClaudeCodeService.ts +++ b/src/services/ClaudeCodeService.ts @@ -2,7 +2,6 @@ import { spawn } from "child_process"; import { ConfigurationService } from "./ConfigurationService"; import { WorkflowService } from "./WorkflowService"; import { WorkflowExecution, StepOutput } from "../types/WorkflowTypes"; -import { ShellDetection } from "../utils/ShellDetection"; import { ClaudeDetectionService } from "./ClaudeDetectionService"; export interface TaskOptions { @@ -60,8 +59,8 @@ export class ClaudeCodeService { constructor(private readonly configService: ConfigurationService) {} async checkInstallation(): Promise { - const isInstalled = await ShellDetection.checkClaudeInstallation("auto"); - if (!isInstalled) { + const result = await ClaudeDetectionService.detectClaude("auto"); + if (!result.isInstalled) { throw new Error( "Claude Code CLI not found in PATH. Please install Claude Code.", ); @@ -405,7 +404,7 @@ export class ClaudeCodeService { } else { let errorMsg = stderr || `Command failed with exit code ${exitCode}`; if (exitCode === 127) { - errorMsg = `Command not found: ${args[0]}. Ensure claude CLI is installed via npm.`; + errorMsg = `Claude CLI not found in this terminal PATH. The installation itself is still registered – re-open VS Code or fix your PATH if you need it here.`; } resolve({ success: false, diff --git a/src/services/UsageReportService.ts b/src/services/UsageReportService.ts index ddb8885..37858fe 100644 --- a/src/services/UsageReportService.ts +++ b/src/services/UsageReportService.ts @@ -1,4 +1,4 @@ -import { readFile } from "fs/promises"; +import { readFile, writeFile, mkdir, stat } from "fs/promises"; import { homedir } from "os"; import path from "path"; import { glob } from "glob"; @@ -47,6 +47,20 @@ export interface PeriodUsageReport { totals: Omit & { models: string[] }; } +interface HourlyUsage { + hour: string; // "2025-06-19T14:00:00.000Z" + models: Record< + string, + { + input: number; + output: number; + cacheCreate: number; + cacheRead: number; + cost: number; + } + >; +} + const LITELLM_PRICING_URL = "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json"; @@ -57,6 +71,74 @@ export class UsageReportService { constructor() {} + // ---------- cache paths ---------- + private getUsageDir(): string { + return path.join(homedir(), ".claude", "usage"); + } + + private getHourlyDir(): string { + return path.join(this.getUsageDir(), "hourly"); + } + + private getMetaPath(): string { + return path.join(this.getUsageDir(), "meta.json"); + } + + // ---------- meta helpers ---------- + private async readMeta(): Promise<{ last?: string }> { + try { + return JSON.parse(await readFile(this.getMetaPath(), "utf8")); + } catch { + return {}; + } + } + + private async writeMeta(last: string): Promise { + await mkdir(this.getUsageDir(), { recursive: true }); + await writeFile(this.getMetaPath(), JSON.stringify({ last })); + } + + // ---------- hourly I/O ---------- + private hourlyFilename(dt: Date): string { + return path.join( + this.getHourlyDir(), + `${dt.toISOString().slice(0, 13)}.json`, + ); // YYYY-MM-DDTHH + } + + private async appendToHourly( + hour: string, + delta: HourlyUsage, + ): Promise { + const fp = this.hourlyFilename(new Date(hour)); + await mkdir(this.getHourlyDir(), { recursive: true }); + + let current: HourlyUsage; + try { + const content = await readFile(fp, "utf8"); + current = content.trim() ? JSON.parse(content) : { hour, models: {} }; + } catch { + current = { hour, models: {} }; + } + + // merge per-model numbers + for (const [model, m] of Object.entries(delta.models)) { + const tgt = (current.models[model] ||= { + input: 0, + output: 0, + cacheCreate: 0, + cacheRead: 0, + cost: 0, + }); + tgt.input += m.input; + tgt.output += m.output; + tgt.cacheCreate += m.cacheCreate; + tgt.cacheRead += m.cacheRead; + tgt.cost += m.cost; + } + await writeFile(fp, JSON.stringify(current)); + } + private async fetchPricing(): Promise> { const now = Date.now(); @@ -218,7 +300,7 @@ export class UsageReportService { return `${messageId}:${requestId}`; } - private async loadUsageData(): Promise { + private async readNewLines(since: Date): Promise { const claudePath = this.getDefaultClaudePath(); const claudeDir = path.join(claudePath, "projects"); @@ -233,10 +315,16 @@ export class UsageReportService { } const processedHashes = new Set(); - const allEntries: UsageData[] = []; + const newEntries: UsageData[] = []; for (const file of files) { try { + // 🚀 FAST EXIT – file hasn't changed since we last saw it + const { mtime } = await stat(file); + if (mtime <= since) { + continue; + } + const content = await readFile(file, "utf-8"); const lines = content .trim() @@ -252,6 +340,12 @@ export class UsageReportService { continue; } + // Skip entries older than since + const entryDate = new Date(data.timestamp); + if (entryDate <= since) { + continue; + } + // Check for duplicates const uniqueHash = this.createUniqueHash(data); if (uniqueHash && processedHashes.has(uniqueHash)) { @@ -262,7 +356,7 @@ export class UsageReportService { processedHashes.add(uniqueHash); } - allEntries.push(data); + newEntries.push(data); } catch { // Skip invalid JSON lines } @@ -272,17 +366,74 @@ export class UsageReportService { } } - return allEntries; + return newEntries; } catch (error) { - console.error("Failed to load usage data:", error); + console.error("Failed to read new usage data:", error); return []; } } + private async loadUsageData(): Promise { + const meta = await this.readMeta(); + const since = meta.last ? new Date(meta.last) : new Date(0); + + const rawLines = await this.readNewLines(since); + if (rawLines.length === 0) { + return []; + } + + const byHour: Record = {}; + let newest = since; + + for (const entry of rawLines) { + const ts = new Date(entry.timestamp); + if (ts > newest) { + newest = ts; + } + const hourIso = ts.toISOString().slice(0, 13) + ":00:00.000Z"; + + const m = entry.message.model ?? "unknown"; + byHour[hourIso] ??= { hour: hourIso, models: {} }; + const agg = (byHour[hourIso].models[m] ||= { + input: 0, + output: 0, + cacheCreate: 0, + cacheRead: 0, + cost: 0, + }); + + agg.input += entry.message.usage.input_tokens; + agg.output += entry.message.usage.output_tokens; + agg.cacheCreate += entry.message.usage.cache_creation_input_tokens ?? 0; + agg.cacheRead += entry.message.usage.cache_read_input_tokens ?? 0; + agg.cost += + entry.costUSD ?? (await this.calculateCost(entry.message.usage, m)); + } + + // persist each hour block + for (const h of Object.values(byHour)) { + await this.appendToHourly(h.hour, h); + } + + await this.writeMeta(newest.toISOString()); + return []; + } + + /** + * Make sure the on-disk hourly cache is current. + * `loadUsageData()` already performs a meta-timestamp check internally, + * so calling it once is enough – if there is nothing new it returns almost + * immediately, otherwise it processes the fresh lines and updates meta. + */ + private async ensureCache(): Promise { + await this.loadUsageData(); // single scan, single write + } + public async generateReport( period: "today" | "week" | "month", ): Promise { - const usageData = await this.loadUsageData(); + // Bring the hourly cache up to date (cheap no-op when unchanged) + await this.ensureCache(); const now = new Date(); let startDate: Date; @@ -306,27 +457,40 @@ export class UsageReportService { break; } - // Filter data by date range - const filteredData = usageData.filter((entry) => { - const entryDate = new Date(entry.timestamp); - return entryDate >= startDate && entryDate <= endDate; - }); + // locate which hour files fall in [startDate, endDate] + const hours: string[] = []; + for ( + let d = new Date(startDate); + d <= endDate; + d.setHours(d.getHours() + 1) + ) { + hours.push(this.hourlyFilename(d)); + } + + const hourlyData: HourlyUsage[] = []; + for (const fp of hours) { + try { + hourlyData.push(JSON.parse(await readFile(fp, "utf8"))); + } catch { + /* missing hour – user idle, safe to ignore */ + } + } - // Group by date - const dailyData = new Map(); - for (const entry of filteredData) { - const date = this.formatDate(entry.timestamp); + // Group hourly data by date + const dailyData = new Map(); + for (const hourData of hourlyData) { + const date = this.formatDate(hourData.hour); if (!dailyData.has(date)) { dailyData.set(date, []); } - dailyData.get(date)?.push(entry); + dailyData.get(date)?.push(hourData); } // Generate daily reports const dailyReports: UsageReport[] = []; const allModels = new Set(); - for (const [date, entries] of dailyData) { + for (const [date, hours] of dailyData) { const modelStats = new Map< string, { @@ -338,38 +502,28 @@ export class UsageReportService { } >(); - for (const entry of entries) { - const model = entry.message.model ?? "unknown"; - if (model !== "") { - allModels.add(model); - } - - const existing = modelStats.get(model) ?? { - inputTokens: 0, - outputTokens: 0, - cacheCreateTokens: 0, - cacheReadTokens: 0, - cost: 0, - }; - - const usage = entry.message.usage; - let cost = entry.costUSD ?? 0; + for (const hourData of hours) { + for (const [model, stats] of Object.entries(hourData.models)) { + if (model !== "") { + allModels.add(model); + } - // If no pre-calculated cost, calculate it - if (!cost && model && model !== "unknown") { - cost = await this.calculateCost(usage, model); + const existing = modelStats.get(model) ?? { + inputTokens: 0, + outputTokens: 0, + cacheCreateTokens: 0, + cacheReadTokens: 0, + cost: 0, + }; + + modelStats.set(model, { + inputTokens: existing.inputTokens + stats.input, + outputTokens: existing.outputTokens + stats.output, + cacheCreateTokens: existing.cacheCreateTokens + stats.cacheCreate, + cacheReadTokens: existing.cacheReadTokens + stats.cacheRead, + cost: existing.cost + stats.cost, + }); } - - modelStats.set(model, { - inputTokens: existing.inputTokens + usage.input_tokens, - outputTokens: existing.outputTokens + usage.output_tokens, - cacheCreateTokens: - existing.cacheCreateTokens + - (usage.cache_creation_input_tokens ?? 0), - cacheReadTokens: - existing.cacheReadTokens + (usage.cache_read_input_tokens ?? 0), - cost: existing.cost + cost, - }); } // Aggregate totals for the day From 7ab57b0b74fa02df399dd90e939cfeda6341ca5a Mon Sep 17 00:00:00 2001 From: Mehdi Date: Sat, 21 Jun 2025 00:39:42 +0000 Subject: [PATCH 2/3] Add hourly usage report --- CLAUDE.md | 17 - Makefile | 7 +- README.md | 2 + VERSION | 2 +- assets/marketplace/currentuse.png | Bin 0 -> 42510 bytes package.json | 7 +- src/components/hooks/useVSCodeAPI.ts | 8 +- src/components/panels/UsageReportPanel.tsx | 272 ++++- src/components/styles.css | 7 + src/components/webview/MessageRouter.ts | 145 +++ src/components/webview/index.ts | 5 + .../webview/main.ts} | 4 +- src/components/webview/template.ts | 23 + src/controllers/RunnerController.ts | 736 +++++++++++++ src/providers/ClaudeRunnerPanel.ts | 982 +++--------------- src/services/UsageReportService.ts | 305 +++++- .../UsageReportService.aggregation.test.ts | 225 ++++ .../UsageReportService.simple.test.ts | 303 ++++++ src/types/runner.ts | 94 ++ vsix.md | 3 + webpack.config.js | 2 +- 21 files changed, 2199 insertions(+), 950 deletions(-) create mode 100644 assets/marketplace/currentuse.png create mode 100644 src/components/webview/MessageRouter.ts create mode 100644 src/components/webview/index.ts rename src/{webview-main.ts => components/webview/main.ts} (96%) create mode 100644 src/components/webview/template.ts create mode 100644 src/controllers/RunnerController.ts create mode 100644 src/test/services/UsageReportService.aggregation.test.ts create mode 100644 src/test/services/UsageReportService.simple.test.ts create mode 100644 src/types/runner.ts diff --git a/CLAUDE.md b/CLAUDE.md index 21ec6a8..04c6fbb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -393,20 +393,3 @@ The extension supports all current Claude models: - Handle model-specific capabilities and limitations - Provide fallbacks for deprecated model versions - Support both alias and full model names - -## Future Enhancements - -### Planned Features - -- MCP (Model Context Protocol) server integration -- Visual session management with history -- Advanced tool permission management -- Team/shared configuration support -- Performance monitoring and usage analytics - -### Architecture Evolution - -- Plugin system for custom integrations -- Advanced caching for improved performance -- Real-time collaboration features -- Integration with other AI development tools diff --git a/Makefile b/Makefile index b60fe1d..1bd3884 100644 --- a/Makefile +++ b/Makefile @@ -110,11 +110,8 @@ test-watch: # Run linting and fix issues lint: - @echo "🔍 Running ESLint..." - @npm run lint -- --fix || true - @echo "" - @echo "📋 Checking for remaining issues..." - @npm run lint + @echo "🔍 Running ESLint with auto-fix..." + @npm run lint -- --fix @echo "✅ Linting complete" # Run all validation diff --git a/README.md b/README.md index 36b79e2..c6814a1 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A Visual Studio Code extension that provides a seamless interface for executing Claude Code commands directly from your development environment. +[Visual Studio Market place](https://marketplace.visualstudio.com/items?itemName=Codingworkflow.claude-runner) + ## Features - **Model Selection**: Choose from all available Claude models (Opus 4, Sonnet 4, Sonnet 3.7, Haiku 3.5) diff --git a/VERSION b/VERSION index 6c6aa7c..6da28dd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0 \ No newline at end of file +0.1.1 \ No newline at end of file diff --git a/assets/marketplace/currentuse.png b/assets/marketplace/currentuse.png new file mode 100644 index 0000000000000000000000000000000000000000..605ff3100fbfed7af9cda24114532c084d71e725 GIT binary patch literal 42510 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

YK|x643u@YX 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 literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 214fcbd..915146c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "claude-runner", "displayName": "Claude Runner", "description": "Execute Claude Code commands directly from VS Code with an intuitive interface", - "version": "0.1.0", + "version": "0.1.1", "publisher": "Codingworkflow", "private": true, "license": "GPL-3.0", @@ -220,7 +220,7 @@ "prepare-marketplace": "node scripts/prepare-marketplace.js", "optimize-images": "node scripts/optimize-images.js", "quality": "npm run lint && npm run type-check && npm run format:check", - "quality:fix": "npm run lint --fix && npm run format", + "quality:fix": "npm run lint -- --fix && npm run format", "validate": "npm run quality && npm run test:unit", "ci": "npm run clean && npm run quality && npm run compile && npm run test:unit" }, @@ -264,6 +264,7 @@ "glob": "^10.3.10", "js-yaml": "^4.1.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "rxjs": "^7.8.2" } } diff --git a/src/components/hooks/useVSCodeAPI.ts b/src/components/hooks/useVSCodeAPI.ts index 237493f..494d90d 100644 --- a/src/components/hooks/useVSCodeAPI.ts +++ b/src/components/hooks/useVSCodeAPI.ts @@ -154,8 +154,12 @@ export const useVSCodeAPI = () => { ); const requestUsageReport = useCallback( - (period: "today" | "week" | "month") => { - sendMessage("requestUsageReport", { period }); + ( + period: "today" | "week" | "month" | "hourly", + hours?: number, + startHour?: number, + ) => { + sendMessage("requestUsageReport", { period, hours, startHour }); }, [sendMessage], ); diff --git a/src/components/panels/UsageReportPanel.tsx b/src/components/panels/UsageReportPanel.tsx index 73c430d..273d154 100644 --- a/src/components/panels/UsageReportPanel.tsx +++ b/src/components/panels/UsageReportPanel.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef, useCallback } from "react"; import Card from "../common/Card"; import { useVSCodeAPI } from "../hooks/useVSCodeAPI"; @@ -6,7 +6,7 @@ interface UsageReportPanelProps { disabled?: boolean; } -type Period = "today" | "week" | "month"; +type Period = "hourly" | "today" | "week" | "month"; interface UsageReport { date: string; @@ -31,34 +31,100 @@ const UsageReportPanel: React.FC = ({ disabled = false, }) => { const [selectedPeriod, setSelectedPeriod] = useState("today"); + const [totalHours, setTotalHours] = useState(5); + // Get UTC hour instead of local timezone hour + const [startHour, setStartHour] = useState(new Date().getUTCHours()); + const [limitType, setLimitType] = useState<"input" | "output" | "cost">( + "output", + ); + const [limitValue, setLimitValue] = useState(0); + const [autoRefresh, setAutoRefresh] = useState(false); const [report, setReport] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const timeoutRef = useRef(null); + const refreshIntervalRef = useRef(null); const { requestUsageReport } = useVSCodeAPI(); - const loadReport = (period: Period) => { - setLoading(true); - setError(null); - - // Clear any existing timeout - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); + const getCurrentValue = (): number => { + if (!report) { + return 0; + } + switch (limitType) { + case "input": + return report.totals.inputTokens; + case "output": + return report.totals.outputTokens; + case "cost": + return report.totals.costUSD; + default: + return 0; } + }; - requestUsageReport(period); + const loadReport = useCallback( + (period: Period, hours?: number, start?: number, silent = false) => { + if (!silent) { + setLoading(true); + } + setError(null); - // Add timeout to handle cases where extension doesn't respond - timeoutRef.current = setTimeout(() => { - setLoading(false); - setError("Request timed out. Please try again."); - }, 30000); // 30 second timeout - }; + // Clear any existing timeout + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + if (period === "hourly" && hours !== undefined && start !== undefined) { + requestUsageReport(period, hours, start); + } else { + requestUsageReport(period); + } + + // Add timeout to handle cases where extension doesn't respond + timeoutRef.current = setTimeout(() => { + setLoading(false); + setError("Request timed out. Please try again."); + }, 30000); // 30 second timeout + }, + [requestUsageReport], + ); useEffect(() => { - loadReport(selectedPeriod); - }, [selectedPeriod]); + if (selectedPeriod === "hourly") { + loadReport(selectedPeriod, totalHours, startHour); + } else { + loadReport(selectedPeriod); + } + }, [selectedPeriod, totalHours, startHour]); + + // Auto-refresh effect + useEffect(() => { + if (autoRefresh) { + refreshIntervalRef.current = setInterval( + () => { + // Use silent mode for auto-refresh to avoid loading spinner + if (selectedPeriod === "hourly") { + loadReport(selectedPeriod, totalHours, startHour, true); + } else { + loadReport(selectedPeriod, undefined, undefined, true); + } + }, + 5 * 60 * 1000, + ); // 5 minutes + } else { + if (refreshIntervalRef.current) { + clearInterval(refreshIntervalRef.current); + refreshIntervalRef.current = null; + } + } + + return () => { + if (refreshIntervalRef.current) { + clearInterval(refreshIntervalRef.current); + } + }; + }, [autoRefresh, selectedPeriod, totalHours, startHour, loadReport]); // Listen for usage report data from extension useEffect(() => { @@ -106,6 +172,18 @@ const UsageReportPanel: React.FC = ({ return "Last 7 Days"; case "month": return "Last 30 Days"; + case "hourly": { + const endHour = startHour + totalHours; + const startDisplay = + startHour < 0 + ? `-${Math.abs(startHour).toString().padStart(2, "0")}` + : startHour.toString().padStart(2, "0"); + const endDisplay = + endHour < 0 + ? `-${Math.abs(endHour).toString().padStart(2, "0")}` + : endHour.toString().padStart(2, "0"); + return `${totalHours} Hours (${startDisplay}:00 – ${endDisplay}:00 UTC)`; + } } }; @@ -127,10 +205,154 @@ const UsageReportPanel: React.FC = ({ + + {selectedPeriod === "hourly" && ( +

+
+
+ + { + const newValue = Math.max( + 1, + Math.min(24, parseInt(e.target.value) || 1), + ); + setTotalHours(newValue); + }} + disabled={disabled || loading} + className="dropdown" + style={{ + width: "50px", + textAlign: "center", + marginLeft: "8px", + }} + /> +
+
+ + +
+
+
+
+ + +
+
+ + setLimitValue(parseInt(e.target.value) || 0)} + disabled={disabled || loading} + className="dropdown" + style={{ + width: "80px", + textAlign: "center", + marginLeft: "8px", + }} + /> +
+
+
+ setAutoRefresh(e.target.checked)} + disabled={disabled || loading} + /> + +
+ {limitValue > 0 && report && ( +
+
+ {limitType === "input" && + `Input tokens: ${report.totals.inputTokens} / ${limitValue}`} + {limitType === "output" && + `Output tokens: ${report.totals.outputTokens} / ${limitValue}`} + {limitType === "cost" && + `Cost: $${report.totals.costUSD.toFixed(2)} / $${limitValue}`} +
+
+
limitValue + ? "var(--vscode-errorForeground)" + : "#4CAF50", + transition: "width 0.3s ease", + }} + /> +
+
+ )} +
+ )} + {loading && (

Loading usage data...

@@ -141,7 +363,13 @@ const UsageReportPanel: React.FC = ({

Error: {error}