feat: scheduled task permission auto-accept and composer-style editor toggles
This commit is contained in:
@@ -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: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{draft.execution.goalEnabled ? (
|
||||
<div className="flex flex-wrap items-center gap-x-8 gap-y-2">
|
||||
<label className="inline-flex cursor-pointer items-center gap-2">
|
||||
<Checkbox
|
||||
checked={draft.execution.goalEnabled}
|
||||
onChange={(goalEnabled) => setDraft((prev) => ({
|
||||
...prev,
|
||||
execution: { ...prev.execution, goalEnabled },
|
||||
}))}
|
||||
ariaLabel={t('sessions.scheduledTasks.editor.goal.aria')}
|
||||
/>
|
||||
<span className="typography-meta">{t('sessions.scheduledTasks.editor.goal.label')}</span>
|
||||
</label>
|
||||
{draft.execution.goalEnabled ? (
|
||||
<label className="inline-flex cursor-pointer items-center gap-2">
|
||||
<Checkbox
|
||||
checked={draft.execution.goalTokenBudget !== null}
|
||||
@@ -1647,8 +1646,7 @@ export function ScheduledTaskEditorDialog(props: {
|
||||
/>
|
||||
<span className="typography-meta">{t('sessions.scheduledTasks.editor.goal.budgetLabel')}</span>
|
||||
</label>
|
||||
) : null}
|
||||
{draft.execution.goalEnabled && draft.execution.goalTokenBudget !== null ? (
|
||||
{draft.execution.goalTokenBudget !== null ? (
|
||||
<NumberInput
|
||||
value={draft.execution.goalTokenBudget}
|
||||
onValueChange={(value) => {
|
||||
@@ -1665,6 +1663,7 @@ export function ScheduledTaskEditorDialog(props: {
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1680,6 +1679,48 @@ export function ScheduledTaskEditorDialog(props: {
|
||||
</label>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(EDITOR_TOGGLE_BUTTON_CLASS)}
|
||||
onClick={() => setDraft((prev) => ({
|
||||
...prev,
|
||||
execution: { ...prev.execution, permissionAutoAccept: !prev.execution.permissionAutoAccept },
|
||||
}))}
|
||||
aria-pressed={draft.execution.permissionAutoAccept}
|
||||
aria-label={t('sessions.scheduledTasks.editor.permissionAutoAccept.aria')}
|
||||
>
|
||||
{draft.execution.permissionAutoAccept ? (
|
||||
<Icon name="shield-check" className="h-[18px] w-[18px]" style={{ color: 'var(--status-info)' }} aria-hidden="true" />
|
||||
) : (
|
||||
<Icon name="shield-user" className="h-[18px] w-[18px]" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={6}>{t('sessions.scheduledTasks.editor.permissionAutoAccept.label')}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(EDITOR_TOGGLE_BUTTON_CLASS, draft.execution.goalEnabled && 'text-[var(--status-info)]')}
|
||||
onClick={() => setDraft((prev) => ({
|
||||
...prev,
|
||||
execution: { ...prev.execution, goalEnabled: !prev.execution.goalEnabled },
|
||||
}))}
|
||||
aria-pressed={draft.execution.goalEnabled}
|
||||
aria-label={t('sessions.scheduledTasks.editor.goal.aria')}
|
||||
>
|
||||
{draft.execution.goalEnabled ? (
|
||||
<Icon name="target-fill" className="h-[18px] w-[18px] text-current" aria-hidden="true" />
|
||||
) : (
|
||||
<Icon name="target" className="h-[18px] w-[18px] text-current" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={6}>{t('sessions.scheduledTasks.editor.goal.label')}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={() => onOpenChange(false)} disabled={saving}>
|
||||
{t('sessions.scheduledTasks.editor.actions.cancel')}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user