diff --git a/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx b/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx
index def077c3..10d0b2b3 100644
--- a/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx
+++ b/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx
@@ -18,17 +18,23 @@ import { CommandAutocomplete, type CommandAutocompleteHandle, type CommandInfo }
import { FileMentionAutocomplete, type FileMentionHandle } from '@/components/chat/FileMentionAutocomplete';
import { SnippetAutocomplete, type SnippetAutocompleteHandle } from '@/components/chat/SnippetAutocomplete';
import { Icon } from "@/components/icon/Icon";
+import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { useConfigStore } from '@/stores/useConfigStore';
import { useUIStore } from '@/stores/useUIStore';
import type { ScheduledTask } from '@/lib/scheduledTasksApi';
import { useI18n } from '@/lib/i18n';
import { isValidCronExpression, getNextRuns, CRON_EXAMPLES } from '@/lib/cron';
+import { canonicalizeTimezone } from '@/lib/timezones';
const WEEKDAY_INDEXES = [0, 1, 2, 3, 4, 5, 6] as const;
+// Mirrors the composer's footer icon toggles (PermissionAutoAcceptButton /
+// SessionGoalButton) so the state reads the same in both places.
+const EDITOR_TOGGLE_BUTTON_CLASS = 'flex h-6 w-6 cursor-pointer items-center justify-center text-foreground transition-none outline-none focus:outline-none flex-shrink-0';
+
const TIMEZONE_OPTIONS = (() => {
if (typeof Intl !== 'undefined' && typeof Intl.supportedValuesOf === 'function') {
- return Intl.supportedValuesOf('timeZone');
+ return [...new Set(Intl.supportedValuesOf('timeZone').map(canonicalizeTimezone))];
}
return [
'UTC',
@@ -470,6 +476,7 @@ type ScheduledTaskDraft = {
agent: string;
goalEnabled: boolean;
goalTokenBudget: number | null;
+ permissionAutoAccept: boolean;
};
state?: ScheduledTask['state'];
};
@@ -499,7 +506,7 @@ const toDraft = (
agent: string;
},
): ScheduledTaskDraft => {
- const timezoneFallback = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
+ const timezoneFallback = canonicalizeTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC');
if (!task) {
return {
name: '',
@@ -521,6 +528,7 @@ const toDraft = (
agent: defaults.agent,
goalEnabled: false,
goalTokenBudget: null,
+ permissionAutoAccept: false,
},
};
}
@@ -543,7 +551,7 @@ const toDraft = (
? task.schedule.time
: '09:00',
weekdays: Array.isArray(task.schedule.weekdays) ? task.schedule.weekdays : [1],
- timezone: task.schedule.timezone || timezoneFallback,
+ timezone: canonicalizeTimezone(task.schedule.timezone || timezoneFallback),
cronExpression: task.schedule.kind === 'cron' && typeof task.schedule.cron === 'string'
? task.schedule.cron
: '',
@@ -558,6 +566,7 @@ const toDraft = (
goalTokenBudget: typeof task.execution.goalTokenBudget === 'number' && task.execution.goalTokenBudget > 0
? task.execution.goalTokenBudget
: null,
+ permissionAutoAccept: task.execution.permissionAutoAccept === true,
},
state: task.state,
};
@@ -1163,6 +1172,7 @@ export function ScheduledTaskEditorDialog(props: {
modelID: draft.execution.modelID,
...(draft.execution.variant.trim() ? { variant: draft.execution.variant.trim() } : {}),
...(draft.execution.agent.trim() ? { agent: draft.execution.agent.trim() } : {}),
+ ...(draft.execution.permissionAutoAccept ? { permissionAutoAccept: true } : {}),
...(draft.execution.goalEnabled ? { goalEnabled: true } : {}),
...(draft.execution.goalEnabled && draft.execution.goalTokenBudget
? { goalTokenBudget: draft.execution.goalTokenBudget }
@@ -1187,7 +1197,7 @@ export function ScheduledTaskEditorDialog(props: {
if (typeof document === 'undefined') return false;
return Boolean(
document.querySelector(
- '[data-slot="dropdown-menu-content"], [data-slot="select-content"]'
+ '[data-slot="dropdown-menu-content"][data-open], [data-slot="select-content"][data-open]'
)
);
}, []);
@@ -1623,19 +1633,8 @@ export function ScheduledTaskEditorDialog(props: {
+ {draft.execution.goalEnabled ? (
-
- {draft.execution.goalEnabled ? (
- ) : null}
- {draft.execution.goalEnabled && draft.execution.goalTokenBudget !== null ? (
+ {draft.execution.goalTokenBudget !== null ? (
{
@@ -1665,6 +1663,7 @@ export function ScheduledTaskEditorDialog(props: {
/>
) : null}
+ ) : null}
);
@@ -1680,6 +1679,48 @@ export function ScheduledTaskEditorDialog(props: {
+
+
+
+
+ {t('sessions.scheduledTasks.editor.permissionAutoAccept.label')}
+
+
+
+
+
+ {t('sessions.scheduledTasks.editor.goal.label')}
+
diff --git a/packages/ui/src/components/session/ScheduledTasksDialog.tsx b/packages/ui/src/components/session/ScheduledTasksDialog.tsx
index 80589d66..0b1a0702 100644
--- a/packages/ui/src/components/session/ScheduledTasksDialog.tsx
+++ b/packages/ui/src/components/session/ScheduledTasksDialog.tsx
@@ -34,6 +34,7 @@ import {
type ScheduledTaskStatus,
} from '@/lib/scheduledTasksApi';
import { ScheduledTaskEditorDialog } from './ScheduledTaskEditorDialog';
+import { canonicalizeTimezone } from '@/lib/timezones';
const scheduleTimes = (task: ScheduledTask): string[] => {
const raw = Array.isArray(task.schedule.times)
@@ -59,7 +60,7 @@ const formatSchedule = (task: ScheduledTask, t: ReturnType['t'])
if (task.schedule.timezone) {
return t('sessions.scheduledTasks.dialog.schedule.dailyWithTimezone', {
time: timesLabel,
- timezone: task.schedule.timezone,
+ timezone: canonicalizeTimezone(task.schedule.timezone),
});
}
return t('sessions.scheduledTasks.dialog.schedule.daily', { time: timesLabel });
@@ -72,7 +73,7 @@ const formatSchedule = (task: ScheduledTask, t: ReturnType['t'])
return t('sessions.scheduledTasks.dialog.schedule.weeklyWithTimezone', {
days,
time: timesLabel,
- timezone: task.schedule.timezone,
+ timezone: canonicalizeTimezone(task.schedule.timezone),
});
}
return t('sessions.scheduledTasks.dialog.schedule.weekly', { days, time: timesLabel });
@@ -88,7 +89,7 @@ const formatSchedule = (task: ScheduledTask, t: ReturnType['t'])
return t('sessions.scheduledTasks.dialog.schedule.onceWithTimezone', {
date,
time,
- timezone: task.schedule.timezone,
+ timezone: canonicalizeTimezone(task.schedule.timezone),
});
}
return t('sessions.scheduledTasks.dialog.schedule.once', { date, time });
@@ -96,7 +97,7 @@ const formatSchedule = (task: ScheduledTask, t: ReturnType['t'])
if (task.schedule.timezone) {
return t('sessions.scheduledTasks.dialog.schedule.cronWithTimezone', {
cron: task.schedule.cron || '',
- timezone: task.schedule.timezone,
+ timezone: canonicalizeTimezone(task.schedule.timezone),
});
}
return t('sessions.scheduledTasks.dialog.schedule.cron', { cron: task.schedule.cron || '' });
diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts
index f0bac895..00dd6225 100644
--- a/packages/ui/src/lib/i18n/messages/en.ts
+++ b/packages/ui/src/lib/i18n/messages/en.ts
@@ -296,6 +296,8 @@ export const dict = {
'sessions.scheduledTasks.editor.goal.aria': 'Run this task as a goal the agent pursues to completion',
'sessions.scheduledTasks.editor.goal.budgetLabel': 'Token budget',
'sessions.scheduledTasks.editor.goal.budgetAria': 'Limit the goal to a token budget',
+ 'sessions.scheduledTasks.editor.permissionAutoAccept.label': 'Auto-accept permissions',
+ 'sessions.scheduledTasks.editor.permissionAutoAccept.aria': 'Start the task session with permission auto-accept enabled',
'sessions.scheduledTasks.editor.actions.closeAria': 'Close',
'sessions.scheduledTasks.editor.actions.cancel': 'Cancel',
'sessions.scheduledTasks.editor.actions.save': 'Save',
diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts
index 799a7b08..abcade0c 100644
--- a/packages/ui/src/lib/i18n/messages/es.ts
+++ b/packages/ui/src/lib/i18n/messages/es.ts
@@ -297,6 +297,8 @@ export const dict: Record = {
"sessions.scheduledTasks.editor.goal.aria": "Ejecutar esta tarea como un objetivo que el agente persigue hasta completarlo",
"sessions.scheduledTasks.editor.goal.budgetLabel": "Presupuesto de tokens",
"sessions.scheduledTasks.editor.goal.budgetAria": "Limitar el objetivo a un presupuesto de tokens",
+ "sessions.scheduledTasks.editor.permissionAutoAccept.label": "Aceptar permisos automáticamente",
+ "sessions.scheduledTasks.editor.permissionAutoAccept.aria": "Iniciar la sesión de la tarea con la aceptación automática de permisos activada",
"sessions.scheduledTasks.editor.actions.closeAria": "Cerrar",
"sessions.scheduledTasks.editor.actions.cancel": "Cancelar",
"sessions.scheduledTasks.editor.actions.save": "Guardar",
diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts
index a2b2f893..3dc56279 100644
--- a/packages/ui/src/lib/i18n/messages/fr.ts
+++ b/packages/ui/src/lib/i18n/messages/fr.ts
@@ -139,6 +139,8 @@ export const dict = {
'sessions.scheduledTasks.editor.goal.aria': 'Exécuter cette tâche comme un objectif que l\'agent poursuit jusqu\'au bout',
'sessions.scheduledTasks.editor.goal.budgetLabel': 'Budget de tokens',
'sessions.scheduledTasks.editor.goal.budgetAria': 'Limiter l’objectif à un budget de tokens',
+ 'sessions.scheduledTasks.editor.permissionAutoAccept.label': 'Accepter automatiquement les autorisations',
+ 'sessions.scheduledTasks.editor.permissionAutoAccept.aria': 'Démarrer la session de la tâche avec l’acceptation automatique des autorisations activée',
'sessions.scheduledTasks.editor.actions.closeAria': 'Fermer',
'sessions.scheduledTasks.editor.actions.cancel': 'Annuler',
'sessions.scheduledTasks.editor.actions.save': 'Sauvegarder',
diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts
index 3243bac1..a62bda58 100644
--- a/packages/ui/src/lib/i18n/messages/ja.ts
+++ b/packages/ui/src/lib/i18n/messages/ja.ts
@@ -297,6 +297,8 @@ export const dict: Record = {
'sessions.scheduledTasks.editor.goal.aria': 'このタスクをエージェントが完了まで追求するゴールとして実行します',
'sessions.scheduledTasks.editor.goal.budgetLabel': 'トークン予算',
'sessions.scheduledTasks.editor.goal.budgetAria': 'ゴールをトークン予算で制限します',
+ 'sessions.scheduledTasks.editor.permissionAutoAccept.label': '権限を自動承認',
+ 'sessions.scheduledTasks.editor.permissionAutoAccept.aria': '権限の自動承認を有効にしてタスクのセッションを開始します',
'sessions.scheduledTasks.editor.actions.closeAria': '閉じる',
'sessions.scheduledTasks.editor.actions.cancel': 'キャンセル',
'sessions.scheduledTasks.editor.actions.save': '保存',
diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts
index 8600b022..e5c2ea4d 100644
--- a/packages/ui/src/lib/i18n/messages/ko.ts
+++ b/packages/ui/src/lib/i18n/messages/ko.ts
@@ -297,6 +297,8 @@ export const dict: Record = {
'sessions.scheduledTasks.editor.goal.aria': '이 작업을 에이전트가 완료까지 추진하는 목표로 실행합니다',
'sessions.scheduledTasks.editor.goal.budgetLabel': '토큰 예산',
'sessions.scheduledTasks.editor.goal.budgetAria': '목표를 토큰 예산으로 제한합니다',
+ 'sessions.scheduledTasks.editor.permissionAutoAccept.label': '권한 자동 승인',
+ 'sessions.scheduledTasks.editor.permissionAutoAccept.aria': '권한 자동 승인이 켜진 상태로 작업 세션을 시작합니다',
'sessions.scheduledTasks.editor.actions.closeAria': '닫기',
'sessions.scheduledTasks.editor.actions.cancel': '취소',
'sessions.scheduledTasks.editor.actions.save': '저장',
diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts
index e36af240..13b8f78c 100644
--- a/packages/ui/src/lib/i18n/messages/pl.ts
+++ b/packages/ui/src/lib/i18n/messages/pl.ts
@@ -391,6 +391,8 @@ export const dict: Record = {
'sessions.scheduledTasks.editor.goal.aria': 'Uruchom to zadanie jako cel, do którego agent dąży aż do ukończenia',
'sessions.scheduledTasks.editor.goal.budgetLabel': 'Budżet tokenów',
'sessions.scheduledTasks.editor.goal.budgetAria': 'Ogranicz cel budżetem tokenów',
+ 'sessions.scheduledTasks.editor.permissionAutoAccept.label': 'Automatycznie akceptuj uprawnienia',
+ 'sessions.scheduledTasks.editor.permissionAutoAccept.aria': 'Uruchom sesję zadania z włączonym automatycznym akceptowaniem uprawnień',
'sessions.scheduledTasks.editor.actions.closeAria': 'Zamknij',
'sessions.scheduledTasks.editor.actions.cancel': 'Anuluj',
'sessions.scheduledTasks.editor.actions.save': 'Zapisz',
diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts
index cf9d78cc..65400be9 100644
--- a/packages/ui/src/lib/i18n/messages/pt-BR.ts
+++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts
@@ -297,6 +297,8 @@ export const dict: Record = {
"sessions.scheduledTasks.editor.goal.aria": "Executar esta tarefa como um objetivo que o agente persegue até concluir",
"sessions.scheduledTasks.editor.goal.budgetLabel": "Orçamento de tokens",
"sessions.scheduledTasks.editor.goal.budgetAria": "Limitar o objetivo a um orçamento de tokens",
+ "sessions.scheduledTasks.editor.permissionAutoAccept.label": "Aceitar permissões automaticamente",
+ "sessions.scheduledTasks.editor.permissionAutoAccept.aria": "Iniciar a sessão da tarefa com a aceitação automática de permissões ativada",
"sessions.scheduledTasks.editor.actions.closeAria": "Fechar",
"sessions.scheduledTasks.editor.actions.cancel": "Cancelar",
"sessions.scheduledTasks.editor.actions.save": "Salvar",
diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts
index 32e452bf..22c3251d 100644
--- a/packages/ui/src/lib/i18n/messages/uk.ts
+++ b/packages/ui/src/lib/i18n/messages/uk.ts
@@ -297,6 +297,8 @@ export const dict: Record = {
"sessions.scheduledTasks.editor.goal.aria": "Виконати це завдання як ціль, яку агент веде до завершення",
"sessions.scheduledTasks.editor.goal.budgetLabel": "Бюджет токенів",
"sessions.scheduledTasks.editor.goal.budgetAria": "Обмежити ціль бюджетом токенів",
+ "sessions.scheduledTasks.editor.permissionAutoAccept.label": "Автоматично приймати дозволи",
+ "sessions.scheduledTasks.editor.permissionAutoAccept.aria": "Запускати сесію завдання з увімкненим автоматичним прийняттям дозволів",
"sessions.scheduledTasks.editor.actions.closeAria": "Закрити",
"sessions.scheduledTasks.editor.actions.cancel": "Скасувати",
"sessions.scheduledTasks.editor.actions.save": "Зберегти",
diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts
index b85d3e77..8d24a202 100644
--- a/packages/ui/src/lib/i18n/messages/zh-CN.ts
+++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts
@@ -297,6 +297,8 @@ export const dict: Record = {
'sessions.scheduledTasks.editor.goal.aria': '将此任务作为代理持续推进直至完成的目标运行',
'sessions.scheduledTasks.editor.goal.budgetLabel': '令牌预算',
'sessions.scheduledTasks.editor.goal.budgetAria': '用令牌预算限制该目标',
+ 'sessions.scheduledTasks.editor.permissionAutoAccept.label': '自动接受权限',
+ 'sessions.scheduledTasks.editor.permissionAutoAccept.aria': '启动任务会话时启用权限自动接受',
'sessions.scheduledTasks.editor.actions.closeAria': '关闭',
'sessions.scheduledTasks.editor.actions.cancel': '取消',
'sessions.scheduledTasks.editor.actions.save': '保存',
diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts
index a614462e..cb38d4b4 100644
--- a/packages/ui/src/lib/i18n/messages/zh-TW.ts
+++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts
@@ -310,6 +310,8 @@ export const dict: Record = {
'sessions.scheduledTasks.editor.goal.aria': '將此任務作為代理持續推進直至完成的目標執行',
'sessions.scheduledTasks.editor.goal.budgetLabel': '權杖預算',
'sessions.scheduledTasks.editor.goal.budgetAria': '以權杖預算限制該目標',
+ 'sessions.scheduledTasks.editor.permissionAutoAccept.label': '自動接受權限',
+ 'sessions.scheduledTasks.editor.permissionAutoAccept.aria': '啟動任務工作階段時啟用權限自動接受',
'sessions.scheduledTasks.editor.actions.closeAria': '關閉',
'sessions.scheduledTasks.editor.actions.cancel': '取消',
'sessions.scheduledTasks.editor.actions.save': '儲存',
diff --git a/packages/ui/src/lib/scheduledTasksApi.ts b/packages/ui/src/lib/scheduledTasksApi.ts
index 5948266c..8b215d64 100644
--- a/packages/ui/src/lib/scheduledTasksApi.ts
+++ b/packages/ui/src/lib/scheduledTasksApi.ts
@@ -23,6 +23,7 @@ export type ScheduledTask = {
agent?: string;
goalEnabled?: boolean;
goalTokenBudget?: number;
+ permissionAutoAccept?: boolean;
};
state: {
createdAt: number;
diff --git a/packages/ui/src/lib/timezones.ts b/packages/ui/src/lib/timezones.ts
new file mode 100644
index 00000000..bbb2da7d
--- /dev/null
+++ b/packages/ui/src/lib/timezones.ts
@@ -0,0 +1,14 @@
+// Some ICU builds still expose pre-rename IANA timezone ids. Display (and
+// persist going forward) the modern names.
+const LEGACY_TIMEZONE_ALIASES: Record = {
+ 'Europe/Kiev': 'Europe/Kyiv',
+ 'Asia/Calcutta': 'Asia/Kolkata',
+ 'Asia/Saigon': 'Asia/Ho_Chi_Minh',
+ 'Asia/Rangoon': 'Asia/Yangon',
+ 'Asia/Katmandu': 'Asia/Kathmandu',
+ 'America/Godthab': 'America/Nuuk',
+ 'Atlantic/Faeroe': 'Atlantic/Faroe',
+ 'Pacific/Enderbury': 'Pacific/Kanton',
+};
+
+export const canonicalizeTimezone = (timezone: string): string => LEGACY_TIMEZONE_ALIASES[timezone] ?? timezone;
diff --git a/packages/web/server/index.js b/packages/web/server/index.js
index a9fefea9..a1715a11 100644
--- a/packages/web/server/index.js
+++ b/packages/web/server/index.js
@@ -1068,6 +1068,7 @@ const scheduledTasksRuntime = createScheduledTasksRuntime({
buildOpenCodeUrl,
getOpenCodeAuthHeaders,
waitForOpenCodeReady,
+ setSessionAutoAccept: (sessionId, enabled, directory) => permissionAutoAcceptRuntime.setSessionPolicy(sessionId, enabled, directory),
emitTaskRunEvent: (event) => {
for (const client of uiOpenChamberEventClients) {
try {
diff --git a/packages/web/server/lib/projects/project-config.js b/packages/web/server/lib/projects/project-config.js
index 67c5f284..ed8d98b6 100644
--- a/packages/web/server/lib/projects/project-config.js
+++ b/packages/web/server/lib/projects/project-config.js
@@ -214,6 +214,7 @@ const normalizeExecution = (value) => {
const variant = asNonEmptyString(value.variant);
const agent = asNonEmptyString(value.agent);
const goalEnabled = value.goalEnabled === true;
+ const permissionAutoAccept = value.permissionAutoAccept === true;
const goalTokenBudget = typeof value.goalTokenBudget === 'number'
&& Number.isFinite(value.goalTokenBudget)
&& value.goalTokenBudget > 0
@@ -238,6 +239,7 @@ const normalizeExecution = (value) => {
...(agent ? { agent } : {}),
...(goalEnabled ? { goalEnabled: true } : {}),
...(goalEnabled && goalTokenBudget ? { goalTokenBudget } : {}),
+ ...(permissionAutoAccept ? { permissionAutoAccept: true } : {}),
};
};
diff --git a/packages/web/server/lib/scheduled-tasks/runtime.js b/packages/web/server/lib/scheduled-tasks/runtime.js
index 7534cf97..c1f42fd6 100644
--- a/packages/web/server/lib/scheduled-tasks/runtime.js
+++ b/packages/web/server/lib/scheduled-tasks/runtime.js
@@ -225,6 +225,7 @@ export const createScheduledTasksRuntime = (deps) => {
getOpenCodeAuthHeaders,
waitForOpenCodeReady,
emitTaskRunEvent,
+ setSessionAutoAccept,
logger = console,
maxGlobalConcurrency = DEFAULT_GLOBAL_CONCURRENCY,
maxProjectConcurrency = DEFAULT_PROJECT_CONCURRENCY,
@@ -611,6 +612,17 @@ export const createScheduledTasksRuntime = (deps) => {
} catch {
}
+ if (task.execution.permissionAutoAccept && typeof setSessionAutoAccept === 'function') {
+ // Enroll before the prompt goes out so the very first permission request
+ // is already auto-approved. Enrollment failure must not kill the run —
+ // the task still executes, permissions just wait for the user.
+ try {
+ await setSessionAutoAccept(sessionID, true, projectPath);
+ } catch (error) {
+ logger.warn?.('[scheduled-tasks] failed to enable permission auto-accept for session', sessionID, error?.message ?? error);
+ }
+ }
+
if (task.execution.goalEnabled) {
await createTaskGoal({ baseUrl, authHeaders, sessionID, projectPath, task });
}