mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-08-02 16:51:13 +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' },
|
{ value: 'max', label: 'Max' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export function ModelSelector() {
|
type Props = {
|
||||||
const { currentModel, availableModels, effortLevel, setModel, setEffort } = useSettingsStore()
|
/** 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 [open, setOpen] = useState(false)
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const isControlled = value !== undefined
|
||||||
|
const selectedModel = isControlled ? availableModels.find((m) => m.id === value) || null : storeModel
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return
|
if (!open) return
|
||||||
const handleClick = (e: MouseEvent) => {
|
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"
|
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 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>
|
<span className="material-symbols-outlined text-[12px]">expand_more</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -64,11 +74,18 @@ export function ModelSelector() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{availableModels.map((model) => {
|
{availableModels.map((model) => {
|
||||||
const isSelected = model.id === currentModel?.id
|
const isSelected = model.id === selectedModel?.id
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={model.id}
|
key={model.id}
|
||||||
onClick={() => { setModel(model.id); setOpen(false) }}
|
onClick={() => {
|
||||||
|
if (isControlled) {
|
||||||
|
onChange?.(model.id)
|
||||||
|
} else {
|
||||||
|
setModel(model.id)
|
||||||
|
}
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
className={`
|
className={`
|
||||||
w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors
|
w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-left transition-colors
|
||||||
${isSelected
|
${isSelected
|
||||||
@ -104,8 +121,8 @@ export function ModelSelector() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Effort */}
|
{/* Effort — hidden in controlled mode (not relevant for task creation) */}
|
||||||
<div className="border-t border-[var(--color-border)] p-3">
|
{!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">
|
<div className="text-[10px] font-bold uppercase tracking-widest text-[var(--color-outline)] mb-2 px-1">
|
||||||
Effort
|
Effort
|
||||||
</div>
|
</div>
|
||||||
@ -129,7 +146,7 @@ export function ModelSelector() {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -56,8 +56,16 @@ const MODE_LABELS: Record<PermissionMode, string> = {
|
|||||||
dontAsk: 'Don\'t ask',
|
dontAsk: 'Don\'t ask',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PermissionModeSelector({ workDir: workDirProp }: { workDir?: string } = {}) {
|
type Props = {
|
||||||
const { permissionMode, setPermissionMode } = useSettingsStore()
|
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 setSessionPermissionMode = useChatStore((s) => s.setSessionPermissionMode)
|
||||||
const sessions = useSessionStore((s) => s.sessions)
|
const sessions = useSessionStore((s) => s.sessions)
|
||||||
const activeSessionId = useSessionStore((s) => s.activeSessionId)
|
const activeSessionId = useSessionStore((s) => s.activeSessionId)
|
||||||
@ -65,6 +73,9 @@ export function PermissionModeSelector({ workDir: workDirProp }: { workDir?: str
|
|||||||
const [confirmDialog, setConfirmDialog] = useState(false)
|
const [confirmDialog, setConfirmDialog] = useState(false)
|
||||||
const ref = useRef<HTMLDivElement>(null)
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const isControlled = value !== undefined
|
||||||
|
const currentMode = isControlled ? value : storeMode
|
||||||
|
|
||||||
const activeSession = sessions.find((s) => s.id === activeSessionId)
|
const activeSession = sessions.find((s) => s.id === activeSessionId)
|
||||||
const workDir = workDirProp || activeSession?.workDir || '~'
|
const workDir = workDirProp || activeSession?.workDir || '~'
|
||||||
|
|
||||||
@ -90,8 +101,8 @@ export function PermissionModeSelector({ workDir: workDirProp }: { workDir?: str
|
|||||||
onClick={() => setOpen(!open)}
|
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"
|
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 className="material-symbols-outlined text-[14px]">{MODE_ICONS[currentMode]}</span>
|
||||||
<span>{MODE_LABELS[permissionMode]}</span>
|
<span>{MODE_LABELS[currentMode]}</span>
|
||||||
<span className="material-symbols-outlined text-[12px]">expand_more</span>
|
<span className="material-symbols-outlined text-[12px]">expand_more</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -109,14 +120,18 @@ export function PermissionModeSelector({ workDir: workDirProp }: { workDir?: str
|
|||||||
setConfirmDialog(true)
|
setConfirmDialog(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
void setPermissionMode(item.value)
|
if (isControlled) {
|
||||||
setSessionPermissionMode(item.value)
|
onChange?.(item.value)
|
||||||
|
} else {
|
||||||
|
void setPermissionMode(item.value)
|
||||||
|
setSessionPermissionMode(item.value)
|
||||||
|
}
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}
|
}}
|
||||||
className={`
|
className={`
|
||||||
w-full flex items-start gap-3 px-4 py-3 text-left transition-colors
|
w-full flex items-start gap-3 px-4 py-3 text-left transition-colors
|
||||||
hover:bg-[var(--color-surface-hover)]
|
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)]'}`}>
|
<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-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 className="text-xs text-[var(--color-text-tertiary)] mt-0.5">{item.description}</div>
|
||||||
</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" }}>
|
<span className="material-symbols-outlined text-[16px] text-[var(--color-brand)] mt-0.5" style={{ fontVariationSettings: "'FILL' 1" }}>
|
||||||
check_circle
|
check_circle
|
||||||
</span>
|
</span>
|
||||||
@ -189,8 +204,12 @@ export function PermissionModeSelector({ workDir: workDirProp }: { workDir?: str
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void setPermissionMode('bypassPermissions')
|
if (isControlled) {
|
||||||
setSessionPermissionMode('bypassPermissions')
|
onChange?.('bypassPermissions')
|
||||||
|
} else {
|
||||||
|
void setPermissionMode('bypassPermissions')
|
||||||
|
setSessionPermissionMode('bypassPermissions')
|
||||||
|
}
|
||||||
setConfirmDialog(false)
|
setConfirmDialog(false)
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 text-xs font-semibold text-white bg-[var(--color-error)] hover:opacity-90 rounded-lg transition-colors"
|
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 { useState } from 'react'
|
||||||
import { useTaskStore } from '../../stores/taskStore'
|
import { useTaskStore } from '../../stores/taskStore'
|
||||||
import { useSettingsStore } from '../../stores/settingsStore'
|
import { useSessionStore } from '../../stores/sessionStore'
|
||||||
import { Modal } from '../shared/Modal'
|
import { Modal } from '../shared/Modal'
|
||||||
import { Input } from '../shared/Input'
|
import { Input } from '../shared/Input'
|
||||||
import { Textarea } from '../shared/Textarea'
|
|
||||||
import { Button } from '../shared/Button'
|
import { Button } from '../shared/Button'
|
||||||
|
import { PromptEditor } from './PromptEditor'
|
||||||
|
import type { PermissionMode } from '../../types/settings'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open: boolean
|
open: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const FREQUENCY_OPTIONS = [
|
type FrequencyKey = 'hourly' | 'daily' | 'weekdays' | 'weekly' | 'monthly'
|
||||||
{ value: '0 * * * *', label: 'Hourly' },
|
|
||||||
{ value: '0 9 * * *', label: 'Daily' },
|
const FREQUENCY_OPTIONS: Array<{ value: FrequencyKey; label: string; showTime: boolean }> = [
|
||||||
{ value: '0 9 * * 1-5', label: 'Weekdays' },
|
{ value: 'hourly', label: 'Hourly', showTime: false },
|
||||||
{ value: '0 9 * * 1', label: 'Weekly' },
|
{ value: 'daily', label: 'Daily', showTime: true },
|
||||||
{ value: '0 9 1 * *', label: 'Monthly' },
|
{ value: 'weekdays', label: 'Weekdays', showTime: true },
|
||||||
|
{ value: 'weekly', label: 'Weekly', showTime: true },
|
||||||
|
{ value: 'monthly', label: 'Monthly', showTime: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
const PERMISSION_OPTIONS = [
|
function buildCron(freq: FrequencyKey, time: string): string {
|
||||||
{ value: '', label: 'Default' },
|
const [hours, minutes] = time.split(':').map(Number)
|
||||||
{ value: 'ask', label: 'Ask permissions' },
|
switch (freq) {
|
||||||
{ value: 'auto-accept', label: 'Auto accept edits' },
|
case 'hourly': return '0 * * * *'
|
||||||
{ value: 'plan', label: 'Plan mode' },
|
case 'daily': return `${minutes} ${hours} * * *`
|
||||||
{ value: 'bypass', label: 'Bypass permissions' },
|
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) {
|
export function NewTaskModal({ open, onClose }: Props) {
|
||||||
const { createTask } = useTaskStore()
|
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 [name, setName] = useState('')
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState('')
|
||||||
const [prompt, setPrompt] = 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 [model, setModel] = useState('')
|
||||||
const [permissionMode, setPermissionMode] = useState('')
|
const [permissionMode, setPermissionMode] = useState<PermissionMode>('default')
|
||||||
const [folderPath, setFolderPath] = useState('')
|
const [folderPath, setFolderPath] = useState(defaultWorkDir)
|
||||||
|
const [useWorktree, setUseWorktree] = useState(false)
|
||||||
const [isSubmitting, setIsSubmitting] = 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 () => {
|
const handleSubmit = async () => {
|
||||||
if (!canSubmit) return
|
if (!canSubmit) return
|
||||||
@ -48,24 +60,26 @@ export function NewTaskModal({ open, onClose }: Props) {
|
|||||||
try {
|
try {
|
||||||
await createTask({
|
await createTask({
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
description: description.trim() || undefined,
|
description: description.trim(),
|
||||||
cron,
|
cron: buildCron(frequency, time),
|
||||||
prompt: prompt.trim(),
|
prompt: prompt.trim(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
recurring: true,
|
recurring: true,
|
||||||
model: model || undefined,
|
model: model || undefined,
|
||||||
permissionMode: permissionMode || undefined,
|
permissionMode: permissionMode !== 'default' ? permissionMode : undefined,
|
||||||
folderPath: folderPath.trim() || undefined,
|
folderPath: folderPath.trim() || undefined,
|
||||||
|
useWorktree: useWorktree || undefined,
|
||||||
})
|
})
|
||||||
// Reset form
|
// Reset form
|
||||||
setName('')
|
setName('')
|
||||||
setDescription('')
|
setDescription('')
|
||||||
setPrompt('')
|
setPrompt('')
|
||||||
setCron('0 9 * * *')
|
setFrequency('daily')
|
||||||
|
setTime('09:00')
|
||||||
setModel('')
|
setModel('')
|
||||||
setPermissionMode('')
|
setPermissionMode('default')
|
||||||
setFolderPath('')
|
setFolderPath('')
|
||||||
setShowAdvanced(false)
|
setUseWorktree(false)
|
||||||
onClose()
|
onClose()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to create task:', 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 (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
open={open}
|
open={open}
|
||||||
@ -89,8 +101,8 @@ export function NewTaskModal({ open, onClose }: Props) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* Info banner */}
|
{/* 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">
|
<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="text-[var(--color-text-secondary)] text-sm">ℹ</span>
|
<span className="material-symbols-outlined text-[18px] text-[var(--color-text-secondary)]">info</span>
|
||||||
<span className="text-xs text-[var(--color-text-secondary)]">
|
<span className="text-xs text-[var(--color-text-secondary)]">
|
||||||
Local tasks only run while your computer is awake.
|
Local tasks only run while your computer is awake.
|
||||||
</span>
|
</span>
|
||||||
@ -107,80 +119,61 @@ export function NewTaskModal({ open, onClose }: Props) {
|
|||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="Description"
|
label="Description"
|
||||||
|
required
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
placeholder="Review yesterday's commits..."
|
placeholder="Review yesterday's commits and flag anything concerning"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Textarea
|
{/* Prompt editor with embedded controls */}
|
||||||
label="Prompt"
|
<PromptEditor
|
||||||
required
|
|
||||||
value={prompt}
|
value={prompt}
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
onChange={setPrompt}
|
||||||
placeholder="Look at the commits from the last 24 hours. Summarize what changed, call out any risky patterns..."
|
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 */}
|
{/* Frequency */}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label className="text-sm font-medium text-[var(--color-text-primary)]">Frequency</label>
|
<label className="text-sm font-medium text-[var(--color-text-primary)]">Frequency</label>
|
||||||
<select value={cron} onChange={(e) => setCron(e.target.value)} className={selectClass}>
|
<div className="relative">
|
||||||
{FREQUENCY_OPTIONS.map((opt) => (
|
<select
|
||||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
value={frequency}
|
||||||
))}
|
onChange={(e) => setFrequency(e.target.value as FrequencyKey)}
|
||||||
</select>
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Advanced toggle */}
|
{/* Time picker — shown when frequency supports specific time */}
|
||||||
<button
|
{showTime && (
|
||||||
type="button"
|
<div className="flex flex-col gap-1">
|
||||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
<input
|
||||||
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"
|
type="time"
|
||||||
>
|
value={time}
|
||||||
<span
|
onChange={(e) => setTime(e.target.value)}
|
||||||
className="material-symbols-outlined text-[16px] transition-transform"
|
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={{ transform: showAdvanced ? 'rotate(90deg)' : 'rotate(0deg)' }}
|
style={{ maxWidth: 120 }}
|
||||||
>
|
|
||||||
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"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-xs text-[var(--color-text-tertiary)]">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</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="flex-1 overflow-y-auto">
|
||||||
<div className="px-10 py-8">
|
<div className="px-10 py-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-[var(--color-text-primary)]">Scheduled tasks</h1>
|
<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)]">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => openModal('new-task')}>+ New task</Button>
|
<Button onClick={() => openModal('new-task')}>+ New task</Button>
|
||||||
</div>
|
</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 */}
|
{/* Content */}
|
||||||
{!initialized && isLoading ? (
|
{!initialized && isLoading ? (
|
||||||
<div className="flex items-center justify-center py-16">
|
<div className="flex items-center justify-center py-16">
|
||||||
@ -42,10 +50,12 @@ export function ScheduledTasks() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* New Task Modal */}
|
{/* New Task Modal */}
|
||||||
<NewTaskModal
|
{activeModal === 'new-task' && (
|
||||||
open={activeModal === 'new-task'}
|
<NewTaskModal
|
||||||
onClose={closeModal}
|
open
|
||||||
/>
|
onClose={closeModal}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user