// Task progress state helpers. Kept DOM-free so Node tests can cover the // "update_step without title" merge behavior used by the Web UI. export function cloneProgressSteps(steps) { return Array.isArray(steps) ? steps.map(s => ({ ...s })) : []; } export function normalizeProgressStatus(status) { return ["pending", "in_progress", "completed"].includes(status) ? status : "pending"; } export function normalizeProgressStep(step) { if (!step || typeof step !== "object") return null; const id = String(step.id || "").trim(); const title = String(step.title || "").trim(); const status = normalizeProgressStatus(step.status); if (!id || !title) return null; return { id, title, status }; } export function applyProgressAction(progress, args) { const current = cloneProgressSteps(progress); if (!args || typeof args !== "object") return current; const action = args.action || ""; if (action === "clear") return []; if (action === "set_plan") { return Array.isArray(args.steps) ? args.steps.map(normalizeProgressStep).filter(Boolean) : []; } if (action === "update_step") { const raw = args.step; if (!raw || typeof raw !== "object") return current; const id = String(raw.id || "").trim(); if (!id) return current; let found = false; const next = current.map((s) => { if (s.id !== id) return s; found = true; return { id: s.id, title: String(raw.title || s.title || "").trim(), status: normalizeProgressStatus(raw.status || s.status), }; }).filter(s => s.title); if (!found && raw.title) { next.push({ id, title: String(raw.title).trim(), status: normalizeProgressStatus(raw.status), }); } return next; } return current; } export function progressActionsFromToolCalls(toolCalls, baseSteps = []) { let steps = cloneProgressSteps(baseSteps); let sawProgress = false; if (!Array.isArray(toolCalls)) return { steps, sawProgress }; for (const tc of toolCalls) { const fn = (tc && tc.function && tc.function.name) || ""; if (fn !== "task_progress") continue; let args = {}; try { args = JSON.parse((tc.function && tc.function.arguments) || "{}"); } catch (e) { args = {}; } steps = applyProgressAction(steps, args); sawProgress = true; } return { steps, sawProgress }; }