feat: add privacy settings for time and timezone in context

- Add includeCurrentTime and includeTimezone settings to ProviderSettings
- Update getEnvironmentDetails to respect privacy settings
- Create PrivacySettings UI component with proper controls
- Add timezone dependency on current time setting
- Include comprehensive test coverage for privacy settings
- Add translations for privacy settings UI
- Default to true for backward compatibility

Fixes #8731
This commit is contained in:
Roo Code 2025-10-19 14:08:35 +00:00
parent a8f87d2b1d
commit 286fdbba57
6 changed files with 213 additions and 8 deletions

View file

@ -173,6 +173,10 @@ const baseProviderSettingsSchema = z.object({
rateLimitSeconds: z.number().optional(),
consecutiveMistakeLimit: z.number().min(0).optional(),
// Privacy settings
includeCurrentTime: z.boolean().optional(),
includeTimezone: z.boolean().optional(),
// Model reasoning.
enableReasoningEffort: z.boolean().optional(),
reasoningEffort: reasoningEffortWithMinimalSchema.optional(),

View file

@ -390,4 +390,73 @@ describe("getEnvironmentDetails", () => {
const result = await getEnvironmentDetails(cline as Task)
expect(result).toContain("REMINDERS")
})
describe("Privacy settings for time and timezone", () => {
it("should include both time and timezone when both settings are true", async () => {
mockProvider.getState.mockResolvedValue({
...mockState,
apiConfiguration: { includeCurrentTime: true, includeTimezone: true },
})
const result = await getEnvironmentDetails(mockCline as Task)
expect(result).toContain("# Current Time")
expect(result).toContain("Current time in ISO 8601 UTC format:")
expect(result).toContain("User time zone:")
})
it("should include only time when includeCurrentTime is true but includeTimezone is false", async () => {
mockProvider.getState.mockResolvedValue({
...mockState,
apiConfiguration: { includeCurrentTime: true, includeTimezone: false },
})
const result = await getEnvironmentDetails(mockCline as Task)
expect(result).toContain("# Current Time")
expect(result).toContain("Current time in ISO 8601 UTC format:")
expect(result).not.toContain("User time zone:")
})
it("should exclude time section when includeCurrentTime is false", async () => {
mockProvider.getState.mockResolvedValue({
...mockState,
apiConfiguration: { includeCurrentTime: false, includeTimezone: true },
})
const result = await getEnvironmentDetails(mockCline as Task)
expect(result).not.toContain("# Current Time")
expect(result).not.toContain("Current time in ISO 8601 UTC format:")
expect(result).not.toContain("User time zone:")
})
it("should include both time and timezone by default when settings are undefined", async () => {
mockProvider.getState.mockResolvedValue({
...mockState,
apiConfiguration: {},
})
const result = await getEnvironmentDetails(mockCline as Task)
expect(result).toContain("# Current Time")
expect(result).toContain("Current time in ISO 8601 UTC format:")
expect(result).toContain("User time zone:")
})
it("should include both time and timezone when apiConfiguration is undefined", async () => {
mockProvider.getState.mockResolvedValue({
...mockState,
apiConfiguration: undefined,
})
const result = await getEnvironmentDetails(mockCline as Task)
expect(result).toContain("# Current Time")
expect(result).toContain("Current time in ISO 8601 UTC format:")
expect(result).toContain("User time zone:")
})
it("should handle null apiConfiguration gracefully", async () => {
mockProvider.getState.mockResolvedValue({
...mockState,
apiConfiguration: null,
})
const result = await getEnvironmentDetails(mockCline as Task)
// Should default to including both
expect(result).toContain("# Current Time")
expect(result).toContain("Current time in ISO 8601 UTC format:")
expect(result).toContain("User time zone:")
})
})
})

View file

