mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add AI Inference Summary for total costs and token usage
- Add persistent usage tracking service that stores data in ~/.roo/usage-tracking.json - Create CostSummary React component to display costs and token breakdown - Integrate usage tracking with task history updates - Add workspace filtering (Current vs All Workspaces) - Implement mode breakdown showing costs per AI mode - Data persists even when tasks are deleted from history Fixes #6822
This commit is contained in:
parent
72668fef8d
commit
c138f3dfec
10 changed files with 508 additions and 1 deletions
|
|
@ -20,6 +20,7 @@ export * from "./telemetry.js"
|
|||
export * from "./terminal.js"
|
||||
export * from "./tool.js"
|
||||
export * from "./type-fu.js"
|
||||
export * from "./usage.js"
|
||||
export * from "./vscode.js"
|
||||
|
||||
export * from "./providers/index.js"
|
||||
|
|
|
|||
42
packages/types/src/usage.ts
Normal file
42
packages/types/src/usage.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { z } from "zod"
|
||||
|
||||
/**
|
||||
* Schema for persistent usage tracking item
|
||||
* Stores cost and token data for completed tasks
|
||||
*/
|
||||
export const PersistentUsageItemSchema = z.object({
|
||||
taskId: z.string(),
|
||||
taskWorkspace: z.string(), // Workspace path for filtering
|
||||
mode: z.string().optional(),
|
||||
timestamp: z.number(),
|
||||
cost: z.number().default(0),
|
||||
inputTokens: z.number().default(0),
|
||||
outputTokens: z.number().default(0),
|
||||
cacheReads: z.number().default(0),
|
||||
cacheWrites: z.number().default(0),
|
||||
})
|
||||
|
||||
export type PersistentUsageItem = z.infer<typeof PersistentUsageItemSchema>
|
||||
|
||||
/**
|
||||
* Usage summary data structure for frontend display
|
||||
*/
|
||||
export interface UsageSummary {
|
||||
totalCost: number
|
||||
totalInputTokens: number
|
||||
totalOutputTokens: number
|
||||
totalCacheReads: number
|
||||
totalCacheWrites: number
|
||||
modeBreakdown: Record<
|
||||
string,
|
||||
{
|
||||
cost: number
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheReads: number
|
||||
cacheWrites: number
|
||||
count: number
|
||||
}
|
||||
>
|
||||
workspaceName?: string
|
||||
}
|
||||
|
|
@ -1432,6 +1432,42 @@ export class ClineProvider
|
|||
if (!this.checkMdmCompliance()) {
|
||||
await this.postMessageToWebview({ type: "action", action: "accountButtonClicked" })
|
||||
}
|
||||
|
||||
// Load persistent usage data asynchronously
|
||||
this.loadPersistentUsageDataAsync()
|
||||
}
|
||||
|
||||
/**
|
||||
* Load persistent usage data asynchronously and send to webview
|
||||
*/
|
||||
public async loadPersistentUsageDataAsync() {
|
||||
try {
|
||||
const { getUsageTrackingService } = await import("../../services/usage-tracking")
|
||||
const usageService = getUsageTrackingService()
|
||||
|
||||
// Get both all workspaces and current workspace data
|
||||
const [allData, currentData] = await Promise.all([
|
||||
usageService.getUsageSummary(),
|
||||
usageService.getUsageSummary(this.cwd),
|
||||
])
|
||||
|
||||
// Send to webview
|
||||
await this.postMessageToWebview({
|
||||
type: "persistentUsageData",
|
||||
persistentUsageData: {
|
||||
all: allData,
|
||||
current: currentData,
|
||||
},
|
||||
})
|
||||
|
||||
// Migrate existing task history if needed
|
||||
const taskHistory = this.getGlobalState("taskHistory") ?? []
|
||||
if (taskHistory.length > 0) {
|
||||
await usageService.migrateFromTaskHistory(taskHistory, this.cwd)
|
||||
}
|
||||
} catch (error) {
|
||||
this.log(`Failed to load persistent usage data: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1957,6 +1993,16 @@ export class ClineProvider
|
|||
}
|
||||
|
||||
await this.updateGlobalState("taskHistory", history)
|
||||
|
||||
// Update persistent usage tracking
|
||||
try {
|
||||
const { getUsageTrackingService } = await import("../../services/usage-tracking")
|
||||
const usageService = getUsageTrackingService()
|
||||
await usageService.updateUsageTracking(item, this.cwd)
|
||||
} catch (error) {
|
||||
this.log(`Failed to update usage tracking: ${error}`)
|
||||
}
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2604,5 +2604,10 @@ export const webviewMessageHandler = async (
|
|||
}
|
||||
break
|
||||
}
|
||||
case "requestPersistentUsageData": {
|
||||
// Load persistent usage data asynchronously
|
||||
provider.loadPersistentUsageDataAsync()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
250
src/services/usage-tracking/index.ts
Normal file
250
src/services/usage-tracking/index.ts
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
import * as path from "path"
|
||||
import * as os from "os"
|
||||
import { PersistentUsageItem, PersistentUsageItemSchema, UsageSummary, HistoryItem } from "@roo-code/types"
|
||||
import { safeWriteJson } from "../../utils/safeWriteJson"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import * as fs from "fs/promises"
|
||||
|
||||
/**
|
||||
* Service for managing persistent usage tracking data
|
||||
* Stores data in ~/.roo/usage-tracking.json
|
||||
*/
|
||||
export class UsageTrackingService {
|
||||
private static instance: UsageTrackingService | null = null
|
||||
private readonly filePath: string
|
||||
|
||||
private constructor() {
|
||||
// Store in global ~/.roo directory
|
||||
const rooDir = path.join(os.homedir(), ".roo")
|
||||
this.filePath = path.join(rooDir, "usage-tracking.json")
|
||||
}
|
||||
|
||||
public static getInstance(): UsageTrackingService {
|
||||
if (!UsageTrackingService.instance) {
|
||||
UsageTrackingService.instance = new UsageTrackingService()
|
||||
}
|
||||
return UsageTrackingService.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* Load existing usage data from disk
|
||||
*/
|
||||
private async loadData(): Promise<PersistentUsageItem[]> {
|
||||
try {
|
||||
if (!(await fileExistsAtPath(this.filePath))) {
|
||||
return []
|
||||
}
|
||||
|
||||
const content = await fs.readFile(this.filePath, "utf-8")
|
||||
const data = JSON.parse(content)
|
||||
|
||||
// Validate and parse each item
|
||||
if (Array.isArray(data)) {
|
||||
return data
|
||||
.map((item) => {
|
||||
try {
|
||||
return PersistentUsageItemSchema.parse(item)
|
||||
} catch {
|
||||
// Skip invalid items
|
||||
return null
|
||||
}
|
||||
})
|
||||
.filter((item): item is PersistentUsageItem => item !== null)
|
||||
}
|
||||
|
||||
return []
|
||||
} catch (error) {
|
||||
console.error("Failed to load usage tracking data:", error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save usage data to disk
|
||||
*/
|
||||
private async saveData(data: PersistentUsageItem[]): Promise<void> {
|
||||
try {
|
||||
await safeWriteJson(this.filePath, data)
|
||||
} catch (error) {
|
||||
console.error("Failed to save usage tracking data:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update usage tracking with new task data
|
||||
*/
|
||||
public async updateUsageTracking(historyItem: HistoryItem, workspacePath: string): Promise<void> {
|
||||
try {
|
||||
const data = await this.loadData()
|
||||
|
||||
// Remove existing entry for this task if it exists
|
||||
const filteredData = data.filter((item) => item.taskId !== historyItem.id)
|
||||
|
||||
// Calculate total cost and tokens from the history item
|
||||
let totalCost = 0
|
||||
let totalInputTokens = 0
|
||||
let totalOutputTokens = 0
|
||||
let totalCacheReads = 0
|
||||
let totalCacheWrites = 0
|
||||
|
||||
// Sum up all API request info from the task
|
||||
if (historyItem.totalCost) {
|
||||
totalCost = historyItem.totalCost
|
||||
}
|
||||
if (historyItem.tokensIn) {
|
||||
totalInputTokens = historyItem.tokensIn
|
||||
}
|
||||
if (historyItem.tokensOut) {
|
||||
totalOutputTokens = historyItem.tokensOut
|
||||
}
|
||||
if (historyItem.cacheReads) {
|
||||
totalCacheReads = historyItem.cacheReads
|
||||
}
|
||||
if (historyItem.cacheWrites) {
|
||||
totalCacheWrites = historyItem.cacheWrites
|
||||
}
|
||||
|
||||
// Add new usage item
|
||||
const newItem: PersistentUsageItem = {
|
||||
taskId: historyItem.id,
|
||||
taskWorkspace: workspacePath,
|
||||
mode: historyItem.mode,
|
||||
timestamp: historyItem.ts,
|
||||
cost: totalCost,
|
||||
inputTokens: totalInputTokens,
|
||||
outputTokens: totalOutputTokens,
|
||||
cacheReads: totalCacheReads,
|
||||
cacheWrites: totalCacheWrites,
|
||||
}
|
||||
|
||||
filteredData.push(newItem)
|
||||
|
||||
// Save updated data
|
||||
await this.saveData(filteredData)
|
||||
} catch (error) {
|
||||
console.error("Failed to update usage tracking:", error)
|
||||
// Don't throw - we don't want to break the main flow
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage summary for display
|
||||
*/
|
||||
public async getUsageSummary(workspacePath?: string): Promise<UsageSummary> {
|
||||
try {
|
||||
const data = await this.loadData()
|
||||
|
||||
// Filter by workspace if specified
|
||||
const filteredData = workspacePath ? data.filter((item) => item.taskWorkspace === workspacePath) : data
|
||||
|
||||
// Calculate totals
|
||||
let totalCost = 0
|
||||
let totalInputTokens = 0
|
||||
let totalOutputTokens = 0
|
||||
let totalCacheReads = 0
|
||||
let totalCacheWrites = 0
|
||||
const modeBreakdown: UsageSummary["modeBreakdown"] = {}
|
||||
|
||||
for (const item of filteredData) {
|
||||
totalCost += item.cost
|
||||
totalInputTokens += item.inputTokens
|
||||
totalOutputTokens += item.outputTokens
|
||||
totalCacheReads += item.cacheReads
|
||||
totalCacheWrites += item.cacheWrites
|
||||
|
||||
// Update mode breakdown
|
||||
const mode = item.mode || "unknown"
|
||||
if (!modeBreakdown[mode]) {
|
||||
modeBreakdown[mode] = {
|
||||
cost: 0,
|
||||
inputTokens: 0,
|
||||
outputTokens: 0,
|
||||
cacheReads: 0,
|
||||
cacheWrites: 0,
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
modeBreakdown[mode].cost += item.cost
|
||||
modeBreakdown[mode].inputTokens += item.inputTokens
|
||||
modeBreakdown[mode].outputTokens += item.outputTokens
|
||||
modeBreakdown[mode].cacheReads += item.cacheReads
|
||||
modeBreakdown[mode].cacheWrites += item.cacheWrites
|
||||
modeBreakdown[mode].count += 1
|
||||
}
|
||||
|
||||
// Get workspace name from path
|
||||
const workspaceName = workspacePath ? path.basename(workspacePath) : undefined
|
||||
|
||||
return {
|
||||
totalCost,
|
||||
totalInputTokens,
|
||||
totalOutputTokens,
|
||||
totalCacheReads,
|
||||
totalCacheWrites,
|
||||
modeBreakdown,
|
||||
workspaceName,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to get usage summary:", error)
|
||||
// Return empty summary on error
|
||||
return {
|
||||
totalCost: 0,
|
||||
totalInputTokens: 0,
|
||||
totalOutputTokens: 0,
|
||||
totalCacheReads: 0,
|
||||
totalCacheWrites: 0,
|
||||
modeBreakdown: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate existing task history to persistent storage
|
||||
*/
|
||||
public async migrateFromTaskHistory(taskHistory: HistoryItem[], workspacePath: string): Promise<void> {
|
||||
try {
|
||||
const data = await this.loadData()
|
||||
const existingTaskIds = new Set(data.map((item) => item.taskId))
|
||||
|
||||
// Only migrate tasks that aren't already in persistent storage
|
||||
const tasksToMigrate = taskHistory.filter((task) => !existingTaskIds.has(task.id))
|
||||
|
||||
for (const task of tasksToMigrate) {
|
||||
if (task.totalCost || task.tokensIn || task.tokensOut) {
|
||||
await this.updateUsageTracking(task, workspacePath)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to migrate task history:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all usage data (for testing or reset)
|
||||
*/
|
||||
public async clearAllData(): Promise<void> {
|
||||
try {
|
||||
await this.saveData([])
|
||||
} catch (error) {
|
||||
console.error("Failed to clear usage data:", error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove usage data for a specific task
|
||||
*/
|
||||
public async removeTask(taskId: string): Promise<void> {
|
||||
try {
|
||||
const data = await this.loadData()
|
||||
const filteredData = data.filter((item) => item.taskId !== taskId)
|
||||
await this.saveData(filteredData)
|
||||
} catch (error) {
|
||||
console.error("Failed to remove task from usage tracking:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance getter
|
||||
export const getUsageTrackingService = () => UsageTrackingService.getInstance()
|
||||
|
|
@ -121,6 +121,7 @@ export interface ExtensionMessage {
|
|||
| "showEditMessageDialog"
|
||||
| "commands"
|
||||
| "insertTextIntoTextarea"
|
||||
| "persistentUsageData"
|
||||
text?: string
|
||||
payload?: any // Add a generic payload for now, can refine later
|
||||
action?:
|
||||
|
|
@ -195,6 +196,10 @@ export interface ExtensionMessage {
|
|||
messageTs?: number
|
||||
context?: string
|
||||
commands?: Command[]
|
||||
persistentUsageData?: {
|
||||
all: any // UsageSummary type
|
||||
current: any // UsageSummary type
|
||||
}
|
||||
}
|
||||
|
||||
export type ExtensionState = Pick<
|
||||
|
|
|
|||
|
|
@ -210,6 +210,7 @@ export interface WebviewMessage {
|
|||
| "deleteCommand"
|
||||
| "createCommand"
|
||||
| "insertTextIntoTextarea"
|
||||
| "requestPersistentUsageData"
|
||||
text?: string
|
||||
editedMessageContent?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
|
||||
|
|
|
|||
133
webview-ui/src/components/welcome/CostSummary.tsx
Normal file
133
webview-ui/src/components/welcome/CostSummary.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import React, { useState, useEffect } from "react"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { UsageSummary } from "@roo-code/types"
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
|
||||
interface CostSummaryProps {
|
||||
usageData?: {
|
||||
all: UsageSummary
|
||||
current: UsageSummary
|
||||
}
|
||||
}
|
||||
|
||||
const CostSummary: React.FC<CostSummaryProps> = ({ usageData }) => {
|
||||
const [viewMode, setViewMode] = useState<"all" | "current">("all")
|
||||
|
||||
// Request usage data on mount
|
||||
useEffect(() => {
|
||||
vscode.postMessage({ type: "requestPersistentUsageData" })
|
||||
}, [])
|
||||
|
||||
if (!usageData) {
|
||||
return (
|
||||
<div className="cost-summary-card border border-vscode-panel-border rounded-lg p-4 mb-4">
|
||||
<h3 className="text-lg font-semibold mb-3">AI Inference Summary</h3>
|
||||
<div className="text-vscode-descriptionForeground">Loading usage data...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const currentData = viewMode === "all" ? usageData.all : usageData.current
|
||||
const workspaceName = viewMode === "current" ? usageData.current.workspaceName : "All Workspaces"
|
||||
|
||||
// Format currency
|
||||
const formatCost = (cost: number) => {
|
||||
return `$${cost.toFixed(4)}`
|
||||
}
|
||||
|
||||
// Format large numbers with commas
|
||||
const formatNumber = (num: number) => {
|
||||
return num.toLocaleString()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="cost-summary-card border border-vscode-panel-border rounded-lg p-4 mb-4">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h3 className="text-lg font-semibold">AI Inference Summary</h3>
|
||||
<div className="flex gap-2">
|
||||
<VSCodeButton
|
||||
appearance={viewMode === "current" ? "primary" : "secondary"}
|
||||
onClick={() => setViewMode("current")}
|
||||
className="text-xs">
|
||||
Current
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance={viewMode === "all" ? "primary" : "secondary"}
|
||||
onClick={() => setViewMode("all")}
|
||||
className="text-xs">
|
||||
All
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-vscode-descriptionForeground mb-3">{workspaceName}</div>
|
||||
|
||||
{/* Total Summary */}
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<div className="text-xs text-vscode-descriptionForeground">Total Cost</div>
|
||||
<div className="text-xl font-bold">{formatCost(currentData.totalCost)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-vscode-descriptionForeground">Total Tokens</div>
|
||||
<div className="text-xl font-bold">
|
||||
{formatNumber(currentData.totalInputTokens + currentData.totalOutputTokens)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Token Breakdown */}
|
||||
<div className="grid grid-cols-2 gap-2 mb-4 text-sm">
|
||||
<div>
|
||||
<span className="text-vscode-descriptionForeground">Input: </span>
|
||||
<span>{formatNumber(currentData.totalInputTokens)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-vscode-descriptionForeground">Output: </span>
|
||||
<span>{formatNumber(currentData.totalOutputTokens)}</span>
|
||||
</div>
|
||||
{(currentData.totalCacheReads > 0 || currentData.totalCacheWrites > 0) && (
|
||||
<>
|
||||
<div>
|
||||
<span className="text-vscode-descriptionForeground">Cache Reads: </span>
|
||||
<span>{formatNumber(currentData.totalCacheReads)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-vscode-descriptionForeground">Cache Writes: </span>
|
||||
<span>{formatNumber(currentData.totalCacheWrites)}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mode Breakdown */}
|
||||
{Object.keys(currentData.modeBreakdown).length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-vscode-descriptionForeground mb-2">Breakdown by Mode</div>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(currentData.modeBreakdown)
|
||||
.sort(([, a], [, b]) => b.cost - a.cost)
|
||||
.map(([mode, data]) => (
|
||||
<div key={mode} className="flex justify-between items-center text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="capitalize">{mode}</span>
|
||||
<span className="text-xs text-vscode-descriptionForeground">
|
||||
({data.count} task{data.count !== 1 ? "s" : ""})
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<span>{formatCost(data.cost)}</span>
|
||||
<span className="text-vscode-descriptionForeground">
|
||||
{formatNumber(data.inputTokens + data.outputTokens)} tokens
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CostSummary
|
||||
|
|
@ -15,9 +15,11 @@ import ApiOptions from "../settings/ApiOptions"
|
|||
import { Tab, TabContent } from "../common/Tab"
|
||||
|
||||
import RooHero from "./RooHero"
|
||||
import CostSummary from "./CostSummary"
|
||||
|
||||
const WelcomeView = () => {
|
||||
const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme, machineId } = useExtensionState()
|
||||
const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme, machineId, persistentUsageData } =
|
||||
useExtensionState()
|
||||
const { t } = useAppTranslation()
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
|
||||
|
||||
|
|
@ -51,6 +53,10 @@ const WelcomeView = () => {
|
|||
<Tab>
|
||||
<TabContent className="flex flex-col gap-5 p-16">
|
||||
<RooHero />
|
||||
|
||||
{/* Add CostSummary component */}
|
||||
<CostSummary usageData={persistentUsageData} />
|
||||
|
||||
<h2 className="mt-0 mb-0">{t("welcome:greeting")}</h2>
|
||||
|
||||
<div className="font-bold">
|
||||
|
|
|
|||
|
|
@ -147,6 +147,10 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setMaxDiagnosticMessages: (value: number) => void
|
||||
includeTaskHistoryInEnhance?: boolean
|
||||
setIncludeTaskHistoryInEnhance: (value: boolean) => void
|
||||
persistentUsageData?: {
|
||||
all: any // UsageSummary type
|
||||
current: any // UsageSummary type
|
||||
}
|
||||
}
|
||||
|
||||
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||
|
|
@ -266,6 +270,13 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
global: {},
|
||||
})
|
||||
const [includeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance] = useState(false)
|
||||
const [persistentUsageData, setPersistentUsageData] = useState<
|
||||
| {
|
||||
all: any // UsageSummary type
|
||||
current: any // UsageSummary type
|
||||
}
|
||||
| undefined
|
||||
>(undefined)
|
||||
|
||||
const setListApiConfigMeta = useCallback(
|
||||
(value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
|
||||
|
|
@ -369,6 +380,12 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
}
|
||||
break
|
||||
}
|
||||
case "persistentUsageData": {
|
||||
if (message.persistentUsageData) {
|
||||
setPersistentUsageData(message.persistentUsageData)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
[setListApiConfigMeta],
|
||||
|
|
@ -395,6 +412,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
filePaths,
|
||||
openedTabs,
|
||||
commands,
|
||||
persistentUsageData,
|
||||
soundVolume: state.soundVolume,
|
||||
ttsSpeed: state.ttsSpeed,
|
||||
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue