/** * UserTaskComposer — modal form for creating a user-owned task. * * Tasks default to the personal board ({@link USER_TASKS_THREAD_ID}) and * are *optionally* attachable to an existing conversation thread. On * submit it calls `todosApi.add` and hands the resulting board back to the * parent via `onCreated` so the Tasks tab can refresh in place. */ import debug from 'debug'; import { useState } from 'react'; import { LuX } from 'react-icons/lu'; import { useT } from '../../lib/i18n/I18nContext'; import { todosApi, USER_TASKS_THREAD_ID } from '../../services/api/todosApi'; import { useAppSelector } from '../../store/hooks'; import type { TaskBoard, TaskBoardCardStatus } from '../../types/turnState'; const log = debug('intelligence:task-composer'); // Tasks use three states only: Pending / Working / Done. const STATUS_OPTIONS: { value: TaskBoardCardStatus; labelKey: string }[] = [ { value: 'todo', labelKey: 'conversations.taskKanban.pending' }, { value: 'in_progress', labelKey: 'conversations.taskKanban.working' }, { value: 'done', labelKey: 'conversations.taskKanban.done' }, ]; interface UserTaskComposerProps { /** Called with the updated board for the thread the task landed on. */ onCreated: (threadId: string, board: TaskBoard) => void; onClose: () => void; } export function UserTaskComposer({ onCreated, onClose }: UserTaskComposerProps) { const { t } = useT(); const threads = useAppSelector(state => state.thread.threads ?? []); const [title, setTitle] = useState(''); const [status, setStatus] = useState('todo'); const [objective, setObjective] = useState(''); const [notes, setNotes] = useState(''); const [attachThreadId, setAttachThreadId] = useState(''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); // Only user-initiated conversations are attachable; background // worker/subagent threads (those with a parent) would be confusing // targets for a manual task. const attachableThreads = threads.filter(thread => !thread.parentThreadId); const canSubmit = title.trim().length > 0 && !submitting; const handleSubmit = async () => { const trimmedTitle = title.trim(); if (!trimmedTitle || submitting) return; const threadId = attachThreadId || USER_TASKS_THREAD_ID; setSubmitting(true); setError(null); log('submit threadId=%s status=%s', threadId, status); try { const board = await todosApi.add({ threadId, content: trimmedTitle, status, objective: objective.trim() || null, notes: notes.trim() || null, }); onCreated(threadId, board); onClose(); } catch (err) { const msg = err instanceof Error ? err.message : String(err); log('submit failed: %s', msg); setError(msg); } finally { setSubmitting(false); } }; return (

{t('intelligence.tasks.composer.title')}