Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
69 changes: 69 additions & 0 deletions src/core/environment/__tests__/getEnvironmentDetails.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:")
})
})
})
27 changes: 18 additions & 9 deletions src/core/environment/getEnvironmentDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
98 changes: 98 additions & 0 deletions webview-ui/src/components/settings/PrivacySettings.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
13 changes: 13 additions & 0 deletions webview-ui/src/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
LucideIcon,
SquareSlash,
Glasses,
Shield,
} from "lucide-react"

import type { ProviderSettings, ExperimentId, TelemetrySetting } from "@roo-code/types"
Expand Down Expand Up @@ -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 =
Expand All @@ -90,6 +92,7 @@ const sectionNames = [
"contextManagement",
"terminal",
"prompts",
"privacy",
"ui",
"experimental",
"language",
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions webview-ui/src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"terminal": "Terminal",
"slashCommands": "Slash Commands",
"prompts": "Prompts",
"privacy": "Privacy",
"ui": "UI",
"experimental": "Experimental",
"language": "Language",
Expand All @@ -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."
},
Expand Down
Loading