/** * Task detail: the full view of one task — content, prompt, execution * history — and the only place execution can be triggered. Also offers * delete (with confirmation), manual status moves, and a jump to the * execution's session transcript. */ import { useEffect, useState } from 'react' import type { BoardController } from '../../core/controller.ts' import { isValidCron } from '../../core/schedule.ts' import { MANUAL_STATUSES, TASK_PERMISSIONS, tagTone, type ExecutionRecord, type TaskPermission, type TaskRecord } from '../../core/tasks.ts' import { canEditTaskContent } from '../../core/use-cases/task-update.ts' import { requiresPermissionConfirmation } from '../../core/handover.ts' import { t, type TaskBoardKey } from '../locales.ts' import { SCHEDULE_PRESETS } from '../schedule-presets.ts' import css from '../board.module.css' import { ConfirmDialog } from './ConfirmDialog.tsx' import { EditTaskModal, EditTagsModal } from './EditTaskModal.tsx' import { NewTaskModal } from './NewTaskModal.tsx' import { formatHostTimestamp, formatTime } from './TaskCard.tsx' import { STATUS_KEY } from './status-key.ts' /** Execution outcome → locale key. */ const RESULT_KEY: Record, TaskBoardKey> = { succeeded: 'detail.result.succeeded', failed: 'detail.result.failed', cancelled: 'detail.result.cancelled', } /** One execution-history row. */ function ExecutionRow({ execution, timeZone, onOpen }: { execution: ExecutionRecord; timeZone?: string; onOpen: (sessionId: string) => void }) { const result = execution.result return (
  • {result === undefined ? t('detail.result.running') : t(RESULT_KEY[result])} {t('detail.executionStarted')} {formatTime(execution.startedAt, timeZone)} {execution.endedAt !== undefined && ` · ${t('detail.executionEnded')} ${formatTime(execution.endedAt, timeZone)}`} {execution.initiatedBy !== undefined && ( {t('detail.execution.initiator', { session: execution.initiatedBy })} )} {execution.sessionId !== undefined && ( )} {execution.error !== undefined && execution.error !== '' && ( {execution.error} )}
  • ) } /** The execution-target editor: workspace / mode / permission pickers. */ function ExecutionSettingsSection({ controller, task, pending }: { controller: BoardController; task: TaskRecord; pending: boolean }) { const [options, setOptions] = useState(controller.getSnapshot().executionOptions) useEffect( () => controller.subscribe(() => setOptions(controller.getSnapshot().executionOptions)), [controller], ) const workspaceId = task.workspaceId ?? '' const mode = task.mode ?? '' const permission = task.permission ?? '' const model = task.model ?? '' // A pinned target may disappear from the runtime (workspace deleted, // preset removed); keep it selectable as a stale row instead of silently // dropping it, so the user sees exactly what the task will ask for. const workspaceKnown = workspaceId === '' || options.workspaces.some(item => item.workspaceId === workspaceId) const modeKnown = mode === '' || options.presets.some(item => item.id === mode) const modelKnown = model === '' || (options.models ?? []).some(item => item.id === model) return (

    {t('detail.executionSettings')}

    {t('exec.hint')}

    {t('exec.reuseSessionHint')}

    ) } /** The scheduled-runs editor: enable toggle, cron input + presets, next-run info. */ function ScheduleSection({ controller, task, pending }: { controller: BoardController; task: TaskRecord; pending: boolean }) { const schedule = task.schedule const [cron, setCron] = useState(schedule?.cron ?? '0 9 * * *') const [enabled, setEnabled] = useState(schedule?.enabled ?? false) const [nextRunAt, setNextRunAt] = useState(schedule?.nextRunAt) const [lastTriggeredAt, setLastTriggeredAt] = useState(schedule?.lastTriggeredAt) const [error, setError] = useState(undefined) const timeZone = controller.getSnapshot().host?.scheduler.timeZone // Keep the editor in sync when the task record changes underneath (the // schedule rolls forward as runs trigger). useEffect(() => { setCron(schedule?.cron ?? '0 9 * * *') setEnabled(schedule?.enabled ?? false) setNextRunAt(schedule?.nextRunAt) setLastTriggeredAt(schedule?.lastTriggeredAt) setError(undefined) }, [task.id, schedule?.enabled, schedule?.cron, schedule?.nextRunAt, schedule?.lastTriggeredAt]) /** Validate + persist the current cron text (Enter or blur). */ const saveCron = (value: string): void => { const trimmed = value.trim() setCron(trimmed) if (trimmed === '' || !isValidCron(trimmed)) { setError(t('detail.schedule.invalid')) return } setError(undefined) controller.setSchedule(task.id, { cron: trimmed }) } /** Arm/disarm the schedule (arming first persists the edited cron). */ const toggleEnabled = (next: boolean): void => { const trimmed = cron.trim() if (next && (trimmed === '' || !isValidCron(trimmed))) { setError(t('detail.schedule.invalid')) return } setError(undefined) const submitted = controller.setSchedule(task.id, { enabled: next, ...(next && trimmed !== schedule?.cron ? { cron: trimmed } : {}), }) if (submitted && !controller.isHostBacked()) setEnabled(next) } const applyPreset = (preset: string): void => { if (preset === '') return setCron(preset) setError(undefined) controller.setSchedule(task.id, { cron: preset }) } const nextLabel = !enabled || nextRunAt === undefined ? t('detail.schedule.notScheduled') : nextRunAt <= Date.now() ? t('detail.schedule.dueSoon') : formatHostTimestamp(nextRunAt, timeZone) const lastLabel = lastTriggeredAt === undefined ? '—' : formatHostTimestamp(lastTriggeredAt, timeZone) return (

    {t('detail.schedule')}

    { setCron(event.target.value); setError(undefined) }} onBlur={() => { saveCron(cron) }} onKeyDown={event => { if (event.key === 'Enter') saveCron(cron) }} />
    {error !== undefined &&

    {error}

    }

    {t('detail.schedule.nextRun')} {nextLabel} {' · '}{t('detail.schedule.lastTriggered')} {lastLabel}

    ) } /** Task detail overlay. */ export function TaskDetail({ controller, task }: { controller: BoardController; task: TaskRecord }) { const [confirmDelete, setConfirmDelete] = useState(false) const [showEdit, setShowEdit] = useState(false) const [showEditTags, setShowEditTags] = useState(false) const [showDuplicate, setShowDuplicate] = useState(false) // Keep the overlay in sync if the task record changes underneath. const [latest, setLatest] = useState(task) useEffect(() => { setLatest(task) }, [task]) // A re-used overlay instance must not carry an edit session across tasks. useEffect(() => { setShowEdit(false) setShowEditTags(false) setShowDuplicate(false) }, [task.id]) const current = latest const snapshot = controller.getSnapshot() const running = current.status === 'running' const archived = current.archivedAt !== undefined const pending = snapshot.pendingTaskIds.includes(current.id) const transportError = snapshot.transportError const timeZone = snapshot.host?.scheduler.timeZone const permissionPending = requiresPermissionConfirmation(current, snapshot.host?.sessionDefaultPermission) return (
    { if (event.target === event.currentTarget) controller.closeTask() }}>

    {current.title}

    {archived ? t('board.archive') : t(STATUS_KEY[current.status])}
    {transportError !== undefined && (
    {t('board.hostError', { error: transportError })}{' '}
    )}

    {t('detail.description')}

    {current.description !== '' ? current.description : '—'}

    {current.tags !== undefined && current.tags.length > 0 && (

    {t('new.tags')}

    {current.tags.map(tag => ( {tag.name} ))}
    )} {current.freeze !== undefined && (

    {t('detail.freeze')}

    {current.freeze.redacted === true &&

    {t('detail.freeze.redacted')}

    }

    {t('detail.freeze.goal')}

    {current.freeze.goal}

    {t('detail.freeze.progress')}

    {current.freeze.progress}

    {t('detail.freeze.next')}

    {current.freeze.next}

    {t('detail.freeze.frozenAt', { time: formatHostTimestamp(current.freeze.frozenAt, timeZone) })}

    {current.freeze.frozenBy !== undefined && (

    {t('detail.freeze.frozenBy', { session: current.freeze.frozenBy })}

    )}
    )} {current.handover !== undefined && (

    {t('detail.handover')}

    {t('new.workspace')}: {current.handover.workspaceId ?? t('exec.workspace.recent')} {' · '}{t('new.mode')}: {current.handover.mode ?? t('exec.mode.default')} {' · '}{t('new.permission')}: {current.handover.permission === undefined ? t('exec.permission.default') : t(`exec.permission.${current.handover.permission}` as TaskBoardKey)}

    {t('detail.handover.references')}

      {current.handover.references.map((reference, index) => ( // References are free text from the freeze block, so the same // string can appear twice; the index keeps the key unique (#1492).
    • {reference}
    • ))}

    {t('detail.handover.bundledAt', { time: formatHostTimestamp(current.handover.bundledAt, timeZone) })}

    )} {permissionPending && (

    {t('detail.permissionPending', { permission: t(`exec.permission.${current.handover?.permission ?? current.permission}` as TaskBoardKey) })}

    )} {current.permissionConfirmedAt !== undefined && (

    {t('detail.permissionConfirmed', { time: formatHostTimestamp(current.permissionConfirmedAt, timeZone) })}

    )}

    {t('detail.prompt')}

    {current.prompt !== '' ? current.prompt : current.title}
    {!archived && ( <> )}

    {t('detail.execution')}

    {current.executions.length === 0 ? (

    {t('detail.noExecution')}

    ) : (
      {[...current.executions].reverse().map(execution => ( { controller.openSession(sessionId) }} /> ))}
    )}
    {!archived && (

    {t('board.status')}

    {MANUAL_STATUSES.map(status => ( ))}
    )}
    {!archived && pending && {t('board.pending')}…} {!archived && canEditTaskContent(current) && ( )} {!archived && !canEditTaskContent(current) && current.status !== 'running' && ( )} {!archived && ( )} {!archived && ( )} {archived ? ( ) : ( (current.status === 'done' || current.status === 'failed') && ( ) )} {t('board.created')} {formatTime(current.createdAt, timeZone)} {archived && ` · ${t('detail.archivedAt', { time: formatTime(current.archivedAt!, timeZone) })}`}
    {confirmDelete && ( { setConfirmDelete(false) }} onConfirm={() => { setConfirmDelete(false) controller.deleteTask(current.id) }} /> )} {showEdit && !archived && canEditTaskContent(current) && ( { setShowEdit(false) }} /> )} {showEditTags && !archived && current.status !== 'running' && ( { setShowEditTags(false) }} /> )} {showDuplicate && !archived && ( { setShowDuplicate(false) }} onDuplicateSuccess={async (sourceId) => { await controller.archiveTask(sourceId) controller.closeTask() }} /> )}
    ) }