mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-11 03:38:15 +00:00
feat: add task storage size display to settings
- Add storage usage display showing total size and task count - Add calculateTaskStorageSize utility to compute global storage folder sizes - Add getTaskStorageSize message handler to communicate storage info to webview - Update About settings section to display storage usage with refresh button - Add taskHistoryStorage translations for all 18 supported languages
This commit is contained in:
parent
f145a52de2
commit
b6f662f852
8 changed files with 513 additions and 2 deletions
|
|
@ -183,6 +183,14 @@ export const globalSettingsSchema = z.object({
|
|||
includeTaskHistoryInEnhance: z.boolean().optional(),
|
||||
// Auto-delete task history on extension reload. "never" | "90" | "60" | "30" | "7" | "3"
|
||||
taskHistoryRetention: z.union([z.enum(["never", "90", "60", "30", "7", "3"]), z.number()]).optional(),
|
||||
// Calculated task history storage size info for the Settings > About page
|
||||
taskHistorySize: z
|
||||
.object({
|
||||
totalBytes: z.number(),
|
||||
taskCount: z.number(),
|
||||
formattedSize: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
historyPreviewCollapsed: z.boolean().optional(),
|
||||
reasoningBlockCollapsed: z.boolean().optional(),
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -396,6 +396,15 @@ export type ExtensionState = Pick<
|
|||
marketplaceInstalledMetadata?: { project: Record<string, any>; global: Record<string, any> }
|
||||
profileThresholds: Record<string, number>
|
||||
hasOpenedModeSelector: boolean
|
||||
/** Task history storage size info for the Settings > About page */
|
||||
taskHistorySize?: {
|
||||
/** Total size in bytes */
|
||||
totalBytes: number
|
||||
/** Number of task directories */
|
||||
taskCount: number
|
||||
/** Formatted size string (e.g., "12.34 MB") */
|
||||
formattedSize: string
|
||||
}
|
||||
openRouterImageApiKey?: string
|
||||
messageQueue?: QueuedMessage[]
|
||||
lastShownAnnouncementId?: string
|
||||
|
|
@ -593,6 +602,7 @@ export interface WebviewMessage {
|
|||
| "requestModes"
|
||||
| "switchMode"
|
||||
| "debugSetting"
|
||||
| "refreshTaskHistorySize"
|
||||
// Worktree messages
|
||||
| "listWorktrees"
|
||||
| "createWorktree"
|
||||
|
|
|
|||
|
|
@ -2054,6 +2054,7 @@ export class ClineProvider
|
|||
enterBehavior,
|
||||
cloudUserInfo,
|
||||
taskHistoryRetention,
|
||||
taskHistorySize,
|
||||
cloudIsAuthenticated,
|
||||
sharingEnabled,
|
||||
publicSharingEnabled,
|
||||
|
|
@ -2239,6 +2240,8 @@ export class ClineProvider
|
|||
includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true,
|
||||
// Task history retention setting for About tab dropdown
|
||||
taskHistoryRetention: taskHistoryRetention ?? "never",
|
||||
// Task history storage size info for the Settings > About page
|
||||
taskHistorySize,
|
||||
includeCurrentTime: includeCurrentTime ?? true,
|
||||
includeCurrentCost: includeCurrentCost ?? true,
|
||||
maxGitStatusFiles: maxGitStatusFiles ?? 0,
|
||||
|
|
@ -2456,6 +2459,8 @@ export class ClineProvider
|
|||
customCondensingPrompt: stateValues.customCondensingPrompt,
|
||||
// Task history retention selection
|
||||
taskHistoryRetention: stateValues.taskHistoryRetention ?? "never",
|
||||
// Task history storage size info
|
||||
taskHistorySize: stateValues.taskHistorySize,
|
||||
codebaseIndexModels: stateValues.codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
|
||||
codebaseIndexConfig: {
|
||||
codebaseIndexEnabled: stateValues.codebaseIndexConfig?.codebaseIndexEnabled ?? false,
|
||||
|
|
|
|||
|
|
@ -532,6 +532,9 @@ export const webviewMessageHandler = async (
|
|||
TelemetryService.instance.updateTelemetryState(isOptedIn)
|
||||
})
|
||||
|
||||
// Note: Task history storage size calculation is triggered on-demand when the About
|
||||
// settings tab is opened (via "refreshTaskHistorySize" message), not on webview launch.
|
||||
|
||||
provider.isViewLaunched = true
|
||||
break
|
||||
case "newTask":
|
||||
|
|
@ -3343,6 +3346,24 @@ export const webviewMessageHandler = async (
|
|||
break
|
||||
}
|
||||
|
||||
case "refreshTaskHistorySize": {
|
||||
// Refresh the task history storage size calculation
|
||||
try {
|
||||
const { calculateTaskStorageSize } = await import("../../utils/task-storage-size")
|
||||
const globalStoragePath = provider.contextProxy.globalStorageUri.fsPath
|
||||
const sizeInfo = await calculateTaskStorageSize(globalStoragePath)
|
||||
|
||||
// Update state and notify webview
|
||||
await provider.contextProxy.setValue("taskHistorySize", sizeInfo)
|
||||
await provider.postStateToWebview()
|
||||
} catch (error) {
|
||||
provider.log(
|
||||
`Error refreshing task history size: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
/**
|
||||
* Git Worktree Management
|
||||
*/
|
||||
|
|
|
|||
237
src/utils/__tests__/task-storage-size.spec.ts
Normal file
237
src/utils/__tests__/task-storage-size.spec.ts
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
import * as path from "path"
|
||||
import { calculateTaskStorageSize, formatBytes } from "../task-storage-size"
|
||||
|
||||
// Mock fs/promises
|
||||
const mockReaddir = vi.fn()
|
||||
const mockStat = vi.fn()
|
||||
|
||||
vi.mock("fs/promises", () => ({
|
||||
readdir: (...args: unknown[]) => mockReaddir(...args),
|
||||
stat: (...args: unknown[]) => mockStat(...args),
|
||||
}))
|
||||
|
||||
describe("formatBytes", () => {
|
||||
it("should format 0 bytes", () => {
|
||||
expect(formatBytes(0)).toBe("0 B")
|
||||
})
|
||||
|
||||
it("should format bytes less than 1 KB", () => {
|
||||
expect(formatBytes(512)).toBe("512 B")
|
||||
expect(formatBytes(1)).toBe("1 B")
|
||||
})
|
||||
|
||||
it("should format kilobytes (0 decimal places)", () => {
|
||||
expect(formatBytes(1024)).toBe("1 KB")
|
||||
expect(formatBytes(2048)).toBe("2 KB")
|
||||
expect(formatBytes(1536)).toBe("2 KB") // Rounds to nearest integer
|
||||
expect(formatBytes(1280)).toBe("1 KB") // Rounds down
|
||||
})
|
||||
|
||||
it("should format megabytes (2 decimal places)", () => {
|
||||
expect(formatBytes(1048576)).toBe("1.00 MB")
|
||||
expect(formatBytes(1572864)).toBe("1.50 MB")
|
||||
expect(formatBytes(10485760)).toBe("10.00 MB")
|
||||
})
|
||||
|
||||
it("should format gigabytes (2 decimal places)", () => {
|
||||
expect(formatBytes(1073741824)).toBe("1.00 GB")
|
||||
expect(formatBytes(2147483648)).toBe("2.00 GB")
|
||||
})
|
||||
|
||||
it("should format terabytes (2 decimal places)", () => {
|
||||
expect(formatBytes(1099511627776)).toBe("1.00 TB")
|
||||
})
|
||||
|
||||
it("should handle decimal precision correctly", () => {
|
||||
expect(formatBytes(1234567)).toBe("1.18 MB")
|
||||
expect(formatBytes(123456789)).toBe("117.74 MB")
|
||||
})
|
||||
})
|
||||
|
||||
describe("calculateTaskStorageSize", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should return zeros when tasks directory does not exist", async () => {
|
||||
mockReaddir.mockRejectedValue(new Error("ENOENT: no such file or directory"))
|
||||
|
||||
const result = await calculateTaskStorageSize("/global/storage")
|
||||
|
||||
expect(result).toEqual({
|
||||
totalBytes: 0,
|
||||
taskCount: 0,
|
||||
formattedSize: "0 B",
|
||||
})
|
||||
})
|
||||
|
||||
it("should calculate size of empty tasks directory", async () => {
|
||||
mockReaddir.mockResolvedValue([])
|
||||
|
||||
const result = await calculateTaskStorageSize("/global/storage")
|
||||
|
||||
expect(result).toEqual({
|
||||
totalBytes: 0,
|
||||
taskCount: 0,
|
||||
formattedSize: "0 B",
|
||||
})
|
||||
})
|
||||
|
||||
it("should count task directories correctly", async () => {
|
||||
// Mock the tasks directory read
|
||||
mockReaddir.mockImplementation((dirPath: string, options?: { withFileTypes: boolean }) => {
|
||||
const pathStr = typeof dirPath === "string" ? dirPath : String(dirPath)
|
||||
if (pathStr.endsWith("tasks")) {
|
||||
// Return task directories
|
||||
return Promise.resolve([
|
||||
{ name: "task-1", isDirectory: () => true, isFile: () => false, isSymbolicLink: () => false },
|
||||
{ name: "task-2", isDirectory: () => true, isFile: () => false, isSymbolicLink: () => false },
|
||||
{ name: "task-3", isDirectory: () => true, isFile: () => false, isSymbolicLink: () => false },
|
||||
])
|
||||
}
|
||||
// Task subdirectories are empty
|
||||
return Promise.resolve([])
|
||||
})
|
||||
|
||||
const result = await calculateTaskStorageSize("/global/storage")
|
||||
|
||||
expect(result.taskCount).toBe(3)
|
||||
})
|
||||
|
||||
it("should calculate total size including files", async () => {
|
||||
// Mock the tasks directory read
|
||||
mockReaddir.mockImplementation((dirPath: string) => {
|
||||
const pathStr = typeof dirPath === "string" ? dirPath : String(dirPath)
|
||||
if (pathStr.endsWith("tasks")) {
|
||||
return Promise.resolve([
|
||||
{ name: "task-1", isDirectory: () => true, isFile: () => false, isSymbolicLink: () => false },
|
||||
])
|
||||
}
|
||||
if (pathStr.includes("task-1")) {
|
||||
return Promise.resolve([
|
||||
{ name: "file1.txt", isDirectory: () => false, isFile: () => true, isSymbolicLink: () => false },
|
||||
{ name: "file2.json", isDirectory: () => false, isFile: () => true, isSymbolicLink: () => false },
|
||||
])
|
||||
}
|
||||
return Promise.resolve([])
|
||||
})
|
||||
|
||||
mockStat.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes("file1.txt")) {
|
||||
return Promise.resolve({ size: 1024 })
|
||||
}
|
||||
if (filePath.includes("file2.json")) {
|
||||
return Promise.resolve({ size: 2048 })
|
||||
}
|
||||
return Promise.resolve({ size: 0 })
|
||||
})
|
||||
|
||||
const result = await calculateTaskStorageSize("/global/storage")
|
||||
|
||||
expect(result.totalBytes).toBe(3072)
|
||||
expect(result.formattedSize).toBe("3 KB")
|
||||
expect(result.taskCount).toBe(1)
|
||||
})
|
||||
|
||||
it("should handle nested directories (like checkpoints)", async () => {
|
||||
mockReaddir.mockImplementation((dirPath: string) => {
|
||||
const pathStr = typeof dirPath === "string" ? dirPath : String(dirPath)
|
||||
if (pathStr.endsWith("tasks")) {
|
||||
return Promise.resolve([
|
||||
{ name: "task-1", isDirectory: () => true, isFile: () => false, isSymbolicLink: () => false },
|
||||
])
|
||||
}
|
||||
if (pathStr.endsWith("task-1") && !pathStr.includes("checkpoints")) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
name: "api_conversation.json",
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
isSymbolicLink: () => false,
|
||||
},
|
||||
{ name: "checkpoints", isDirectory: () => true, isFile: () => false, isSymbolicLink: () => false },
|
||||
])
|
||||
}
|
||||
if (pathStr.includes("checkpoints")) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
name: "checkpoint-1.json",
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
isSymbolicLink: () => false,
|
||||
},
|
||||
])
|
||||
}
|
||||
return Promise.resolve([])
|
||||
})
|
||||
|
||||
mockStat.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes("api_conversation.json")) {
|
||||
return Promise.resolve({ size: 5000 })
|
||||
}
|
||||
if (filePath.includes("checkpoint-1.json")) {
|
||||
return Promise.resolve({ size: 10000 })
|
||||
}
|
||||
return Promise.resolve({ size: 0 })
|
||||
})
|
||||
|
||||
const result = await calculateTaskStorageSize("/global/storage")
|
||||
|
||||
expect(result.totalBytes).toBe(15000)
|
||||
expect(result.taskCount).toBe(1)
|
||||
})
|
||||
|
||||
it("should handle stat errors gracefully", async () => {
|
||||
mockReaddir.mockImplementation((dirPath: string) => {
|
||||
const pathStr = typeof dirPath === "string" ? dirPath : String(dirPath)
|
||||
if (pathStr.endsWith("tasks")) {
|
||||
return Promise.resolve([
|
||||
{ name: "task-1", isDirectory: () => true, isFile: () => false, isSymbolicLink: () => false },
|
||||
])
|
||||
}
|
||||
return Promise.resolve([
|
||||
{ name: "broken-file.txt", isDirectory: () => false, isFile: () => true, isSymbolicLink: () => false },
|
||||
])
|
||||
})
|
||||
|
||||
mockStat.mockRejectedValue(new Error("Permission denied"))
|
||||
|
||||
const result = await calculateTaskStorageSize("/global/storage")
|
||||
|
||||
// Should still return a result, just with 0 bytes for the failed stat
|
||||
expect(result.taskCount).toBe(1)
|
||||
expect(result.totalBytes).toBe(0)
|
||||
})
|
||||
|
||||
it("should handle mixed files and directories in tasks folder", async () => {
|
||||
mockReaddir.mockImplementation((dirPath: string) => {
|
||||
const pathStr = typeof dirPath === "string" ? dirPath : String(dirPath)
|
||||
if (pathStr.endsWith("tasks")) {
|
||||
return Promise.resolve([
|
||||
{ name: "task-1", isDirectory: () => true, isFile: () => false, isSymbolicLink: () => false },
|
||||
{
|
||||
name: "some-file.txt",
|
||||
isDirectory: () => false,
|
||||
isFile: () => true,
|
||||
isSymbolicLink: () => false,
|
||||
}, // Should not count as task
|
||||
])
|
||||
}
|
||||
return Promise.resolve([])
|
||||
})
|
||||
|
||||
mockStat.mockImplementation((filePath: string) => {
|
||||
if (filePath.includes("some-file.txt")) {
|
||||
return Promise.resolve({ size: 100 })
|
||||
}
|
||||
return Promise.resolve({ size: 0 })
|
||||
})
|
||||
|
||||
const result = await calculateTaskStorageSize("/global/storage")
|
||||
|
||||
// Only directories count as tasks
|
||||
expect(result.taskCount).toBe(1)
|
||||
// But file size should be included
|
||||
expect(result.totalBytes).toBe(100)
|
||||
})
|
||||
})
|
||||
138
src/utils/task-storage-size.ts
Normal file
138
src/utils/task-storage-size.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import type { Dirent, Stats } from "fs"
|
||||
|
||||
import { getStorageBasePath } from "./storage"
|
||||
|
||||
/**
|
||||
* Result of calculating task history storage size
|
||||
*/
|
||||
export interface TaskStorageSizeResult {
|
||||
/** Total size in bytes */
|
||||
totalBytes: number
|
||||
/** Number of task directories found */
|
||||
taskCount: number
|
||||
/** Formatted size string (e.g., "12.34 MB") */
|
||||
formattedSize: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats bytes into a human-readable string with appropriate units.
|
||||
* @param bytes Number of bytes
|
||||
* @returns Formatted string (e.g., "12.34 MB", "5.67 GB")
|
||||
*/
|
||||
export function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return "0 B"
|
||||
|
||||
const units = ["B", "KB", "MB", "GB", "TB"]
|
||||
const k = 1024
|
||||
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), units.length - 1)
|
||||
const size = bytes / Math.pow(k, i)
|
||||
|
||||
// Use 2 decimal places for MB and above, 0 for B and KB
|
||||
const decimals = i >= 2 ? 2 : 0
|
||||
return `${size.toFixed(decimals)} ${units[i]}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively calculates the total size of a directory.
|
||||
* @param dirPath Path to the directory
|
||||
* @returns Total size in bytes
|
||||
*/
|
||||
async function getDirectorySize(dirPath: string, depth: number = 0): Promise<number> {
|
||||
let totalSize = 0
|
||||
|
||||
// Safety check: prevent infinite recursion by limiting depth
|
||||
if (depth > 50) {
|
||||
return 0
|
||||
}
|
||||
|
||||
try {
|
||||
const entries: Dirent[] = await fs.readdir(dirPath, { withFileTypes: true })
|
||||
|
||||
// Process entries in parallel for better performance
|
||||
const sizes = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const entryPath = path.join(dirPath, entry.name)
|
||||
|
||||
try {
|
||||
// Check for symlinks to prevent infinite loops
|
||||
if (entry.isSymbolicLink()) {
|
||||
return 0
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
return await getDirectorySize(entryPath, depth + 1)
|
||||
} else if (entry.isFile()) {
|
||||
const stat: Stats = await fs.stat(entryPath)
|
||||
return stat.size
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors for individual entries (permission issues, deleted files, etc.)
|
||||
}
|
||||
|
||||
return 0
|
||||
}),
|
||||
)
|
||||
|
||||
totalSize = sizes.reduce((acc, size) => acc + size, 0)
|
||||
} catch {
|
||||
// Directory doesn't exist or can't be read
|
||||
}
|
||||
|
||||
return totalSize
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total storage size used by task history.
|
||||
* This includes all files in the tasks/ directory (task data, checkpoints, etc.).
|
||||
*
|
||||
* This function is designed to be non-blocking and safe for background execution.
|
||||
* Errors are handled gracefully and will return 0 bytes if the directory doesn't exist
|
||||
* or can't be read.
|
||||
*
|
||||
* @param globalStoragePath VS Code global storage fsPath (context.globalStorageUri.fsPath)
|
||||
* @param log Optional logger function for debugging
|
||||
* @returns TaskStorageSizeResult with size info
|
||||
*/
|
||||
export async function calculateTaskStorageSize(
|
||||
globalStoragePath: string,
|
||||
log?: (message: string) => void,
|
||||
): Promise<TaskStorageSizeResult> {
|
||||
const defaultResult: TaskStorageSizeResult = {
|
||||
totalBytes: 0,
|
||||
taskCount: 0,
|
||||
formattedSize: "0 B",
|
||||
}
|
||||
|
||||
let basePath: string
|
||||
|
||||
try {
|
||||
basePath = await getStorageBasePath(globalStoragePath)
|
||||
} catch (e) {
|
||||
log?.(`[TaskStorageSize] Failed to resolve storage base path: ${e instanceof Error ? e.message : String(e)}`)
|
||||
return defaultResult
|
||||
}
|
||||
|
||||
const tasksDir = path.join(basePath, "tasks")
|
||||
|
||||
// Count task directories
|
||||
let taskCount = 0
|
||||
try {
|
||||
const entries = await fs.readdir(tasksDir, { withFileTypes: true })
|
||||
taskCount = entries.filter((d) => d.isDirectory()).length
|
||||
} catch {
|
||||
// Tasks directory doesn't exist yet
|
||||
log?.(`[TaskStorageSize] Tasks directory not found at ${tasksDir}`)
|
||||
return defaultResult
|
||||
}
|
||||
|
||||
// Calculate total size
|
||||
const totalBytes = await getDirectorySize(tasksDir)
|
||||
|
||||
return {
|
||||
totalBytes,
|
||||
taskCount,
|
||||
formattedSize: formatBytes(totalBytes),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,19 @@
|
|||
import { HTMLAttributes } from "react"
|
||||
import { HTMLAttributes, useState, useCallback, useEffect } from "react"
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { Trans } from "react-i18next"
|
||||
import { Download, Upload, TriangleAlert, Bug, Lightbulb, Shield, MessageCircle, MessagesSquare } from "lucide-react"
|
||||
import {
|
||||
Download,
|
||||
Upload,
|
||||
TriangleAlert,
|
||||
Bug,
|
||||
Lightbulb,
|
||||
Shield,
|
||||
MessageCircle,
|
||||
MessagesSquare,
|
||||
RefreshCw,
|
||||
HardDrive,
|
||||
Loader2,
|
||||
} from "lucide-react"
|
||||
import { VSCodeCheckbox, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import type { TelemetrySetting } from "@roo-code/types"
|
||||
|
|
@ -16,6 +28,12 @@ import { SectionHeader } from "./SectionHeader"
|
|||
import { Section } from "./Section"
|
||||
import { SearchableSetting } from "./SearchableSetting"
|
||||
|
||||
type TaskHistorySize = {
|
||||
totalBytes: number
|
||||
taskCount: number
|
||||
formattedSize: string
|
||||
}
|
||||
|
||||
type AboutProps = HTMLAttributes<HTMLDivElement> & {
|
||||
telemetrySetting: TelemetrySetting
|
||||
setTelemetrySetting: (setting: TelemetrySetting) => void
|
||||
|
|
@ -23,6 +41,7 @@ type AboutProps = HTMLAttributes<HTMLDivElement> & {
|
|||
setDebug?: (debug: boolean) => void
|
||||
taskHistoryRetention: "never" | "90" | "60" | "30" | "7" | "3"
|
||||
setTaskHistoryRetention: (value: "never" | "90" | "60" | "30" | "7" | "3") => void
|
||||
taskHistorySize?: TaskHistorySize
|
||||
}
|
||||
|
||||
export const About = ({
|
||||
|
|
@ -32,10 +51,46 @@ export const About = ({
|
|||
setDebug,
|
||||
taskHistoryRetention,
|
||||
setTaskHistoryRetention,
|
||||
taskHistorySize,
|
||||
className,
|
||||
...props
|
||||
}: AboutProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
const [cachedSize, setCachedSize] = useState<TaskHistorySize | undefined>(taskHistorySize)
|
||||
|
||||
// Update cached size when taskHistorySize changes and reset refreshing state
|
||||
useEffect(() => {
|
||||
if (taskHistorySize) {
|
||||
setCachedSize(taskHistorySize)
|
||||
setIsRefreshing(false)
|
||||
}
|
||||
}, [taskHistorySize])
|
||||
|
||||
const handleRefreshStorageSize = useCallback(() => {
|
||||
setIsRefreshing(true)
|
||||
vscode.postMessage({ type: "refreshTaskHistorySize" })
|
||||
}, [])
|
||||
|
||||
const getStorageDisplayText = (): string => {
|
||||
// Use cached size if available, otherwise show "Calculating" only if no cached value
|
||||
const displaySize = taskHistorySize || cachedSize
|
||||
if (!displaySize) {
|
||||
return t("settings:taskHistoryStorage.calculating")
|
||||
}
|
||||
if (displaySize.taskCount === 0) {
|
||||
return t("settings:taskHistoryStorage.empty")
|
||||
}
|
||||
if (displaySize.taskCount === 1) {
|
||||
return t("settings:taskHistoryStorage.formatSingular", {
|
||||
size: displaySize.formattedSize,
|
||||
})
|
||||
}
|
||||
return t("settings:taskHistoryStorage.format", {
|
||||
size: displaySize.formattedSize,
|
||||
count: displaySize.taskCount,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2", className)} {...props}>
|
||||
|
|
@ -173,6 +228,32 @@ export const About = ({
|
|||
</div>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
settingId="about-task-history-storage"
|
||||
section="about"
|
||||
label={t("settings:taskHistoryStorage.label")}
|
||||
className="mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="size-4 text-vscode-descriptionForeground shrink-0" />
|
||||
<span className="text-sm">
|
||||
{t("settings:taskHistoryStorage.label")}: {getStorageDisplayText()}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRefreshStorageSize}
|
||||
disabled={isRefreshing}
|
||||
className="h-6 w-6 p-0"
|
||||
title={t("settings:taskHistoryStorage.refresh")}>
|
||||
{isRefreshing ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="size-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
settingId="about-manage-settings"
|
||||
section="about"
|
||||
|
|
|
|||
|
|
@ -216,6 +216,9 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
taskHistoryRetention,
|
||||
} = cachedState
|
||||
|
||||
// taskHistorySize is read-only (not a saveable setting) so we use extensionState directly
|
||||
const taskHistorySize = extensionState.taskHistorySize
|
||||
|
||||
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -561,6 +564,13 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
scrollToActiveTab()
|
||||
}, [activeTab, scrollToActiveTab])
|
||||
|
||||
// Effect to trigger task history size calculation when About tab is opened
|
||||
useEffect(() => {
|
||||
if (activeTab === "about") {
|
||||
vscode.postMessage({ type: "refreshTaskHistorySize" })
|
||||
}
|
||||
}, [activeTab])
|
||||
|
||||
// Effect to scroll when the webview becomes visible
|
||||
useLayoutEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
|
|
@ -962,6 +972,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
setDebug={setDebug}
|
||||
taskHistoryRetention={normalizedTaskHistoryRetention}
|
||||
setTaskHistoryRetention={(value) => setCachedStateField("taskHistoryRetention", value)}
|
||||
taskHistorySize={taskHistorySize}
|
||||
/>
|
||||
)}
|
||||
</SearchIndexProvider>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue