diff --git a/package.json b/package.json index 89a00915e9..901d846055 100644 --- a/package.json +++ b/package.json @@ -4271,6 +4271,43 @@ "experimental" ] }, + "github.copilot.chat.advanced.context.timeFormat": { + "type": "string", + "default": "off", + "enum": [ + "off", + "24h", + "12h" + ], + "enumDescriptions": [ + "Do not include current time", + "24-hour format (e.g., 23:06:32)", + "12-hour format (e.g., 11:06:32 PM)" + ], + "markdownDescription": "%github.copilot.config.advanced.context.timeFormat%", + "tags": [ + "advanced", + "experimental" + ] + }, + "github.copilot.chat.advanced.context.showWeekday": { + "type": "boolean", + "default": false, + "markdownDescription": "%github.copilot.config.advanced.context.showWeekday%", + "tags": [ + "advanced", + "experimental" + ] + }, + "github.copilot.chat.advanced.context.showTimezone": { + "type": "boolean", + "default": false, + "markdownDescription": "%github.copilot.config.advanced.context.showTimezone%", + "tags": [ + "advanced", + "experimental" + ] + }, "github.copilot.chat.agent.omitFileAttachmentContents": { "type": "boolean", "default": false, diff --git a/package.nls.json b/package.nls.json index 3e6198dd14..a4f8db912b 100644 --- a/package.nls.json +++ b/package.nls.json @@ -383,6 +383,9 @@ "github.copilot.config.inlineEdits.chatSessionContextProvider.enabled": "Enable chat session context provider for next edit suggestions.", "github.copilot.config.codesearch.agent.enabled": "Enable code search capabilities in agent mode.", "github.copilot.config.agent.temperature": "Temperature setting for agent mode requests.", + "github.copilot.config.advanced.context.timeFormat": "Include the current time in the date context provided to the model. Choose between 24-hour and 12-hour format, or disable.", + "github.copilot.config.advanced.context.showWeekday": "Include the day of the week in the date context provided to the model.", + "github.copilot.config.advanced.context.showTimezone": "Include the timezone offset (e.g., GMT+2) in the date context provided to the model. Only applies when `#github.copilot.chat.advanced.context.timeFormat#` is not \"off\".", "github.copilot.config.agent.omitFileAttachmentContents": "Omit summarized file contents from file attachments in agent mode, to encourage the agent to properly read and explore.", "github.copilot.config.agent.largeToolResultsToDisk.enabled": "When enabled, large tool results are written to disk instead of being included directly in the context, helping manage context window usage.", "github.copilot.config.agent.largeToolResultsToDisk.thresholdBytes": "The size threshold in bytes above which tool results are written to disk. Only applies when largeToolResultsToDisk.enabled is true.", diff --git a/src/extension/prompts/common/currentDateContext.ts b/src/extension/prompts/common/currentDateContext.ts new file mode 100644 index 0000000000..555656b6a6 --- /dev/null +++ b/src/extension/prompts/common/currentDateContext.ts @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; + +/** + * Formats the current date/time context string for inclusion in prompts. + * Respects user settings for time, weekday, and timezone display. + * + * Default (all settings off): "The current date is May 3, 2026." + * With all settings on: "The current date is Sunday, May 3, 2026. The current time is 23:06:32 GMT+2." + * Timezone is always in GMT±N offset format for consistency across all regions. + * + * timeFormat values: "off" (default), "24h", "12h" + */ +export function formatCurrentDateContext(configurationService: IConfigurationService): string { + const now = new Date(); + const parts: string[] = []; + + parts.push('The current date is '); + + if (configurationService.getConfig(ConfigKey.Advanced.ShowWeekday)) { + parts.push(now.toLocaleDateString('en-US', { weekday: 'long' })); + parts.push(', '); + } + + parts.push(now.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })); + parts.push('.'); + + const timeFormat = configurationService.getConfig(ConfigKey.Advanced.TimeFormat); + if (timeFormat && timeFormat !== 'off') { + const hour12 = timeFormat === '12h'; + const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12 }); + parts.push(` The current time is ${timeStr}`); + + if (configurationService.getConfig(ConfigKey.Advanced.ShowTimezone)) { + const tz = new Intl.DateTimeFormat('en-US', { timeZoneName: 'shortOffset' }).formatToParts(now).find(p => p.type === 'timeZoneName')?.value; + if (tz) { + parts.push(` ${tz}`); + } + } + + parts.push('.'); + } + + return parts.join(''); +} diff --git a/src/extension/prompts/common/test/currentDateContext.spec.ts b/src/extension/prompts/common/test/currentDateContext.spec.ts new file mode 100644 index 0000000000..6afc02571a --- /dev/null +++ b/src/extension/prompts/common/test/currentDateContext.spec.ts @@ -0,0 +1,181 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { formatCurrentDateContext } from '../currentDateContext'; +import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService'; + +function createMockConfigService(overrides: { timeFormat?: string; showWeekday?: boolean; showTimezone?: boolean } = {}): IConfigurationService { + return { + _serviceBrand: undefined, + getConfig(key: unknown) { + if (key === ConfigKey.Advanced.TimeFormat) { return overrides.timeFormat ?? 'off'; } + if (key === ConfigKey.Advanced.ShowWeekday) { return overrides.showWeekday ?? false; } + if (key === ConfigKey.Advanced.ShowTimezone) { return overrides.showTimezone ?? false; } + return undefined; + }, + } as unknown as IConfigurationService; +} + +describe('formatCurrentDateContext', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.stubEnv('TZ', 'UTC'); + // Monday, June 15, 2026 14:30:45 UTC + vi.setSystemTime(new Date('2026-06-15T14:30:45.000Z')); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllEnvs(); + }); + + it('returns date only by default (all settings off)', () => { + const result = formatCurrentDateContext(createMockConfigService()); + expect(result).toBe('The current date is June 15, 2026.'); + }); + + it('includes time in 24h format', () => { + const result = formatCurrentDateContext(createMockConfigService({ timeFormat: '24h' })); + expect(result).toBe('The current date is June 15, 2026. The current time is 14:30:45.'); + }); + + it('includes time in 12h format', () => { + const result = formatCurrentDateContext(createMockConfigService({ timeFormat: '12h' })); + expect(result).toBe('The current date is June 15, 2026. The current time is 02:30:45 PM.'); + }); + + it('includes weekday when showWeekday is enabled', () => { + const result = formatCurrentDateContext(createMockConfigService({ showWeekday: true })); + expect(result).toBe('The current date is Monday, June 15, 2026.'); + }); + + it('includes timezone when both time and showTimezone are enabled', () => { + const result = formatCurrentDateContext(createMockConfigService({ timeFormat: '24h', showTimezone: true })); + expect(result).toBe('The current date is June 15, 2026. The current time is 14:30:45 GMT+0.'); + }); + + it('does not include timezone when time is off', () => { + const result = formatCurrentDateContext(createMockConfigService({ showTimezone: true })); + expect(result).toBe('The current date is June 15, 2026.'); + }); + + it('includes all parts when all settings are enabled (24h)', () => { + const result = formatCurrentDateContext(createMockConfigService({ timeFormat: '24h', showWeekday: true, showTimezone: true })); + expect(result).toBe('The current date is Monday, June 15, 2026. The current time is 14:30:45 GMT+0.'); + }); + + it('includes all parts when all settings are enabled (12h)', () => { + const result = formatCurrentDateContext(createMockConfigService({ timeFormat: '12h', showWeekday: true, showTimezone: true })); + expect(result).toBe('The current date is Monday, June 15, 2026. The current time is 02:30:45 PM GMT+0.'); + }); + + it('weekday + time without timezone', () => { + const result = formatCurrentDateContext(createMockConfigService({ timeFormat: '24h', showWeekday: true })); + expect(result).toBe('The current date is Monday, June 15, 2026. The current time is 14:30:45.'); + }); + + it('treats unknown timeFormat values as 24h', () => { + const result = formatCurrentDateContext(createMockConfigService({ timeFormat: 'garbage' as any })); + expect(result).toBe('The current date is June 15, 2026. The current time is 14:30:45.'); + }); +}); + +describe('formatCurrentDateContext (US Eastern timezone)', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.stubEnv('TZ', 'America/New_York'); + vi.setSystemTime(new Date('2026-06-15T14:30:45.000Z')); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllEnvs(); + }); + + it('shows local time and GMT-4 timezone', () => { + const result = formatCurrentDateContext(createMockConfigService({ timeFormat: '24h', showTimezone: true })); + expect(result).toBe('The current date is June 15, 2026. The current time is 10:30:45 GMT-4.'); + }); + + it('shows all parts in Eastern timezone', () => { + const result = formatCurrentDateContext(createMockConfigService({ timeFormat: '12h', showWeekday: true, showTimezone: true })); + expect(result).toBe('The current date is Monday, June 15, 2026. The current time is 10:30:45 AM GMT-4.'); + }); +}); + +describe('formatCurrentDateContext (Tokyo timezone)', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.stubEnv('TZ', 'Asia/Tokyo'); + vi.setSystemTime(new Date('2026-06-15T14:30:45.000Z')); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllEnvs(); + }); + + it('shows local time in Tokyo timezone', () => { + const result = formatCurrentDateContext(createMockConfigService({ timeFormat: '24h', showTimezone: true })); + expect(result).toBe('The current date is June 15, 2026. The current time is 23:30:45 GMT+9.'); + }); +}); + +describe('formatCurrentDateContext (US Pacific winter — PST)', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.stubEnv('TZ', 'America/Los_Angeles'); + // January = PST (no daylight saving) + vi.setSystemTime(new Date('2026-01-15T20:30:45.000Z')); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllEnvs(); + }); + + it('shows GMT-8 in LA winter', () => { + const result = formatCurrentDateContext(createMockConfigService({ timeFormat: '24h', showTimezone: true })); + expect(result).toBe('The current date is January 15, 2026. The current time is 12:30:45 GMT-8.'); + }); +}); + +describe('formatCurrentDateContext (India — non-whole-hour offset)', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.stubEnv('TZ', 'Asia/Kolkata'); + vi.setSystemTime(new Date('2026-06-15T14:30:45.000Z')); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllEnvs(); + }); + + it('shows GMT+5:30 offset', () => { + const result = formatCurrentDateContext(createMockConfigService({ timeFormat: '24h', showTimezone: true })); + expect(result).toBe('The current date is June 15, 2026. The current time is 20:00:45 GMT+5:30.'); + }); +}); + +describe('formatCurrentDateContext (date rollover — UTC night → Tokyo next day)', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.stubEnv('TZ', 'Asia/Tokyo'); + // UTC Sunday 23:30 → Tokyo Monday 08:30, date rolls to June 16 + vi.setSystemTime(new Date('2026-06-15T23:30:45.000Z')); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.unstubAllEnvs(); + }); + + it('shows correct local date and weekday after rollover', () => { + const result = formatCurrentDateContext(createMockConfigService({ timeFormat: '24h', showWeekday: true, showTimezone: true })); + expect(result).toBe('The current date is Tuesday, June 16, 2026. The current time is 08:30:45 GMT+9.'); + }); +}); diff --git a/src/extension/prompts/node/agent/agentPrompt.tsx b/src/extension/prompts/node/agent/agentPrompt.tsx index 30da7e78ab..7fe09db77e 100644 --- a/src/extension/prompts/node/agent/agentPrompt.tsx +++ b/src/extension/prompts/node/agent/agentPrompt.tsx @@ -8,6 +8,7 @@ import type { ChatRequestEditedFileEvent, LanguageModelToolInformation, Notebook import { sessionResourceToId } from '../../../../platform/chat/common/chatDebugFileLoggerService'; import { ChatLocation } from '../../../../platform/chat/common/commonTypes'; import { ConfigKey, IConfigurationService } from '../../../../platform/configuration/common/configurationService'; +import { formatCurrentDateContext } from '../../common/currentDateContext'; import { ICustomInstructionsService } from '../../../../platform/customInstructions/common/customInstructionsService'; import { USE_SKILL_ADHERENCE_PROMPT_SETTING } from '../../../../platform/customInstructions/common/promptTypes'; import { CacheType } from '../../../../platform/endpoint/common/endpointTypes'; @@ -492,15 +493,15 @@ class UserOSPrompt extends PromptElement { class CurrentDatePrompt extends PromptElement { constructor( props: BasePromptElementProps, - @IEnvService private readonly envService: IEnvService) { + @IEnvService private readonly envService: IEnvService, + @IConfigurationService private readonly configurationService: IConfigurationService) { super(props); } async render(state: void, sizing: PromptSizing) { - const dateStr = new Date().toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }); // Only include current date when not running simulations, since if we generate cache entries with the current date, the cache will be invalidated every day return ( - !this.envService.isSimulation() && <>The current date is {dateStr}. + !this.envService.isSimulation() && <>{formatCurrentDateContext(this.configurationService)} ); } } diff --git a/src/platform/configuration/common/configurationService.ts b/src/platform/configuration/common/configurationService.ts index 07733065b1..f55d0f29a4 100644 --- a/src/platform/configuration/common/configurationService.ts +++ b/src/platform/configuration/common/configurationService.ts @@ -604,6 +604,9 @@ export namespace ConfigKey { export const EditRecordingEnabled = defineAndMigrateSetting('chat.advanced.editRecording.enabled', 'chat.editRecording.enabled', false); export const CodeSearchAgentEnabled = defineAndMigrateSetting('chat.advanced.codesearch.agent.enabled', 'chat.codesearch.agent.enabled', true); export const AgentTemperature = defineAndMigrateSetting('chat.advanced.agent.temperature', 'chat.agent.temperature', undefined); + export const TimeFormat = defineSetting<'off' | '24h' | '12h'>('chat.advanced.context.timeFormat', ConfigType.Simple, 'off'); + export const ShowWeekday = defineSetting('chat.advanced.context.showWeekday', ConfigType.Simple, false); + export const ShowTimezone = defineSetting('chat.advanced.context.showTimezone', ConfigType.Simple, false); export const EnableUserPreferences = defineAndMigrateSetting('chat.advanced.enableUserPreferences', 'chat.enableUserPreferences', false); export const SummarizeAgentConversationHistoryThreshold = defineAndMigrateSetting('chat.advanced.summarizeAgentConversationHistoryThreshold', 'chat.summarizeAgentConversationHistoryThreshold', undefined); export const AgentHistorySummarizationMode = defineAndMigrateSetting('chat.advanced.agentHistorySummarizationMode', 'chat.agentHistorySummarizationMode', undefined);