diff --git a/.changelog/next/changed-issue-4153.md b/.changelog/next/changed-issue-4153.md new file mode 100644 index 0000000000..94affae05d --- /dev/null +++ b/.changelog/next/changed-issue-4153.md @@ -0,0 +1 @@ +- CoS tasks: split the overloaded `metadata.context` into `metadata.prompt` (the full agent-facing payload) and `metadata.context` (a one-line human note), with a reader fallback for pre-split tasks and a migration for existing queues (#4153) diff --git a/client/src/components/cos/tabs/TaskItem.jsx b/client/src/components/cos/tabs/TaskItem.jsx index d97ef9ff7e..d82bcf262f 100644 --- a/client/src/components/cos/tabs/TaskItem.jsx +++ b/client/src/components/cos/tabs/TaskItem.jsx @@ -72,8 +72,16 @@ export default function TaskItem({ task, isSystem, onRefresh, providers, duratio setEditingInternal(val); onEditingChange?.(val); }, [onEditingChange]); + // A task written since the #4153 split keeps its full agent-facing payload in + // `metadata.prompt` and only a short human note in `metadata.context`. Legacy + // tasks (and peers still on the old code) have no `prompt` at all — their + // payload is still in `context` — so the Prompt field is offered ONLY when the + // task actually carries one, and is omitted from the PATCH otherwise rather + // than writing an empty `prompt` key onto every task the user edits. + const hasPromptField = typeof task.metadata?.prompt === 'string'; const [editData, setEditData] = useState({ description: task.description, + prompt: task.metadata?.prompt || '', context: task.metadata?.context || '', model: task.metadata?.model || '', provider: task.metadata?.provider || '' @@ -172,7 +180,9 @@ export default function TaskItem({ task, isSystem, onRefresh, providers, duratio }; const handleSave = async () => { - const result = await api.updateCosTask(task.id, { ...editData, type: taskSource }, { silent: true }).catch(err => { + const { prompt, ...rest } = editData; + const payload = hasPromptField ? { ...rest, prompt, type: taskSource } : { ...rest, type: taskSource }; + const result = await api.updateCosTask(task.id, payload, { silent: true }).catch(err => { toast.error(err.message); return null; }); @@ -187,6 +197,7 @@ export default function TaskItem({ task, isSystem, onRefresh, providers, duratio // changed something, so an unmodified Cancel still discards with no friction. const hasUnsavedEdits = editData.description !== task.description || + (hasPromptField && editData.prompt !== (task.metadata?.prompt || '')) || editData.context !== (task.metadata?.context || '') || editData.model !== (task.metadata?.model || '') || editData.provider !== (task.metadata?.provider || ''); @@ -207,6 +218,7 @@ export default function TaskItem({ task, isSystem, onRefresh, providers, duratio confirmDiscard(() => { setEditData({ description: task.description, + prompt: task.metadata?.prompt || '', context: task.metadata?.context || '', model: task.metadata?.model || '', provider: task.metadata?.provider || '' @@ -339,13 +351,24 @@ export default function TaskItem({ task, isSystem, onRefresh, providers, duratio onChange={e => setEditData(d => ({ ...d, description: e.target.value }))} className="w-full px-2 py-1 bg-port-bg border border-port-border rounded text-white text-sm" /> - {/* A textarea, not an input: for orchestrator tasks the context - holds the task's entire multi-line prompt, which is unreadable - and unnavigable in a single-line field. Bounded rows + its own + {/* A textarea, not an input: for orchestrator tasks this holds the + task's entire multi-line prompt, which is unreadable and + unnavigable in a single-line field. Bounded rows + its own scroll so editing a long prompt doesn't stretch the card. */} + {hasPromptField && ( +