diff --git a/CHANGELOG.md b/CHANGELOG.md index 770be57643..021774944e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,16 @@ _In development — bullets added per PR; finalized at release._ +### 🔧 Bug Fixes + +- **Engine Combos editor: saving a pipeline no longer fails silently with HTTP 400 (#4955).** The + named-combos pipeline dropdown offered four engines (`headroom`, `session-dedup`, `ccr`, + `llmlingua`) that the `PUT /api/context/combos/[id]` schema rejects, so selecting one made the + save return 400 while the UI swallowed the error. The dropdown is now sourced from a single + canonical engine map shared with `stackedPipelineStepSchema` (parity guarded by a unit test), and + the editor surfaces save errors and empty-name/empty-pipeline validation instead of failing + quietly. + --- ## [3.8.36] — 2026-06-25 diff --git a/src/app/(dashboard)/dashboard/context/combos/CompressionCombosPageClient.tsx b/src/app/(dashboard)/dashboard/context/combos/CompressionCombosPageClient.tsx index a9b48c3e47..8bdfc4703d 100644 --- a/src/app/(dashboard)/dashboard/context/combos/CompressionCombosPageClient.tsx +++ b/src/app/(dashboard)/dashboard/context/combos/CompressionCombosPageClient.tsx @@ -8,6 +8,7 @@ // matching `EngineConfigPage` / `CompressionHub`, both of which hydrate cleanly. import { useEffect, useState } from "react"; +import { STACKED_PIPELINE_ENGINE_INTENSITIES } from "@/shared/validation/compressionConfigSchemas"; import CompressionHub from "./CompressionHub"; type PipelineStep = { engine: string; intensity?: string }; @@ -29,17 +30,9 @@ const EMPTY_PIPELINE: PipelineStep[] = [ { engine: "caveman", intensity: "full" }, ]; -const ENGINE_INTENSITIES: Record = { - rtk: ["minimal", "standard", "aggressive"], - caveman: ["lite", "full", "ultra"], - lite: ["lite"], - aggressive: ["standard"], - ultra: ["ultra"], - headroom: ["standard"], - "session-dedup": ["standard"], - ccr: ["standard"], - llmlingua: ["standard"], -}; +// Engine list is sourced from the API schema so the dropdown can never offer an engine +// the `PUT /api/context/combos/[id]` route would reject with HTTP 400 (#4955). +const ENGINE_INTENSITIES: Record = STACKED_PIPELINE_ENGINE_INTENSITIES; function NamedCombosManager() { const [combos, setCombos] = useState([]); @@ -55,6 +48,7 @@ function NamedCombosManager() { const [assignmentIds, setAssignmentIds] = useState([]); const [saving, setSaving] = useState(false); const [activeComboId, setActiveComboId] = useState(null); + const [error, setError] = useState(null); const refresh = () => { fetch("/api/context/combos") @@ -88,6 +82,7 @@ function NamedCombosManager() { setOutputMode(false); setOutputModeIntensity("full"); setAssignmentIds([]); + setError(null); }; const loadAssignments = async (id: string) => { @@ -112,7 +107,15 @@ function NamedCombosManager() { const saveCombo = async () => { const trimmed = name.trim(); - if (!trimmed) return; + if (!trimmed) { + setError("Enter a combo name before saving."); + return; + } + if (pipeline.length === 0) { + setError("Add at least one pipeline step before saving."); + return; + } + setError(null); setSaving(true); try { const payload = { @@ -131,7 +134,11 @@ function NamedCombosManager() { body: JSON.stringify(payload), } ); - if (!res.ok) return; + if (!res.ok) { + const body = await res.json().catch(() => null); + setError(body?.error || `Failed to save combo (HTTP ${res.status}).`); + return; + } const combo = await res.json(); await fetch(`/api/context/combos/${combo.id}/assignments`, { method: "PUT", @@ -315,6 +322,12 @@ function NamedCombosManager() { + {error && ( +

+ {error} +

+ )} +