mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
feat: redesign scheduled task dialog to match Claude Code original
Refactor PermissionModeSelector and ModelSelector to support controlled (prop-driven) mode alongside the existing store-driven mode, enabling reuse in the new task creation dialog. Rewrite NewTaskModal with embedded prompt editor that combines textarea + permission/model/folder controls, matching Claude's original design. Add desktop-online warning banner on the scheduled tasks list page and default folder to current session workDir. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f81fe41797
commit
602c61ad4c
@ -15,11 +15,21 @@ const EFFORT_OPTIONS: { value: EffortLevel; label: string }[] = [
|
||||
{ value: 'max', label: 'Max' },
|
||||
]
|
||||
|
||||
export function ModelSelector() {
|
||||
const { currentModel, availableModels, effortLevel, setModel, setEffort } = useSettingsStore()
|
||||
type Props = {
|
||||
/** Controlled mode: model ID override */
|
||||
value?: string
|
||||
/** Controlled mode: called on change instead of updating global store */
|
||||
onChange?: (modelId: string) => void
|
||||
}
|
||||
|
||||
export function ModelSelector({ value, onChange }: Props = {}) {
|
||||
const { currentModel: storeModel, availableModels, effortLevel, setModel, setEffort } = useSettingsStore()
|
||||
const [open, setOpen] = useState(false)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
const isControlled = value !== undefined
|
||||
const selectedModel = isControlled ? availableModels.find((m) => m.id === value) || null : storeModel
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
@ -51,7 +61,7 @@ export function ModelSelector() {
|
||||
className="flex items-center gap-1.5 px-2.5 py-1.5 bg-[var(--color-surface-container-low)] hover:bg-[var(--color-surface-hover)] rounded-full text-xs font-medium text-[var(--color-text-secondary)] transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] text-[var(--color-brand)]">auto_awesome</span>
|
||||
<span>{currentModel?.name ?? 'Select model'}</span>
|
||||
<span>{selectedModel?.name ?? 'Select model'}</span>
|
||||
<span className="material-symbols-outlined text-[12px]">expand_more</span>
|
||||
</button>
|
||||
|
||||
@ -64,11 +74,18 @@ export function ModelSelector() {
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{availableModels.map((model) => {
|
||||
const isSelected = model.id === currentModel?.id
|
||||
const isSelected = model.id === selectedModel?.id
|
||||
return (
|
||||
<button
|
||||
key={model.id}
|
||||
onClick={() => { setModel(model.id); setOpen(false) }}
|
||||
onClick={() => {
|
||||
if (isControlled) {
|
||||
onChange?.(model.id)
|
||||
} else {
|
||||
setModel(model.id)
|
||||
}
|
||||
setOpen(false)
|
||||
}}
|
||||
className={`
|
||||
w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors
|
||||
${isSelected
|
||||
@ -104,8 +121,8 @@ export function ModelSelector() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Effort */}
|
||||
<div className="border-t border-[var(--color-border)] p-3">
|
||||
{/* Effort — hidden in controlled mode (not relevant for task creation) */}
|
||||
{!isControlled && <div className="border-t border-[var(--color-border)] p-3">
|
||||
<div className="text-[10px] font-bold uppercase tracking-widest text-[var(--color-outline)] mb-2 px-1">
|
||||
Effort
|
||||
</div>
|
||||
@ -129,7 +146,7 @@ export function ModelSelector() {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -56,8 +56,16 @@ const MODE_LABELS: Record<PermissionMode, string> = {
|
||||
dontAsk: 'Don\'t ask',
|
||||
}
|
||||
|
||||
export function PermissionModeSelector({ workDir: workDirProp }: { workDir?: string } = {}) {
|
||||
const { permissionMode, setPermissionMode } = useSettingsStore()
|
||||
type Props = {
|
||||
workDir?: string
|
||||
/** Controlled mode: override current value */
|
||||
value?: PermissionMode
|
||||
/** Controlled mode: called on change instead of updating global store */
|
||||
onChange?: (mode: PermissionMode) => void
|
||||
}
|
||||
|
||||
export function PermissionModeSelector({ workDir: workDirProp, value, onChange }: Props = {}) {
|
||||
const { permissionMode: storeMode, setPermissionMode } = useSettingsStore()
|
||||
const setSessionPermissionMode = useChatStore((s) => s.setSessionPermissionMode)
|
||||
const sessions = useSessionStore((s) => s.sessions)
|
||||
const activeSessionId = useSessionStore((s) => s.activeSessionId)
|
||||
@ -65,6 +73,9 @@ export function PermissionModeSelector({ workDir: workDirProp }: { workDir?: str
|
||||
const [confirmDialog, setConfirmDialog] = useState(false)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
const isControlled = value !== undefined
|
||||
const currentMode = isControlled ? value : storeMode
|
||||
|
||||
const activeSession = sessions.find((s) => s.id === activeSessionId)
|
||||
const workDir = workDirProp || activeSession?.workDir || '~'
|
||||
|
||||
@ -90,8 +101,8 @@ export function PermissionModeSelector({ workDir: workDirProp }: { workDir?: str
|
||||
onClick={() => setOpen(!open)}
|
||||
className="flex items-center gap-1.5 px-2.5 py-1.5 bg-[var(--color-surface-container-low)] hover:bg-[var(--color-surface-hover)] rounded-full text-xs font-medium text-[var(--color-text-secondary)] transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">{MODE_ICONS[permissionMode]}</span>
|
||||
<span>{MODE_LABELS[permissionMode]}</span>
|
||||
<span className="material-symbols-outlined text-[14px]">{MODE_ICONS[currentMode]}</span>
|
||||
<span>{MODE_LABELS[currentMode]}</span>
|
||||
<span className="material-symbols-outlined text-[12px]">expand_more</span>
|
||||
</button>
|
||||
|
||||
@ -109,14 +120,18 @@ export function PermissionModeSelector({ workDir: workDirProp }: { workDir?: str
|
||||
setConfirmDialog(true)
|
||||
return
|
||||
}
|
||||
void setPermissionMode(item.value)
|
||||
setSessionPermissionMode(item.value)
|
||||
if (isControlled) {
|
||||
onChange?.(item.value)
|
||||
} else {
|
||||
void setPermissionMode(item.value)
|
||||
setSessionPermissionMode(item.value)
|
||||
}
|
||||
setOpen(false)
|
||||
}}
|
||||
className={`
|
||||
w-full flex items-start gap-3 px-4 py-3 text-left transition-colors
|
||||
hover:bg-[var(--color-surface-hover)]
|
||||
${item.value === permissionMode ? 'bg-[var(--color-surface-selected)]' : ''}
|
||||
${item.value === currentMode ? 'bg-[var(--color-surface-selected)]' : ''}
|
||||
`}
|
||||
>
|
||||
<span className={`material-symbols-outlined text-[20px] mt-0.5 ${item.color || 'text-[var(--color-text-secondary)]'}`}>
|
||||
@ -126,7 +141,7 @@ export function PermissionModeSelector({ workDir: workDirProp }: { workDir?: str
|
||||
<div className="text-sm font-semibold text-[var(--color-text-primary)]">{item.label}</div>
|
||||
<div className="text-xs text-[var(--color-text-tertiary)] mt-0.5">{item.description}</div>
|
||||
</div>
|
||||
{item.value === permissionMode && (
|
||||
{item.value === currentMode && (
|
||||
<span className="material-symbols-outlined text-[16px] text-[var(--color-brand)] mt-0.5" style={{ fontVariationSettings: "'FILL' 1" }}>
|
||||
check_circle
|
||||
</span>
|
||||
@ -189,8 +204,12 @@ export function PermissionModeSelector({ workDir: workDirProp }: { workDir?: str
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
void setPermissionMode('bypassPermissions')
|
||||
setSessionPermissionMode('bypassPermissions')
|
||||
if (isControlled) {
|
||||
onChange?.('bypassPermissions')
|
||||
} else {
|
||||
void setPermissionMode('bypassPermissions')
|
||||
setSessionPermissionMode('bypassPermissions')
|
||||
}
|
||||
setConfirmDialog(false)
|
||||
}}
|
||||
className="px-4 py-2 text-xs font-semibold text-white bg-[var(--color-error)] hover:opacity-90 rounded-lg transition-colors"
|
||||
|
||||
@ -1,46 +1,58 @@
|
||||
import { useState } from 'react'
|
||||
import { useTaskStore } from '../../stores/taskStore'
|
||||
import { useSettingsStore } from '../../stores/settingsStore'
|
||||
import { useSessionStore } from '../../stores/sessionStore'
|
||||
import { Modal } from '../shared/Modal'
|
||||
import { Input } from '../shared/Input'
|
||||
import { Textarea } from '../shared/Textarea'
|
||||
import { Button } from '../shared/Button'
|
||||
import { PromptEditor } from './PromptEditor'
|
||||
import type { PermissionMode } from '../../types/settings'
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const FREQUENCY_OPTIONS = [
|
||||
{ value: '0 * * * *', label: 'Hourly' },
|
||||
{ value: '0 9 * * *', label: 'Daily' },
|
||||
{ value: '0 9 * * 1-5', label: 'Weekdays' },
|
||||
{ value: '0 9 * * 1', label: 'Weekly' },
|
||||
{ value: '0 9 1 * *', label: 'Monthly' },
|
||||
type FrequencyKey = 'hourly' | 'daily' | 'weekdays' | 'weekly' | 'monthly'
|
||||
|
||||
const FREQUENCY_OPTIONS: Array<{ value: FrequencyKey; label: string; showTime: boolean }> = [
|
||||
{ value: 'hourly', label: 'Hourly', showTime: false },
|
||||
{ value: 'daily', label: 'Daily', showTime: true },
|
||||
{ value: 'weekdays', label: 'Weekdays', showTime: true },
|
||||
{ value: 'weekly', label: 'Weekly', showTime: true },
|
||||
{ value: 'monthly', label: 'Monthly', showTime: true },
|
||||
]
|
||||
|
||||
const PERMISSION_OPTIONS = [
|
||||
{ value: '', label: 'Default' },
|
||||
{ value: 'ask', label: 'Ask permissions' },
|
||||
{ value: 'auto-accept', label: 'Auto accept edits' },
|
||||
{ value: 'plan', label: 'Plan mode' },
|
||||
{ value: 'bypass', label: 'Bypass permissions' },
|
||||
]
|
||||
function buildCron(freq: FrequencyKey, time: string): string {
|
||||
const [hours, minutes] = time.split(':').map(Number)
|
||||
switch (freq) {
|
||||
case 'hourly': return '0 * * * *'
|
||||
case 'daily': return `${minutes} ${hours} * * *`
|
||||
case 'weekdays': return `${minutes} ${hours} * * 1-5`
|
||||
case 'weekly': return `${minutes} ${hours} * * 1`
|
||||
case 'monthly': return `${minutes} ${hours} 1 * *`
|
||||
}
|
||||
}
|
||||
|
||||
export function NewTaskModal({ open, onClose }: Props) {
|
||||
const { createTask } = useTaskStore()
|
||||
const availableModels = useSettingsStore((s) => s.availableModels)
|
||||
const sessions = useSessionStore((s) => s.sessions)
|
||||
const activeSessionId = useSessionStore((s) => s.activeSessionId)
|
||||
const activeSession = sessions.find((s) => s.id === activeSessionId)
|
||||
const defaultWorkDir = activeSession?.workDir || ''
|
||||
|
||||
const [name, setName] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [prompt, setPrompt] = useState('')
|
||||
const [cron, setCron] = useState('0 9 * * *')
|
||||
const [frequency, setFrequency] = useState<FrequencyKey>('daily')
|
||||
const [time, setTime] = useState('09:00')
|
||||
const [model, setModel] = useState('')
|
||||
const [permissionMode, setPermissionMode] = useState('')
|
||||
const [folderPath, setFolderPath] = useState('')
|
||||
const [permissionMode, setPermissionMode] = useState<PermissionMode>('default')
|
||||
const [folderPath, setFolderPath] = useState(defaultWorkDir)
|
||||
const [useWorktree, setUseWorktree] = useState(false)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||
|
||||
const canSubmit = name.trim() && prompt.trim()
|
||||
const canSubmit = name.trim() && description.trim() && prompt.trim()
|
||||
const showTime = FREQUENCY_OPTIONS.find((o) => o.value === frequency)?.showTime ?? false
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!canSubmit) return
|
||||
@ -48,24 +60,26 @@ export function NewTaskModal({ open, onClose }: Props) {
|
||||
try {
|
||||
await createTask({
|
||||
name: name.trim(),
|
||||
description: description.trim() || undefined,
|
||||
cron,
|
||||
description: description.trim(),
|
||||
cron: buildCron(frequency, time),
|
||||
prompt: prompt.trim(),
|
||||
enabled: true,
|
||||
recurring: true,
|
||||
model: model || undefined,
|
||||
permissionMode: permissionMode || undefined,
|
||||
permissionMode: permissionMode !== 'default' ? permissionMode : undefined,
|
||||
folderPath: folderPath.trim() || undefined,
|
||||
useWorktree: useWorktree || undefined,
|
||||
})
|
||||
// Reset form
|
||||
setName('')
|
||||
setDescription('')
|
||||
setPrompt('')
|
||||
setCron('0 9 * * *')
|
||||
setFrequency('daily')
|
||||
setTime('09:00')
|
||||
setModel('')
|
||||
setPermissionMode('')
|
||||
setPermissionMode('default')
|
||||
setFolderPath('')
|
||||
setShowAdvanced(false)
|
||||
setUseWorktree(false)
|
||||
onClose()
|
||||
} catch (err) {
|
||||
console.error('Failed to create task:', err)
|
||||
@ -74,8 +88,6 @@ export function NewTaskModal({ open, onClose }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
const selectClass = 'h-10 px-3 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] text-sm text-[var(--color-text-primary)] outline-none focus:border-[var(--color-border-focus)]'
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
@ -89,8 +101,8 @@ export function NewTaskModal({ open, onClose }: Props) {
|
||||
}
|
||||
>
|
||||
{/* Info banner */}
|
||||
<div className="flex items-start gap-2 px-3 py-2.5 rounded-[var(--radius-md)] bg-[var(--color-surface-info)] mb-4">
|
||||
<span className="text-[var(--color-text-secondary)] text-sm">ℹ</span>
|
||||
<div className="flex items-center gap-2.5 px-3.5 py-2.5 rounded-[var(--radius-md)] bg-[var(--color-surface-container)] mb-5">
|
||||
<span className="material-symbols-outlined text-[18px] text-[var(--color-text-secondary)]">info</span>
|
||||
<span className="text-xs text-[var(--color-text-secondary)]">
|
||||
Local tasks only run while your computer is awake.
|
||||
</span>
|
||||
@ -107,80 +119,61 @@ export function NewTaskModal({ open, onClose }: Props) {
|
||||
|
||||
<Input
|
||||
label="Description"
|
||||
required
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Review yesterday's commits..."
|
||||
placeholder="Review yesterday's commits and flag anything concerning"
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Prompt"
|
||||
required
|
||||
{/* Prompt editor with embedded controls */}
|
||||
<PromptEditor
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder="Look at the commits from the last 24 hours. Summarize what changed, call out any risky patterns..."
|
||||
onChange={setPrompt}
|
||||
placeholder="Look at the commits from the last 24 hours. Summarize what changed, call out any risky patterns or missing tests, and note anything worth following up on."
|
||||
permissionMode={permissionMode}
|
||||
onPermissionModeChange={setPermissionMode}
|
||||
modelId={model}
|
||||
onModelChange={setModel}
|
||||
folderPath={folderPath}
|
||||
onFolderPathChange={setFolderPath}
|
||||
useWorktree={useWorktree}
|
||||
onUseWorktreeChange={setUseWorktree}
|
||||
/>
|
||||
|
||||
{/* Frequency */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-medium text-[var(--color-text-primary)]">Frequency</label>
|
||||
<select value={cron} onChange={(e) => setCron(e.target.value)} className={selectClass}>
|
||||
{FREQUENCY_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={frequency}
|
||||
onChange={(e) => setFrequency(e.target.value as FrequencyKey)}
|
||||
className="w-full h-10 px-3 pr-8 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] text-sm text-[var(--color-text-primary)] outline-none focus:border-[var(--color-border-focus)] appearance-none cursor-pointer"
|
||||
>
|
||||
{FREQUENCY_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="material-symbols-outlined text-[18px] text-[var(--color-text-tertiary)] absolute right-2.5 top-1/2 -translate-y-1/2 pointer-events-none">
|
||||
expand_more
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advanced toggle */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
className="flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors self-start"
|
||||
>
|
||||
<span
|
||||
className="material-symbols-outlined text-[16px] transition-transform"
|
||||
style={{ transform: showAdvanced ? 'rotate(90deg)' : 'rotate(0deg)' }}
|
||||
>
|
||||
chevron_right
|
||||
</span>
|
||||
Advanced options
|
||||
</button>
|
||||
|
||||
{showAdvanced && (
|
||||
<div className="flex flex-col gap-4 pl-2 border-l-2 border-[var(--color-border)]">
|
||||
{/* Model */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-medium text-[var(--color-text-primary)]">Model</label>
|
||||
<select value={model} onChange={(e) => setModel(e.target.value)} className={selectClass}>
|
||||
<option value="">Default (current model)</option>
|
||||
{availableModels.map((m) => (
|
||||
<option key={m.id} value={m.id}>{m.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-[10px] text-[var(--color-text-tertiary)]">Override the model used for this task.</p>
|
||||
</div>
|
||||
|
||||
{/* Permission Mode */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-medium text-[var(--color-text-primary)]">Permission mode</label>
|
||||
<select value={permissionMode} onChange={(e) => setPermissionMode(e.target.value)} className={selectClass}>
|
||||
{PERMISSION_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Folder Path */}
|
||||
<Input
|
||||
label="Working directory"
|
||||
value={folderPath}
|
||||
onChange={(e) => setFolderPath(e.target.value)}
|
||||
placeholder="/path/to/project"
|
||||
{/* Time picker — shown when frequency supports specific time */}
|
||||
{showTime && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<input
|
||||
type="time"
|
||||
value={time}
|
||||
onChange={(e) => setTime(e.target.value)}
|
||||
className="w-auto h-10 px-3 rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface)] text-sm text-[var(--color-text-primary)] outline-none focus:border-[var(--color-border-focus)]"
|
||||
style={{ maxWidth: 120 }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-[var(--color-text-tertiary)]">
|
||||
Scheduled tasks use a randomized delay of up to 5 minutes to avoid rate limits.
|
||||
Scheduled tasks use a randomized delay of several minutes for server performance.
|
||||
</p>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
72
desktop/src/components/tasks/PromptEditor.tsx
Normal file
72
desktop/src/components/tasks/PromptEditor.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import { PermissionModeSelector } from '../controls/PermissionModeSelector'
|
||||
import { ModelSelector } from '../controls/ModelSelector'
|
||||
import { DirectoryPicker } from '../shared/DirectoryPicker'
|
||||
import type { PermissionMode } from '../../types/settings'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
placeholder?: string
|
||||
|
||||
permissionMode: PermissionMode
|
||||
onPermissionModeChange: (mode: PermissionMode) => void
|
||||
|
||||
modelId: string
|
||||
onModelChange: (modelId: string) => void
|
||||
|
||||
folderPath: string
|
||||
onFolderPathChange: (path: string) => void
|
||||
|
||||
useWorktree: boolean
|
||||
onUseWorktreeChange: (checked: boolean) => void
|
||||
}
|
||||
|
||||
export function PromptEditor({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
permissionMode,
|
||||
onPermissionModeChange,
|
||||
modelId,
|
||||
onModelChange,
|
||||
folderPath,
|
||||
onFolderPathChange,
|
||||
useWorktree,
|
||||
onUseWorktreeChange,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="rounded-[var(--radius-lg)] border border-[var(--color-border)] focus-within:border-[var(--color-border-focus)] transition-colors overflow-visible">
|
||||
{/* Prompt textarea */}
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
rows={4}
|
||||
className="w-full resize-y bg-transparent px-3 py-2.5 text-sm leading-relaxed text-[var(--color-text-primary)] outline-none placeholder:text-[var(--color-text-tertiary)]"
|
||||
style={{ minHeight: 120 }}
|
||||
/>
|
||||
|
||||
{/* Bottom toolbar */}
|
||||
<div className="border-t border-[var(--color-border)]/40 px-3 py-2 flex flex-col gap-2 bg-[var(--color-surface-container-low)] rounded-b-[var(--radius-lg)]">
|
||||
{/* Row 1: Permission + Model selectors */}
|
||||
<div className="flex items-center justify-between">
|
||||
<PermissionModeSelector value={permissionMode} onChange={onPermissionModeChange} />
|
||||
<ModelSelector value={modelId} onChange={onModelChange} />
|
||||
</div>
|
||||
|
||||
{/* Row 2: Folder picker */}
|
||||
<div className="flex items-center justify-between">
|
||||
<DirectoryPicker value={folderPath} onChange={onFolderPathChange} />
|
||||
</div>
|
||||
|
||||
{/* Bypass + no folder warning */}
|
||||
{permissionMode === 'bypassPermissions' && (
|
||||
<div className="flex items-center gap-1.5 px-2 py-1.5 rounded-md bg-[var(--color-error)]/8 text-[10px] text-[var(--color-error)]">
|
||||
<span className="material-symbols-outlined text-[12px]">warning</span>
|
||||
Bypass mode grants full system access{folderPath ? ` within ${folderPath}` : ' — select a folder to limit scope'}.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -19,16 +19,24 @@ export function ScheduledTasks() {
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="px-10 py-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-[var(--color-text-primary)]">Scheduled tasks</h1>
|
||||
<p className="mt-1 text-sm text-[var(--color-text-secondary)]">
|
||||
Run tasks on a schedule or whenever you need them.
|
||||
Run tasks on a schedule or whenever you need them. Type <code className="px-1 py-0.5 rounded bg-[var(--color-surface-container)] text-xs font-[var(--font-mono)]">/schedule</code> in any existing session to create one.
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => openModal('new-task')}>+ New task</Button>
|
||||
</div>
|
||||
|
||||
{/* Desktop-online notice */}
|
||||
<div className="flex items-center gap-2.5 px-3.5 py-2.5 rounded-[var(--radius-md)] bg-[var(--color-warning)]/8 border border-[var(--color-warning)]/15 mb-6">
|
||||
<span className="material-symbols-outlined text-[18px] text-[var(--color-warning)]">schedule</span>
|
||||
<span className="text-xs text-[var(--color-text-secondary)]">
|
||||
Scheduled tasks only run while the desktop app is open. Make sure it stays running for tasks to fire on time.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
{!initialized && isLoading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
@ -42,10 +50,12 @@ export function ScheduledTasks() {
|
||||
</div>
|
||||
|
||||
{/* New Task Modal */}
|
||||
<NewTaskModal
|
||||
open={activeModal === 'new-task'}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
{activeModal === 'new-task' && (
|
||||
<NewTaskModal
|
||||
open
|
||||
onClose={closeModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user