diff --git a/packages/builtin-tool-task/src/client/executor/index.ts b/packages/builtin-tool-task/src/client/executor/index.ts index 8c48974031..e1c2d97a5d 100644 --- a/packages/builtin-tool-task/src/client/executor/index.ts +++ b/packages/builtin-tool-task/src/client/executor/index.ts @@ -662,11 +662,9 @@ class TaskExecutor extends BaseExecutor { ): Promise => { try { log('[TaskExecutor] updateTaskComment - commentId:', params.commentId); - await getTaskStoreState().updateComment( - params.commentId, - params.content, - ctx?.taskId ?? undefined, - ); + await getTaskStoreState().updateComment(params.commentId, params.content, { + taskId: ctx?.taskId ?? undefined, + }); return { content: `Comment ${params.commentId} updated.`, diff --git a/packages/database/src/models/task.ts b/packages/database/src/models/task.ts index c6b791f459..5671151986 100644 --- a/packages/database/src/models/task.ts +++ b/packages/database/src/models/task.ts @@ -37,7 +37,6 @@ export class TaskModel { const maxRetries = 5; for (let attempt = 0; attempt < maxRetries; attempt++) { try { - // Get next seq for this user const seqResult = await this.db .select({ maxSeq: sql`COALESCE(MAX(${tasks.seq}), 0)` }) .from(tasks) @@ -46,7 +45,7 @@ export class TaskModel { const nextSeq = Number(seqResult[0].maxSeq) + 1; const identifier = `${identifierPrefix}-${nextSeq}`; - const result = await this.db + const [task] = await this.db .insert(tasks) .values({ ...rest, @@ -56,7 +55,7 @@ export class TaskModel { } as NewTask) .returning(); - return result[0]; + return task; } catch (error: any) { // Retry on unique constraint violation (concurrent seq conflict) // Check error itself, cause, and stringified message for PG error code 23505 @@ -114,13 +113,14 @@ export class TaskModel { id: string, data: Partial>, ): Promise { - const result = await this.db + if (Object.keys(data).length === 0) return this.findById(id); + + const updated = await this.db .update(tasks) .set({ ...data, updatedAt: new Date() }) .where(and(eq(tasks.id, id), eq(tasks.createdByUserId, this.userId))) .returning(); - - return result[0] || null; + return updated[0] || null; } async delete(id: string): Promise { @@ -742,8 +742,6 @@ export class TaskModel { return comment; } - // ========== Comments ========== - async getComments(taskId: string): Promise { return this.db .select() @@ -756,15 +754,22 @@ export class TaskModel { const result = await this.db .delete(taskComments) .where(and(eq(taskComments.id, id), eq(taskComments.userId, this.userId))) - .returning(); return result.length > 0; } - async updateComment(id: string, content: string): Promise { + async updateComment( + id: string, + content: string, + opts?: { editorData?: unknown }, + ): Promise { const [comment] = await this.db .update(taskComments) - .set({ content, updatedAt: new Date() }) + .set({ + content, + ...(opts?.editorData !== undefined ? { editorData: opts.editorData as never } : {}), + updatedAt: new Date(), + }) .where(and(eq(taskComments.id, id), eq(taskComments.userId, this.userId))) .returning(); return comment; diff --git a/packages/prompts/src/prompts/task/index.ts b/packages/prompts/src/prompts/task/index.ts index 51f10ccee5..4ae5e8f729 100644 --- a/packages/prompts/src/prompts/task/index.ts +++ b/packages/prompts/src/prompts/task/index.ts @@ -300,10 +300,21 @@ export const formatCheckpointCreated = (reason: string): string => // ── Task Run Prompt Builder ── +export interface TaskRunPromptAttachment { + fileType?: string; + id: string; + name: string; +} + export interface TaskRunPromptComment { agentId?: string | null; content: string; createdAt?: string; + /** Lightweight metadata of files attached to this comment. The actual file + * content (image bytes / parsed text) is passed to the agent runtime as + * multimodal `fileIds`; this list is just so the LLM knows what files exist + * and which comment they were attached to. */ + files?: TaskRunPromptAttachment[]; id?: string; } @@ -373,6 +384,9 @@ export interface TaskRunPromptInput { assigneeAgentId?: string | null; dependencies?: Array<{ dependsOn: string; type: string }>; description?: string | null; + /** Lightweight metadata of files attached to the task instruction. Actual + * content is forwarded to the agent runtime via `fileIds` on execAgent. */ + files?: TaskRunPromptAttachment[]; id: string; identifier: string; instruction: string; @@ -453,7 +467,11 @@ export const buildTaskRunPrompt = (input: TaskRunPromptInput, now?: Date): strin const ago = c.createdAt ? timeAgo(c.createdAt, now) : ''; const timeAttr = ago ? ` time="${ago}"` : ''; const idAttr = c.id ? ` id="${c.id}"` : ''; - return `${c.content}`; + const attachments = + c.files && c.files.length > 0 + ? `\n\n${c.files.map((f) => ` - ${f.name}${f.fileType ? ` (${f.fileType})` : ''}`).join('\n')}\n` + : ''; + return `${c.content}${attachments}`; }); sections.push(`\n${lines.join('\n')}\n`); } @@ -467,6 +485,12 @@ export const buildTaskRunPrompt = (input: TaskRunPromptInput, now?: Date): strin `Instruction: ${task.instruction}`, ]; if (task.description) taskLines.push(`Description: ${task.description}`); + if (task.files && task.files.length > 0) { + taskLines.push('Attachments (contents provided separately as multimodal inputs):'); + for (const f of task.files) { + taskLines.push(` - ${f.name}${f.fileType ? ` (${f.fileType})` : ''}`); + } + } if (task.assigneeAgentId) taskLines.push(`Agent: ${task.assigneeAgentId}`); if (task.parentIdentifier) taskLines.push(`Parent: ${task.parentIdentifier}`); diff --git a/packages/types/src/task/index.ts b/packages/types/src/task/index.ts index 53c8ea6241..e5c01a6d03 100644 --- a/packages/types/src/task/index.ts +++ b/packages/types/src/task/index.ts @@ -1,4 +1,5 @@ import type { BriefArtifacts } from '../brief'; +import type { ChatFileItem } from '../message/ui/chat'; // ── Task type aliases ── @@ -128,6 +129,7 @@ export interface TaskItem { createdByUserId: string; currentTopicId: string | null; description: string | null; + editorData: unknown; error: string | null; heartbeatInterval: number | null; heartbeatTimeout: number | null; @@ -166,6 +168,7 @@ export interface NewTask { createdByUserId: string; currentTopicId?: string | null; description?: string | null; + editorData?: unknown; error?: string | null; heartbeatInterval?: number | null; heartbeatTimeout?: number | null; @@ -251,6 +254,10 @@ export interface TaskDetailActivity { content?: string; createdAt?: string; cronJobId?: string | null; + /** Comment-only: rich Lexical JSON state. When present, supersedes `content` for rendering. */ + editorData?: unknown; + /** Comment-only: files attached to this comment for rendering in the UI. */ + files?: ChatFileItem[]; id?: string; /** * Topic-only: persisted Gateway operation ID for the task topic, sourced @@ -296,7 +303,11 @@ export interface TaskDetailData { createdAt?: string; dependencies?: Array<{ dependsOn: string; type: string }>; description?: string | null; + /** Rich-editor JSON state for the instruction; preserves details markdown drops (image size, etc.). */ + editorData?: unknown; error?: string | null; + /** Files attached to the task instruction (persistent context for every run). */ + files?: ChatFileItem[]; // heartbeat.interval: periodic execution interval | heartbeat.timeout+lastAt: watchdog monitoring (detects stuck tasks) heartbeat?: { interval?: number | null; diff --git a/src/features/AgentTasks/AgentTaskDetail/CommentCard.tsx b/src/features/AgentTasks/AgentTaskDetail/CommentCard.tsx index 66026aa531..6a8c58d2a1 100644 --- a/src/features/AgentTasks/AgentTaskDetail/CommentCard.tsx +++ b/src/features/AgentTasks/AgentTaskDetail/CommentCard.tsx @@ -1,5 +1,6 @@ import type { TaskDetailActivity } from '@lobechat/types'; -import { Editor, useEditor } from '@lobehub/editor/react'; +import { useEditor } from '@lobehub/editor/react'; +import { LexicalRenderer } from '@lobehub/editor/renderer'; import { ActionIcon, Avatar, @@ -12,23 +13,43 @@ import { Markdown, Text, } from '@lobehub/ui'; -import { confirmModal } from '@lobehub/ui/base-ui'; +import { App } from 'antd'; import { cssVar } from 'antd-style'; import { MessageCircle, MoreHorizontal, Pencil, Trash } from 'lucide-react'; import { memo, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { AttachmentUploadButton } from '@/features/AttachmentInput'; +import { EditorCanvas } from '@/features/EditorCanvas'; +import { seedAttachments } from '@/features/EditorCanvas/attachmentRegistry'; +import { + getAttachmentFileIdsFromEditor, + insertFilesIntoEditor, +} from '@/features/EditorCanvas/editorAttachments'; +import { LinearFileCard } from '@/features/EditorCanvas/LinearFilePlugin'; import { useActivityTime } from '@/hooks/useActivityTime'; import { useTaskStore } from '@/store/task'; import { styles } from '../shared/style'; +// Keep saved comments visually consistent with the editor: render FileNodes +// as the Linear-style card on its own row instead of the default inline pill. +const FILE_WRAPPER_STYLE = { marginBlock: 8 }; +const rendererOverrides = { + file: (node: Record) => ( +
+ [0]['node']} /> +
+ ), +}; + interface CommentCardProps { activity: TaskDetailActivity; } const CommentCard = memo(({ activity }) => { const { t } = useTranslation('chat'); + const { modal } = App.useApp(); const deleteComment = useTaskStore((s) => s.deleteComment); const updateComment = useTaskStore((s) => s.updateComment); @@ -40,21 +61,42 @@ const CommentCard = memo(({ activity }) => { const content = activity.content || t('taskDetail.activities.fallback.comment'); const commentId = activity.id; + const editorData = useMemo( + () => ({ + content: activity.content ?? '', + editorData: activity.editorData, + }), + [activity.content, activity.editorData], + ); + const handleEdit = useCallback(() => { + // Seed URL→fileId map so attachments serialize back to fileIds on save. + if (activity.files && activity.files.length > 0) { + seedAttachments(activity.files.map((f) => ({ id: f.id, url: f.url }))); + } setIsEditing(true); - }, []); + }, [activity.files]); const handleCancel = useCallback(() => { setIsEditing(false); }, []); + const handleAttach = useCallback( + (files: File[]) => { + insertFilesIntoEditor(editor, files); + }, + [editor], + ); + const handleSave = useCallback(async () => { - if (!commentId) return; + if (!commentId || submitting) return; const next = String(editor?.getDocument?.('markdown') ?? '').trim(); - if (!next || submitting) return; + const json = editor?.getDocument?.('json') as unknown; + const hasFiles = getAttachmentFileIdsFromEditor(editor).length > 0; + if (!next && !hasFiles) return; setSubmitting(true); try { - await updateComment(commentId, next); + await updateComment(commentId, next, { editorData: json }); setIsEditing(false); } finally { setSubmitting(false); @@ -63,14 +105,16 @@ const CommentCard = memo(({ activity }) => { const handleDelete = useCallback(() => { if (!commentId) return; - confirmModal({ + modal.confirm({ + centered: true, content: t('taskDetail.comment.deleteConfirm.content'), okButtonProps: { danger: true }, okText: t('taskDetail.comment.deleteConfirm.ok'), onOk: () => deleteComment(commentId), title: t('taskDetail.comment.deleteConfirm.title'), + type: 'error', }); - }, [commentId, deleteComment, t]); + }, [commentId, deleteComment, modal, t]); const menuItems = useMemo( () => [ @@ -118,26 +162,36 @@ const CommentCard = memo(({ activity }) => { )} - {isEditing ? ( + {isEditing && ( <> - - - - + + + + + + - ) : ( + )} + {!isEditing && Boolean(activity.editorData) && ( + [0]['value']} + variant={'chat'} + /> + )} + {!isEditing && !activity.editorData && ( {content} diff --git a/src/features/AgentTasks/AgentTaskDetail/CommentInput.tsx b/src/features/AgentTasks/AgentTaskDetail/CommentInput.tsx index e67b6d85ca..67a88c6c57 100644 --- a/src/features/AgentTasks/AgentTaskDetail/CommentInput.tsx +++ b/src/features/AgentTasks/AgentTaskDetail/CommentInput.tsx @@ -1,8 +1,15 @@ -import { Editor, SendButton, useEditor } from '@lobehub/editor/react'; +import { SendButton, useEditor } from '@lobehub/editor/react'; import { Avatar, Flexbox } from '@lobehub/ui'; +import { $getRoot } from 'lexical'; import { memo, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { AttachmentUploadButton } from '@/features/AttachmentInput'; +import { EditorCanvas } from '@/features/EditorCanvas'; +import { + getAttachmentFileIdsFromEditor, + insertFilesIntoEditor, +} from '@/features/EditorCanvas/editorAttachments'; import { useEnterToSend } from '@/hooks/useEnterToSend'; import { useUserAvatar } from '@/hooks/useUserAvatar'; import { useTaskStore } from '@/store/task'; @@ -16,53 +23,76 @@ const CommentInput = memo<{ taskId: string }>(({ taskId }) => { const userAvatar = useUserAvatar(); const [submitting, setSubmitting] = useState(false); const [hasContent, setHasContent] = useState(false); + const [hasAttachments, setHasAttachments] = useState(false); const shouldSendOnEnter = useEnterToSend(); + const canSubmit = hasContent || hasAttachments; + + const handleContentChange = useCallback(() => { + const lexicalEditor = editor?.getLexicalEditor?.(); + if (!lexicalEditor) return; + lexicalEditor.getEditorState().read(() => { + const text = $getRoot().getTextContent().trim(); + setHasContent(text.length > 0); + }); + setHasAttachments(getAttachmentFileIdsFromEditor(editor).length > 0); + }, [editor]); + + const handleAttach = useCallback( + (files: File[]) => { + insertFilesIntoEditor(editor, files); + }, + [editor], + ); + const handleSubmit = useCallback(async () => { - const trimmed = String(editor?.getDocument?.('markdown') ?? '').trim(); - if (!trimmed || submitting) return; + if (submitting) return; + const json = editor?.getDocument?.('json') as unknown; + const markdown = String(editor?.getDocument?.('markdown') ?? '').trim(); + const hasFiles = getAttachmentFileIdsFromEditor(editor).length > 0; + if (!markdown && !hasFiles) return; + setSubmitting(true); try { - await addComment(taskId, trimmed); + await addComment(taskId, markdown, { editorData: json }); editor?.cleanDocument?.(); setHasContent(false); + setHasAttachments(false); } finally { setSubmitting(false); } }, [taskId, editor, addComment, submitting]); return ( - - -
- { - setHasContent(!ed?.isEmpty); - }} - onPressEnter={({ event }) => { - if (shouldSendOnEnter(event)) { - handleSubmit(); - return true; - } - }} - /> -
-
- -
+ + + +
+ { + if (shouldSendOnEnter(event)) { + handleSubmit(); + return true; + } + }} + /> +
+ + + + +
); }); diff --git a/src/features/AgentTasks/AgentTaskDetail/TaskInstruction.tsx b/src/features/AgentTasks/AgentTaskDetail/TaskInstruction.tsx index c5d395eef3..adbdf4f4a2 100644 --- a/src/features/AgentTasks/AgentTaskDetail/TaskInstruction.tsx +++ b/src/features/AgentTasks/AgentTaskDetail/TaskInstruction.tsx @@ -1,8 +1,12 @@ import { useEditor } from '@lobehub/editor/react'; +import { ActionIcon, Flexbox } from '@lobehub/ui'; +import { Paperclip } from 'lucide-react'; import { memo, useCallback, useEffect, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { EditorCanvas } from '@/features/EditorCanvas'; +import { seedAttachments } from '@/features/EditorCanvas/attachmentRegistry'; +import { pickAndInsertAttachments } from '@/features/EditorCanvas/editorAttachments'; import { useTaskStore } from '@/store/task'; import { taskDetailSelectors } from '@/store/task/selectors'; @@ -11,14 +15,32 @@ const DEBOUNCE_MS = 300; const TaskInstruction = memo(() => { const { t } = useTranslation('chat'); const instruction = useTaskStore(taskDetailSelectors.activeTaskInstruction); + const persistedEditorData = useTaskStore(taskDetailSelectors.activeTaskEditorData); const taskId = useTaskStore(taskDetailSelectors.activeTaskId); + const persistedFiles = useTaskStore(taskDetailSelectors.activeTaskFiles); const updateTask = useTaskStore((s) => s.updateTask); const editor = useEditor(); const debounceRef = useRef>(undefined); + // Skip save when the serialized state matches the last persisted snapshot — + // Lexical fires content-change for selection moves and other no-op events. + const lastSavedJsonRef = useRef(undefined); - const editorData = useMemo(() => ({ content: instruction ?? '' }), [instruction]); + const editorData = useMemo( + () => ({ + content: instruction ?? '', + editorData: persistedEditorData, + }), + [instruction, persistedEditorData], + ); useEffect(() => { + if (persistedFiles && persistedFiles.length > 0) { + seedAttachments(persistedFiles.map((f) => ({ id: f.id, url: f.url }))); + } + }, [persistedFiles]); + + useEffect(() => { + lastSavedJsonRef.current = undefined; return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; @@ -29,21 +51,38 @@ const TaskInstruction = memo(() => { if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => { + const json = editor.getDocument('json') as unknown; + const jsonSignature = JSON.stringify(json); + if (jsonSignature === lastSavedJsonRef.current) return; + lastSavedJsonRef.current = jsonSignature; + const markdown = String(editor.getDocument('markdown') ?? ''); - updateTask(taskId, { instruction: markdown }).catch((e) => { + updateTask(taskId, { editorData: json, instruction: markdown }).catch((e) => { console.error('[TaskInstruction] Failed to save:', e); }); }, DEBOUNCE_MS); }, [editor, taskId, updateTask]); + const handleAttach = useCallback(() => { + pickAndInsertAttachments(editor); + }, [editor]); + return ( - + + + + ); }); diff --git a/src/features/AgentTasks/AgentTaskDetail/TopicChatDrawer/FeedbackInput.tsx b/src/features/AgentTasks/AgentTaskDetail/TopicChatDrawer/FeedbackInput.tsx index b3bbddf936..0b25fc0616 100644 --- a/src/features/AgentTasks/AgentTaskDetail/TopicChatDrawer/FeedbackInput.tsx +++ b/src/features/AgentTasks/AgentTaskDetail/TopicChatDrawer/FeedbackInput.tsx @@ -1,9 +1,16 @@ -import { Editor, SendButton, useEditor } from '@lobehub/editor/react'; +import { SendButton, useEditor } from '@lobehub/editor/react'; import { Avatar, Flexbox } from '@lobehub/ui'; +import { $getRoot } from 'lexical'; import { memo, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { shallow } from 'zustand/shallow'; +import { AttachmentUploadButton } from '@/features/AttachmentInput'; +import { EditorCanvas } from '@/features/EditorCanvas'; +import { + getAttachmentFileIdsFromEditor, + insertFilesIntoEditor, +} from '@/features/EditorCanvas/editorAttachments'; import { useEnterToSend } from '@/hooks/useEnterToSend'; import { useUserAvatar } from '@/hooks/useUserAvatar'; import { useTaskStore } from '@/store/task'; @@ -29,14 +36,41 @@ const FeedbackInput = memo(({ taskId, topicId }) => { ); const [submitting, setSubmitting] = useState(false); const [hasContent, setHasContent] = useState(false); + const [hasAttachments, setHasAttachments] = useState(false); const shouldSendOnEnter = useEnterToSend(); + const canSubmit = hasContent || hasAttachments; + + const handleContentChange = useCallback(() => { + const lexicalEditor = editor?.getLexicalEditor?.(); + if (!lexicalEditor) return; + lexicalEditor.getEditorState().read(() => { + const text = $getRoot().getTextContent().trim(); + setHasContent(text.length > 0); + }); + setHasAttachments(getAttachmentFileIdsFromEditor(editor).length > 0); + }, [editor]); + + const handleAttach = useCallback( + (files: File[]) => { + insertFilesIntoEditor(editor, files); + }, + [editor], + ); + const handleSubmit = useCallback(async () => { - const trimmed = String(editor?.getDocument?.('markdown') ?? '').trim(); - if (!trimmed || submitting) return; + if (submitting) return; + const json = editor?.getDocument?.('json') as unknown; + const markdown = String(editor?.getDocument?.('markdown') ?? '').trim(); + const hasFiles = getAttachmentFileIdsFromEditor(editor).length > 0; + if (!markdown && !hasFiles) return; + setSubmitting(true); try { - await addComment(taskId, trimmed, { topicId }); + await addComment(taskId, markdown, { + editorData: json, + topicId, + }); // Start a NEW topic run that picks up the comment we just attached to the // current topic. Do NOT pass continueTopicId — that would flip the // already-completed topic back to running and overwrite its operation id. @@ -47,6 +81,7 @@ const FeedbackInput = memo(({ taskId, topicId }) => { } editor?.cleanDocument?.(); setHasContent(false); + setHasAttachments(false); closeTopicDrawer(); } finally { setSubmitting(false); @@ -54,38 +89,36 @@ const FeedbackInput = memo(({ taskId, topicId }) => { }, [taskId, topicId, editor, addComment, runTask, closeTopicDrawer, submitting]); return ( - - -
- { - setHasContent(!ed?.isEmpty); - }} - onPressEnter={({ event }) => { - if (shouldSendOnEnter(event)) { - handleSubmit(); - return true; - } - }} - /> -
-
- -
+ + + +
+ { + if (shouldSendOnEnter(event)) { + handleSubmit(); + return true; + } + }} + /> +
+ + + + +
); }); diff --git a/src/features/AgentTasks/AgentTaskList/CreateTaskInlineEntry.tsx b/src/features/AgentTasks/AgentTaskList/CreateTaskInlineEntry.tsx index eb5b048317..2c39c4971a 100644 --- a/src/features/AgentTasks/AgentTaskList/CreateTaskInlineEntry.tsx +++ b/src/features/AgentTasks/AgentTaskList/CreateTaskInlineEntry.tsx @@ -5,11 +5,15 @@ import { ActionIcon, Block, Flexbox, Icon, Text } from '@lobehub/ui'; import { Button } from 'antd'; import { cssVar } from 'antd-style'; import { $getRoot } from 'lexical'; -import { ChevronUp, UserCircle2 } from 'lucide-react'; +import { ChevronUp, Paperclip, UserCircle2 } from 'lucide-react'; import { type KeyboardEvent, memo, useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { EditorCanvas } from '@/features/EditorCanvas'; +import { + getAttachmentFileIdsFromEditor, + pickAndInsertAttachments, +} from '@/features/EditorCanvas/editorAttachments'; import { useGlobalStore } from '@/store/global'; import { useTaskStore } from '@/store/task'; @@ -52,6 +56,7 @@ const CreateTaskInlineEntry = memo((props) => { const [priority, setPriority] = useState(0); const [assigneeAgentId, setAssigneeAgentId] = useState(agentId); const [instruction, setInstruction] = useState(''); + const [hasAttachments, setHasAttachments] = useState(false); const editor = useEditor(); @@ -75,22 +80,35 @@ const CreateTaskInlineEntry = memo((props) => { lexicalEditor.getEditorState().read(() => { setInstruction($getRoot().getTextContent()); }); + setHasAttachments(getAttachmentFileIdsFromEditor(editor).length > 0); + }, [editor]); + + const handleAttach = useCallback(() => { + pickAndInsertAttachments(editor); }, [editor]); const handleSubmit = useCallback(async () => { - const trimmed = instruction.trim(); - if (!trimmed) return; + const markdown = String(editor?.getDocument?.('markdown') ?? '').trim(); + const trimmedText = instruction.trim(); + const hasFiles = getAttachmentFileIdsFromEditor(editor).length > 0; + if (!trimmedText && !markdown && !hasFiles) return; const firstLine = - trimmed + trimmedText .split('\n') .find((line) => line.trim()) - ?.trim() ?? trimmed; - const name = firstLine.length > 30 ? `${firstLine.slice(0, 30)}…` : firstLine; + ?.trim() ?? trimmedText; + let name: string | undefined; + if (firstLine) { + name = firstLine.length > 30 ? `${firstLine.slice(0, 30)}…` : firstLine; + } + + const editorJson = editor?.getDocument?.('json') as unknown; const result = await createTask({ assigneeAgentId, - instruction: trimmed, + editorData: editorJson, + instruction: markdown || trimmedText || name || '', name, parentTaskId, priority: priority || undefined, @@ -220,10 +238,17 @@ const CreateTaskInlineEntry = memo((props) => { )} + +