diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 5262e7602d68..798ea6a53528 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -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(), diff --git a/src/core/environment/__tests__/getEnvironmentDetails.spec.ts b/src/core/environment/__tests__/getEnvironmentDetails.spec.ts index 1110aa8831b9..7e458cfca467 100644 --- a/src/core/environment/__tests__/getEnvironmentDetails.spec.ts +++ b/src/core/environment/__tests__/getEnvironmentDetails.spec.ts @@ -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:") + }) + }) }) diff --git a/src/core/environment/getEnvironmentDetails.ts b/src/core/environment/getEnvironmentDetails.ts index c0139649ab54..82b77bb2f3a0 100644 --- a/src/core/environment/getEnvironmentDetails.ts +++ b/src/core/environment/getEnvironmentDetails.ts @@ -190,15 +190,24 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo details += terminalDetails } - // Add current time information with timezone. - const now = new Date() - - 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}` + // 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 + + 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) diff --git a/webview-ui/src/components/settings/PrivacySettings.tsx b/webview-ui/src/components/settings/PrivacySettings.tsx new file mode 100644 index 000000000000..49f4fee3c238 --- /dev/null +++ b/webview-ui/src/components/settings/PrivacySettings.tsx @@ -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 { + includeCurrentTime: boolean + includeTimezone: boolean + setApiConfigurationField: ( + 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 ( +
+ +
+ +
{t("settings:sections.privacy")}
+
+
+ +
+
+ {/* Include Current Time Setting */} +
+ handleIncludeCurrentTimeChange(e.target.checked)} + data-testid="include-current-time-checkbox"> + {t("settings:privacy.includeCurrentTime.label")} + +
+ {t("settings:privacy.includeCurrentTime.description")} +
+
+ + {/* Include Timezone Setting */} +
+ handleIncludeTimezoneChange(e.target.checked)} + data-testid="include-timezone-checkbox"> + {t("settings:privacy.includeTimezone.label")} + +
+ {t("settings:privacy.includeTimezone.description")} +
+ {!includeCurrentTime && ( +
+ {t("settings:privacy.includeTimezone.disabled")} +
+ )} +
+
+
+
+ ) +} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 93b1b39e506f..b957bda28959 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -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(({ 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(({ onDone, t /> )} + {/* Privacy Section */} + {activeTab === "privacy" && ( + + )} + {/* UI Section */} {activeTab === "ui" && (