diff --git a/extensions/assistant-extension/src/index.ts b/extensions/assistant-extension/src/index.ts
index f50a4c202..2eb9eb347 100644
--- a/extensions/assistant-extension/src/index.ts
+++ b/extensions/assistant-extension/src/index.ts
@@ -75,7 +75,7 @@ export default class JanAssistantExtension extends AssistantExtension {
'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf.',
model: '*',
instructions:
- 'You are a helpful AI assistant. Your primary goal is to assist users with their questions and tasks to the best of your abilities.\n\nWhen responding:\n- Answer directly from your knowledge when you can\n- Be concise, clear, and helpful\n- Admit when you’re unsure rather than making things up\n\nIf tools are available to you:\n- Only use tools when they add real value to your response\n- Use tools when the user explicitly asks (e.g., "search for...", "calculate...", "run this code")\n- Use tools for information you don’t know or that needs verification\n- Never use tools just because they’re available\n\nWhen using tools:\n- Use one tool at a time and wait for results\n- Use actual values as arguments, not variable names\n- Learn from each result before deciding next steps\n- Avoid repeating the same tool call with identical parameters\n\nRemember: Most questions can be answered without tools. Think first whether you need them.',
+ 'You are a helpful AI assistant. Your primary goal is to assist users with their questions and tasks to the best of your abilities.\n\nWhen responding:\n- Answer directly from your knowledge when you can\n- Be concise, clear, and helpful\n- Admit when you’re unsure rather than making things up\n\nIf tools are available to you:\n- Only use tools when they add real value to your response\n- Use tools when the user explicitly asks (e.g., "search for...", "calculate...", "run this code")\n- Use tools for information you don’t know or that needs verification\n- Never use tools just because they’re available\n\nWhen using tools:\n- Use one tool at a time and wait for results\n- Use actual values as arguments, not variable names\n- Learn from each result before deciding next steps\n- Avoid repeating the same tool call with identical parameters\n\nRemember: Most questions can be answered without tools. Think first whether you need them.\n\nCurrent date: {{current_date}}',
tools: [
{
type: 'retrieval',
diff --git a/web-app/src/containers/dialogs/AddEditAssistant.tsx b/web-app/src/containers/dialogs/AddEditAssistant.tsx
index abc852810..17e5654c3 100644
--- a/web-app/src/containers/dialogs/AddEditAssistant.tsx
+++ b/web-app/src/containers/dialogs/AddEditAssistant.tsx
@@ -325,6 +325,9 @@ export default function AddEditAssistant({
className="resize-none"
rows={4}
/>
+
diff --git a/web-app/src/hooks/__tests__/useChat.instructions.test.ts b/web-app/src/hooks/__tests__/useChat.instructions.test.ts
new file mode 100644
index 000000000..b460b79ed
--- /dev/null
+++ b/web-app/src/hooks/__tests__/useChat.instructions.test.ts
@@ -0,0 +1,138 @@
+import { renderHook, act } from '@testing-library/react'
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { useChat } from '../useChat'
+
+// Use hoisted storage for our mock to avoid hoist errors
+const hoisted = vi.hoisted(() => ({
+ builderMock: vi.fn(() => ({
+ addUserMessage: vi.fn(),
+ addAssistantMessage: vi.fn(),
+ getMessages: vi.fn(() => []),
+ })),
+}))
+
+vi.mock('@/lib/messages', () => ({
+ CompletionMessagesBuilder: hoisted.builderMock,
+}))
+
+// Mock dependencies similar to existing tests, but customize assistant
+vi.mock('../../hooks/usePrompt', () => ({
+ usePrompt: vi.fn(() => ({ prompt: 'test prompt', setPrompt: vi.fn() })),
+}))
+
+vi.mock('../../hooks/useAppState', () => ({
+ useAppState: Object.assign(
+ vi.fn(() => ({
+ tools: [],
+ updateTokenSpeed: vi.fn(),
+ resetTokenSpeed: vi.fn(),
+ updateTools: vi.fn(),
+ updateStreamingContent: vi.fn(),
+ updateLoadingModel: vi.fn(),
+ setAbortController: vi.fn(),
+ })),
+ { getState: vi.fn(() => ({ tokenSpeed: { tokensPerSecond: 10 } })) }
+ ),
+}))
+
+vi.mock('../../hooks/useAssistant', () => ({
+ useAssistant: vi.fn(() => ({
+ assistants: [
+ {
+ id: 'test-assistant',
+ instructions: 'Today is {{current_date}}',
+ parameters: { stream: true },
+ },
+ ],
+ currentAssistant: {
+ id: 'test-assistant',
+ instructions: 'Today is {{current_date}}',
+ parameters: { stream: true },
+ },
+ })),
+}))
+
+vi.mock('../../hooks/useModelProvider', () => ({
+ useModelProvider: vi.fn(() => ({
+ getProviderByName: vi.fn(() => ({ provider: 'openai', models: [] })),
+ selectedModel: { id: 'test-model', capabilities: ['tools'] },
+ selectedProvider: 'openai',
+ updateProvider: vi.fn(),
+ })),
+}))
+
+vi.mock('../../hooks/useThreads', () => ({
+ useThreads: vi.fn(() => ({
+ getCurrentThread: vi.fn(() => ({ id: 'test-thread', model: { id: 'test-model', provider: 'openai' } })),
+ createThread: vi.fn(() => Promise.resolve({ id: 'test-thread', model: { id: 'test-model', provider: 'openai' } })),
+ updateThreadTimestamp: vi.fn(),
+ })),
+}))
+
+vi.mock('../../hooks/useMessages', () => ({
+ useMessages: vi.fn(() => ({ getMessages: vi.fn(() => []), addMessage: vi.fn() })),
+}))
+
+vi.mock('../../hooks/useToolApproval', () => ({
+ useToolApproval: vi.fn(() => ({ approvedTools: [], showApprovalModal: vi.fn(), allowAllMCPPermissions: false })),
+}))
+
+vi.mock('../../hooks/useModelContextApproval', () => ({
+ useContextSizeApproval: vi.fn(() => ({ showApprovalModal: vi.fn() })),
+}))
+
+vi.mock('../../hooks/useModelLoad', () => ({
+ useModelLoad: vi.fn(() => ({ setModelLoadError: vi.fn() })),
+}))
+
+vi.mock('@tanstack/react-router', () => ({
+ useRouter: vi.fn(() => ({ navigate: vi.fn() })),
+}))
+
+vi.mock('@/lib/completion', () => ({
+ emptyThreadContent: { thread_id: 'test-thread', content: '' },
+ extractToolCall: vi.fn(),
+ newUserThreadContent: vi.fn(() => ({ thread_id: 'test-thread', content: 'user message' })),
+ newAssistantThreadContent: vi.fn(() => ({ thread_id: 'test-thread', content: 'assistant message' })),
+ sendCompletion: vi.fn(() => Promise.resolve({ choices: [{ message: { content: '' } }] })),
+ postMessageProcessing: vi.fn(),
+ isCompletionResponse: vi.fn(() => true),
+}))
+
+vi.mock('@/services/mcp', () => ({ getTools: vi.fn(() => Promise.resolve([])) }))
+
+vi.mock('@/services/models', () => ({
+ startModel: vi.fn(() => Promise.resolve()),
+ stopModel: vi.fn(() => Promise.resolve()),
+ stopAllModels: vi.fn(() => Promise.resolve()),
+}))
+
+vi.mock('@/services/providers', () => ({ updateSettings: vi.fn(() => Promise.resolve()) }))
+
+vi.mock('@tauri-apps/api/event', () => ({ listen: vi.fn(() => Promise.resolve(vi.fn())) }))
+
+describe('useChat instruction rendering', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('renders assistant instructions by replacing {{current_date}} with today', async () => {
+ vi.useFakeTimers()
+ vi.setSystemTime(new Date('2025-08-16T00:00:00Z'))
+
+ const { result } = renderHook(() => useChat())
+
+ await act(async () => {
+ await result.current.sendMessage('Hello')
+ })
+
+ expect(hoisted.builderMock).toHaveBeenCalled()
+ const calls = (hoisted.builderMock as any).mock.calls as any[]
+ const call = calls[0]
+ expect(call[0]).toEqual([])
+ expect(call[1]).toMatch(/^Today is /)
+ expect(call[1]).not.toContain('{{current_date}}')
+
+ vi.useRealTimers()
+ })
+})
diff --git a/web-app/src/hooks/useAssistant.ts b/web-app/src/hooks/useAssistant.ts
index 51cb1f97f..d878607e1 100644
--- a/web-app/src/hooks/useAssistant.ts
+++ b/web-app/src/hooks/useAssistant.ts
@@ -43,7 +43,7 @@ export const defaultAssistant: Assistant = {
description:
'Jan is a helpful desktop assistant that can reason through complex tasks and use tools to complete them on the user’s behalf.',
instructions:
- 'You are a helpful AI assistant. Your primary goal is to assist users with their questions and tasks to the best of your abilities.\n\nWhen responding:\n- Answer directly from your knowledge when you can\n- Be concise, clear, and helpful\n- Admit when you’re unsure rather than making things up\n\nIf tools are available to you:\n- Only use tools when they add real value to your response\n- Use tools when the user explicitly asks (e.g., "search for...", "calculate...", "run this code")\n- Use tools for information you don’t know or that needs verification\n- Never use tools just because they’re available\n\nWhen using tools:\n- Use one tool at a time and wait for results\n- Use actual values as arguments, not variable names\n- Learn from each result before deciding next steps\n- Avoid repeating the same tool call with identical parameters\n\nRemember: Most questions can be answered without tools. Think first whether you need them.',
+ 'You are a helpful AI assistant. Your primary goal is to assist users with their questions and tasks to the best of your abilities.\n\nWhen responding:\n- Answer directly from your knowledge when you can\n- Be concise, clear, and helpful\n- Admit when you’re unsure rather than making things up\n\nIf tools are available to you:\n- Only use tools when they add real value to your response\n- Use tools when the user explicitly asks (e.g., "search for...", "calculate...", "run this code")\n- Use tools for information you don’t know or that needs verification\n- Never use tools just because they’re available\n\nWhen using tools:\n- Use one tool at a time and wait for results\n- Use actual values as arguments, not variable names\n- Learn from each result before deciding next steps\n- Avoid repeating the same tool call with identical parameters\n\nRemember: Most questions can be answered without tools. Think first whether you need them.\n\nCurrent date: {{current_date}}',
}
export const useAssistant = create
()((set, get) => ({
diff --git a/web-app/src/hooks/useChat.ts b/web-app/src/hooks/useChat.ts
index fd85797fb..e1511ee51 100644
--- a/web-app/src/hooks/useChat.ts
+++ b/web-app/src/hooks/useChat.ts
@@ -17,6 +17,7 @@ import {
sendCompletion,
} from '@/lib/completion'
import { CompletionMessagesBuilder } from '@/lib/messages'
+import { renderInstructions } from '@/lib/instructionTemplate'
import { ChatCompletionMessageToolCall } from 'openai/resources'
import { useAssistant } from './useAssistant'
@@ -239,7 +240,7 @@ export const useChat = () => {
const builder = new CompletionMessagesBuilder(
messages,
- currentAssistant?.instructions
+ renderInstructions(currentAssistant?.instructions)
)
if (troubleshooting) builder.addUserMessage(message, attachments)
diff --git a/web-app/src/lib/__tests__/instructionTemplate.test.ts b/web-app/src/lib/__tests__/instructionTemplate.test.ts
new file mode 100644
index 000000000..26297092e
--- /dev/null
+++ b/web-app/src/lib/__tests__/instructionTemplate.test.ts
@@ -0,0 +1,27 @@
+import { describe, it, expect, vi } from 'vitest'
+import { renderInstructions } from '../instructionTemplate'
+
+describe('renderInstructions', () => {
+ it('replaces {{current_date}} with today when no params provided', () => {
+ vi.useFakeTimers()
+ vi.setSystemTime(new Date('2025-08-16T00:00:00Z'))
+
+ const input = 'Today is {{current_date}}.'
+ const out = renderInstructions(input)
+
+ expect(out).not.toBe(input)
+ expect(out).toMatch(/^Today is /)
+ expect(out).not.toContain('{{current_date}}')
+
+ vi.useRealTimers()
+ })
+
+ it('replaces multiple occurrences of {{current_date}}', () => {
+ const input = 'A {{current_date}} B {{current_date}} C'
+ const out = renderInstructions(input)
+ expect(out).not.toContain('{{current_date}}')
+ expect(out.startsWith('A ')).toBe(true)
+ expect(out.includes(' B ')).toBe(true)
+ expect(out.endsWith(' C')).toBe(true)
+ })
+})
diff --git a/web-app/src/lib/instructionTemplate.ts b/web-app/src/lib/instructionTemplate.ts
new file mode 100644
index 000000000..63a9b63a7
--- /dev/null
+++ b/web-app/src/lib/instructionTemplate.ts
@@ -0,0 +1,23 @@
+import { formatDate } from '@/utils/formatDate'
+
+/**
+ * Render assistant instructions by replacing supported placeholders.
+ * Supported placeholders:
+ * - {{current_date}}: Inserts today’s date (UTC, long month), e.g., August 16, 2025.
+ */
+export function renderInstructions(instructions: string): string
+export function renderInstructions(
+ instructions?: string
+): string | undefined
+export function renderInstructions(
+ instructions?: string
+): string | undefined {
+ if (!instructions) return instructions
+
+ const currentDateStr = formatDate(new Date(), { includeTime: false })
+
+ // Replace current_date (allow spaces inside braces).
+ let rendered = instructions
+ rendered = rendered.replace(/\{\{\s*current_date\s*\}\}/gi, currentDateStr)
+ return rendered
+}
diff --git a/web-app/src/locales/de-DE/assistants.json b/web-app/src/locales/de-DE/assistants.json
index d1c5ce749..2b191ca59 100644
--- a/web-app/src/locales/de-DE/assistants.json
+++ b/web-app/src/locales/de-DE/assistants.json
@@ -30,5 +30,6 @@
"createNew": "Neuen Assistenten anlegen",
"personality": "Persönlichkeit",
"capabilities": "Fähigkeiten",
+ "instructionsDateHint": "Tipp: Verwenden Sie {{current_date}}, um das heutige Datum einzufügen.",
"maxToolSteps": "Maximale Werkzeugschritte"
}
diff --git a/web-app/src/locales/en/assistants.json b/web-app/src/locales/en/assistants.json
index 8f3f6ee14..174d9dec7 100644
--- a/web-app/src/locales/en/assistants.json
+++ b/web-app/src/locales/en/assistants.json
@@ -30,5 +30,6 @@
"createNew": "Create New Assistant",
"personality": "Personality",
"capabilities": "Capabilities",
+ "instructionsDateHint": "Tip: Use {{current_date}} to insert today’s date.",
"maxToolSteps": "Max tool steps"
}
diff --git a/web-app/src/locales/id/assistants.json b/web-app/src/locales/id/assistants.json
index 233aeff54..64fd680ff 100644
--- a/web-app/src/locales/id/assistants.json
+++ b/web-app/src/locales/id/assistants.json
@@ -30,5 +30,6 @@
"createNew": "Buat Asisten Baru",
"personality": "Kepribadian",
"capabilities": "Kemampuan",
+ "instructionsDateHint": "Tips: Gunakan {{current_date}} untuk menyisipkan tanggal hari ini.",
"maxToolSteps": "Langkah alat maksimum"
}
diff --git a/web-app/src/locales/vn/assistants.json b/web-app/src/locales/vn/assistants.json
index ead588649..c4f2e6160 100644
--- a/web-app/src/locales/vn/assistants.json
+++ b/web-app/src/locales/vn/assistants.json
@@ -30,5 +30,6 @@
"createNew": "Tạo Trợ lý Mới",
"personality": "Tính cách",
"capabilities": "Khả năng",
+ "instructionsDateHint": "Mẹo: Dùng {{current_date}} để chèn ngày hôm nay.",
"maxToolSteps": "Bước tối đa của công cụ"
}
diff --git a/web-app/src/locales/zh-CN/assistants.json b/web-app/src/locales/zh-CN/assistants.json
index 57648c92e..520fc584e 100644
--- a/web-app/src/locales/zh-CN/assistants.json
+++ b/web-app/src/locales/zh-CN/assistants.json
@@ -30,5 +30,6 @@
"createNew": "创建新助手",
"personality": "个性",
"capabilities": "能力",
+ "instructionsDateHint": "提示:使用 {{current_date}} 插入今天的日期。",
"maxToolSteps": "最大工具步骤"
}
diff --git a/web-app/src/locales/zh-TW/assistants.json b/web-app/src/locales/zh-TW/assistants.json
index 4eaf764df..6c7d19696 100644
--- a/web-app/src/locales/zh-TW/assistants.json
+++ b/web-app/src/locales/zh-TW/assistants.json
@@ -30,5 +30,6 @@
"createNew": "建立新助理",
"personality": "個性",
"capabilities": "能力",
+ "instructionsDateHint": "提示:使用 {{current_date}} 插入今天的日期。",
"maxToolSteps": "最大工具步驟"
}
diff --git a/web-app/src/utils/__tests__/formatDate.test.ts b/web-app/src/utils/__tests__/formatDate.test.ts
index 1c36b1846..296d95da6 100644
--- a/web-app/src/utils/__tests__/formatDate.test.ts
+++ b/web-app/src/utils/__tests__/formatDate.test.ts
@@ -38,7 +38,7 @@ describe('formatDate', () => {
'2023-12-01T12:00:00Z'
]
- const formatted = dates.map(formatDate)
+ const formatted = dates.map((d) => formatDate(d))
expect(formatted[0]).toMatch(/Jan.*1.*2023/i)
expect(formatted[1]).toMatch(/Feb.*1.*2023/i)
@@ -81,4 +81,23 @@ describe('formatDate', () => {
// Should include abbreviated month name
expect(formatted).toMatch(/Jul/i)
})
+
+ it('supports date-only formatting when includeTime=false', () => {
+ const date = '2023-07-04T12:00:00Z'
+ const formatted = formatDate(date, { includeTime: false })
+
+ // Long month, no time
+ expect(formatted).toMatch(/July.*4.*2023/i)
+ expect(formatted).not.toMatch(/\d{1,2}:\d{2}/i)
+ expect(formatted).not.toMatch(/(AM|PM)/i)
+ })
+
+ it('date-only formatting includes a year and omits time across edge cases', () => {
+ const oldDate = '1900-01-01T00:00:00Z'
+ const formatted = formatDate(oldDate, { includeTime: false })
+
+ expect(formatted).toMatch(/\d{4}/)
+ expect(formatted).not.toMatch(/\d{1,2}:\d{2}/i)
+ expect(formatted).not.toMatch(/(AM|PM)/i)
+ })
})
\ No newline at end of file
diff --git a/web-app/src/utils/formatDate.ts b/web-app/src/utils/formatDate.ts
index 5f6547e07..c77f20097 100644
--- a/web-app/src/utils/formatDate.ts
+++ b/web-app/src/utils/formatDate.ts
@@ -1,10 +1,34 @@
-export const formatDate = (date: string | number | Date): string => {
- return new Date(date).toLocaleString('en-US', {
+type FormatDateOptions = {
+ includeTime?: boolean
+}
+
+export const formatDate = (
+ date: string | number | Date,
+ options?: FormatDateOptions
+): string => {
+ const includeTime = options?.includeTime ?? true
+
+ // Base options shared across both modes
+ const base: Intl.DateTimeFormatOptions = {
year: 'numeric',
- month: 'short',
day: 'numeric',
- hour: 'numeric',
- minute: 'numeric',
- hour12: true,
+ }
+
+ if (includeTime) {
+ // Time mode: short month + time, fixed UTC for stable output in tests
+ return new Date(date).toLocaleString('en-US', {
+ ...base,
+ month: 'short',
+ hour: 'numeric',
+ minute: 'numeric',
+ hour12: true,
+ timeZone: 'UTC',
+ })
+ }
+
+ // Date-only mode: long month, no timezone adjustment
+ return new Date(date).toLocaleDateString('en-US', {
+ ...base,
+ month: 'long',
})
}