@ -190,15 +190,24 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
details += terminalDetails
}
// Add current time information with timezone.
const now = new Date()
// Add current time information with timezone (respecting privacy settings).
// Default to true for backward compatibility
const includeCurrentTime = state?.apiConfiguration?.includeCurrentTime ?? true
const includeTimezone = state?.apiConfiguration?.includeTimezone ?? true
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
const timeZoneOffset = -now.getTimezoneOffset() / 60 // Convert to hours and invert sign to match conventional notation
const timeZoneOffsetHours = Math.floor(Math.abs(timeZoneOffset))
const timeZoneOffsetMinutes = Math.abs(Math.round((Math.abs(timeZoneOffset) - timeZoneOffsetHours) * 60))
const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : "-"}${timeZoneOffsetHours}:${timeZoneOffsetMinutes.toString().padStart(2, "0")}`
details += `\n\n# Current Time\nCurrent time in ISO 8601 UTC format: ${now.toISOString()}\nUser time zone: ${timeZone}, UTC${timeZoneOffsetStr}`
if (includeCurrentTime) {
const now = new Date()
details += `\n\n# Current Time\nCurrent time in ISO 8601 UTC format: ${now.toISOString()}`
if (includeTimezone) {
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
const timeZoneOffset = -now.getTimezoneOffset() / 60 // Convert to hours and invert sign to match conventional notation
const timeZoneOffsetHours = Math.floor(Math.abs(timeZoneOffset))
const timeZoneOffsetMinutes = Math.abs(Math.round((Math.abs(timeZoneOffset) - timeZoneOffsetHours) * 60))
const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : "-"}${timeZoneOffsetHours}:${timeZoneOffsetMinutes.toString().padStart(2, "0")}`
details += `\nUser time zone: ${timeZone}, UTC${timeZoneOffsetStr}`
}
}
// Add context tokens information.
const { contextTokens, totalCost } = getApiMetrics(cline.clineMessages)

View file

@ -0,0 +1,98 @@
import { HTMLAttributes } from "react"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
import { Shield } from "lucide-react"
import { telemetryClient } from "@/utils/TelemetryClient"
import type { ProviderSettings } from "@roo-code/types"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
interface PrivacySettingsProps extends HTMLAttributes<HTMLDivElement> {
includeCurrentTime: boolean
includeTimezone: boolean
setApiConfigurationField: <K extends keyof ProviderSettings>(
field: K,
value: ProviderSettings[K],
isUserAction?: boolean,
) => void
}
export const PrivacySettings = ({
includeCurrentTime,
includeTimezone,
setApiConfigurationField,
...props
}: PrivacySettingsProps) => {
const { t } = useAppTranslation()
const handleIncludeCurrentTimeChange = (value: boolean) => {
setApiConfigurationField("includeCurrentTime", value)
// If disabling current time, also disable timezone
if (!value) {
setApiConfigurationField("includeTimezone", false)
}
// Track telemetry event
telemetryClient.capture("privacy_settings_include_time_changed", {
enabled: value,
})
}
const handleIncludeTimezoneChange = (value: boolean) => {
setApiConfigurationField("includeTimezone", value)
// Track telemetry event
telemetryClient.capture("privacy_settings_include_timezone_changed", {
enabled: value,
})
}
return (
<div {...props}>
<SectionHeader>
<div className="flex items-center gap-2">
<Shield className="w-4" />
<div>{t("settings:sections.privacy")}</div>
</div>
</SectionHeader>
<Section>
<div className="space-y-6">
{/* Include Current Time Setting */}
<div className="flex flex-col gap-1">
<VSCodeCheckbox
checked={includeCurrentTime}
onChange={(e: any) => handleIncludeCurrentTimeChange(e.target.checked)}
data-testid="include-current-time-checkbox">
<span className="font-medium">{t("settings:privacy.includeCurrentTime.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm ml-5 mt-1">
{t("settings:privacy.includeCurrentTime.description")}
</div>
</div>
{/* Include Timezone Setting */}
<div className="flex flex-col gap-1">
<VSCodeCheckbox
checked={includeTimezone}
disabled={!includeCurrentTime}
onChange={(e: any) => handleIncludeTimezoneChange(e.target.checked)}
data-testid="include-timezone-checkbox">
<span className="font-medium">{t("settings:privacy.includeTimezone.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm ml-5 mt-1">
{t("settings:privacy.includeTimezone.description")}
</div>
{!includeCurrentTime && (
<div className="text-vscode-descriptionForeground text-sm ml-5 mt-1 italic">
{t("settings:privacy.includeTimezone.disabled")}
</div>
)}
</div>
</div>
</Section>
</div>
)
}

View file

@ -25,6 +25,7 @@ import {
LucideIcon,
SquareSlash,
Glasses,
Shield,
} from "lucide-react"
import type { ProviderSettings, ExperimentId, TelemetrySetting } from "@roo-code/types"
@ -68,6 +69,7 @@ import { Section } from "./Section"
import PromptsSettings from "./PromptsSettings"
import { SlashCommandsSettings } from "./SlashCommandsSettings"
import { UISettings } from "./UISettings"
import { PrivacySettings } from "./PrivacySettings"
export const settingsTabsContainer = "flex flex-1 overflow-hidden [&.narrow_.tab-label]:hidden"
export const settingsTabList =
@ -90,6 +92,7 @@ const sectionNames = [
"contextManagement",
"terminal",
"prompts",
"privacy",
"ui",
"experimental",
"language",
@ -478,6 +481,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
{ id: "contextManagement", icon: Database },
{ id: "terminal", icon: SquareTerminal },
{ id: "prompts", icon: MessageSquare },
{ id: "privacy", icon: Shield },
{ id: "ui", icon: Glasses },
{ id: "experimental", icon: FlaskConical },
{ id: "language", icon: Globe },
@ -778,6 +782,15 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
/>
)}
{/* Privacy Section */}
{activeTab === "privacy" && (
<PrivacySettings
includeCurrentTime={apiConfiguration?.includeCurrentTime ?? true}
includeTimezone={apiConfiguration?.includeTimezone ?? true}
setApiConfigurationField={setApiConfigurationField}
/>
)}
{/* UI Section */}
{activeTab === "ui" && (
<UISettings

View file

@ -30,6 +30,7 @@
"terminal": "Terminal",
"slashCommands": "Slash Commands",
"prompts": "Prompts",
"privacy": "Privacy",
"ui": "UI",
"experimental": "Experimental",
"language": "Language",
@ -44,6 +45,17 @@
"description": "When enabled, thinking blocks will be collapsed by default until you interact with them"
}
},
"privacy": {
"includeCurrentTime": {
"label": "Include current time in context",
"description": "When enabled, the current time will be included in the context sent to AI providers. This helps the AI understand temporal context but may reveal your time zone."
},
"includeTimezone": {
"label": "Include timezone information",
"description": "When enabled, your timezone information will be included along with the current time. This provides more accurate temporal context but reveals your geographic location.",
"disabled": "Timezone can only be included when current time is enabled."
}
},
"prompts": {
"description": "Configure support prompts that are used for quick actions like enhancing prompts, explaining code, and fixing issues. These prompts help Roo provide better assistance for common development tasks."
},